From 4322f7d0b94b3405c50217ac8091d7ec09d21e13 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Tue, 21 Jun 2022 18:51:50 +0200 Subject: [PATCH 01/58] mempool: make error throwing for CheckTx consistent (#8817) --- internal/mempool/v0/clist_mempool.go | 8 ++------ internal/mempool/v0/clist_mempool_test.go | 10 +++++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/mempool/v0/clist_mempool.go b/internal/mempool/v0/clist_mempool.go index 0a12c7000..b37e745d8 100644 --- a/internal/mempool/v0/clist_mempool.go +++ b/internal/mempool/v0/clist_mempool.go @@ -242,17 +242,13 @@ func (mem *CListMempool) CheckTx( // so we only record the sender for txs still in the mempool. if e, ok := mem.txsMap.Load(tx.Key()); ok { memTx := e.(*clist.CElement).Value.(*mempoolTx) - _, loaded := memTx.senders.LoadOrStore(txInfo.SenderID, true) + memTx.senders.LoadOrStore(txInfo.SenderID, true) // TODO: consider punishing peer for dups, // its non-trivial since invalid txs can become valid, // but they can spam the same tx with little cost to them atm. - if loaded { - return types.ErrTxInCache - } } - mem.logger.Debug("tx exists already in cache", "tx_hash", tx.Hash()) - return nil + return types.ErrTxInCache } if ctx == nil { diff --git a/internal/mempool/v0/clist_mempool_test.go b/internal/mempool/v0/clist_mempool_test.go index 61ec543ef..774e32a96 100644 --- a/internal/mempool/v0/clist_mempool_test.go +++ b/internal/mempool/v0/clist_mempool_test.go @@ -200,7 +200,7 @@ func TestMempoolUpdate(t *testing.T) { err := mp.Update(1, []types.Tx{[]byte{0x01}}, abciResponses(1, abci.CodeTypeOK), nil, nil) require.NoError(t, err) err = mp.CheckTx(context.Background(), []byte{0x01}, nil, mempool.TxInfo{}) - require.NoError(t, err) + assert.Error(t, err) } // 2. Removes valid txs from the mempool @@ -305,11 +305,15 @@ func TestMempool_KeepInvalidTxsInCache(t *testing.T) { // a must be added to the cache err = mp.CheckTx(context.Background(), a, nil, mempool.TxInfo{}) - require.NoError(t, err) + if assert.Error(t, err) { + assert.Equal(t, types.ErrTxInCache, err) + } // b must remain in the cache err = mp.CheckTx(context.Background(), b, nil, mempool.TxInfo{}) - require.NoError(t, err) + if assert.Error(t, err) { + assert.Equal(t, types.ErrTxInCache, err) + } } // 2. An invalid transaction must remain in the cache From 034a9f84227bccb6936379c33d0338983081514c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 17:16:31 -0400 Subject: [PATCH 02/58] build(deps): Bump github.com/spf13/cobra from 1.4.0 to 1.5.0 (#8811) Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.4.0 to 1.5.0. - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](https://github.com/spf13/cobra/compare/v1.4.0...v1.5.0) --- updated-dependencies: - dependency-name: github.com/spf13/cobra dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thane Thomson --- go.mod | 2 +- go.sum | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index f4d5abc9c..a72269ebc 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/rs/zerolog v1.27.0 github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa - github.com/spf13/cobra v1.4.0 + github.com/spf13/cobra v1.5.0 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.2 github.com/tendermint/tm-db v0.6.6 diff --git a/go.sum b/go.sum index 435b316a0..ba72ebf98 100644 --- a/go.sum +++ b/go.sum @@ -232,6 +232,7 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/atomicfile v0.2.6 h1:FgYxYvGcqREApTY8Nxg8msM6P/KVKK3ob5h9FaRUTNg= github.com/creachadair/atomicfile v0.2.6/go.mod h1:BRq8Une6ckFneYXZQ+kO7p1ZZP3I2fzVzf28JxrIkBc= github.com/creachadair/command v0.0.0-20220426235536-a748effdf6a1/go.mod h1:bAM+qFQb/KwWyCc9MLC4U1jvn3XyakqP5QRkds5T6cY= @@ -1018,8 +1019,9 @@ github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= -github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= From e9c87a3c493d239cbe2f911550f367b8cb1ac8ff Mon Sep 17 00:00:00 2001 From: William Banfield <4561443+williambanfield@users.noreply.github.com> Date: Tue, 21 Jun 2022 20:20:04 -0400 Subject: [PATCH 03/58] remove dial wake change (#8824) --- internal/p2p/router.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/p2p/router.go b/internal/p2p/router.go index 015c8610d..1bfd014fc 100644 --- a/internal/p2p/router.go +++ b/internal/p2p/router.go @@ -574,11 +574,6 @@ func (r *Router) dialSleep(ctx context.Context) { } r.options.DialSleep(ctx) - - if !r.peerManager.HasDialedMaxPeers() { - r.peerManager.dialWaker.Wake() - } - } // acceptPeers accepts inbound connections from peers on the given transport, From 24701cd587d732c16fa31c7117ad91d48f1a3a71 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 21:27:28 -0400 Subject: [PATCH 04/58] p2p: more dial routines (#8827) (#8828) --- internal/p2p/router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/p2p/router.go b/internal/p2p/router.go index 1bfd014fc..81a8928a4 100644 --- a/internal/p2p/router.go +++ b/internal/p2p/router.go @@ -530,7 +530,7 @@ func (r *Router) routeChannel( func (r *Router) numConcurrentDials() int { if r.options.NumConcurrentDials == nil { - return runtime.NumCPU() + return runtime.NumCPU() * 32 } return r.options.NumConcurrentDials() From df9363c67c6835df96dad752a63b8693b7413c9b Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Wed, 22 Jun 2022 11:54:03 -0700 Subject: [PATCH 05/58] Prepare changelog for Release v0.35.7 (#8772) --- CHANGELOG.md | 20 ++++++++++++++++++++ CHANGELOG_PENDING.md | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d321b6d8e..1d467f29e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,29 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/cosmos). +## v0.35.7 + +June 16, 2022 + +### BUG FIXES + +- [p2p] [\#8692](https://github.com/tendermint/tendermint/pull/8692) scale the number of stored peers by the configured maximum connections (#8684) +- [rpc] [\#8715](https://github.com/tendermint/tendermint/pull/8715) always close http bodies (backport #8712) +- [p2p] [\#8760](https://github.com/tendermint/tendermint/pull/8760) accept should not abort on first error (backport #8759) + +### BREAKING CHANGES + +- P2P Protocol + + - [p2p] [\#8737](https://github.com/tendermint/tendermint/pull/8737) Introduce "inactive" peer label to avoid re-dialing incompatible peers. (@tychoish) + - [p2p] [\#8737](https://github.com/tendermint/tendermint/pull/8737) Increase frequency of dialing attempts to reduce latency for peer acquisition. (@tychoish) + - [p2p] [\#8737](https://github.com/tendermint/tendermint/pull/8737) Improvements to peer scoring and sorting to gossip a greater variety of peers during PEX. (@tychoish) + - [p2p] [\#8737](https://github.com/tendermint/tendermint/pull/8737) Track incoming and outgoing peers separately to ensure more peer slots open for incoming connections. (@tychoish) + ## v0.35.6 June 3, 2022 + ### FEATURES - [migrate] [\#8672](https://github.com/tendermint/tendermint/pull/8672) provide function for database production (backport #8614) (@tychoish) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 2e820b727..cbc727d49 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -2,7 +2,7 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/cosmos). -## v0.35.7 +## v0.35.8 Month DD, YYYY From 9daea43375e06816a44c30bdb7b02701df4a5f76 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Wed, 22 Jun 2022 15:16:58 -0700 Subject: [PATCH 06/58] Update default version marker. (#8844) --- version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version/version.go b/version/version.go index e2c8a6150..b0096fdca 100644 --- a/version/version.go +++ b/version/version.go @@ -10,7 +10,7 @@ const ( // TMVersionDefault is the used as the fallback version of Tendermint Core // when not using git describe. It is formatted with semantic versioning. - TMVersionDefault = "0.35.6" + TMVersionDefault = "0.35.7" // ABCISemVer is the semantic version of the ABCI library ABCISemVer = "0.17.0" From 8ef63fe3d90ae3ccc8cf0fb24236473105a56db9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jun 2022 10:46:51 -0400 Subject: [PATCH 07/58] e2e: report peer heights in error message (#8843) (#8853) (cherry picked from commit 52b2efb8274879468af9d032afd844b300b40f6e) Co-authored-by: Sam Kleinman --- test/e2e/runner/rpc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/runner/rpc.go b/test/e2e/runner/rpc.go index ad5fa7a64..f608742a8 100644 --- a/test/e2e/runner/rpc.go +++ b/test/e2e/runner/rpc.go @@ -22,7 +22,7 @@ func waitForHeight(ctx context.Context, testnet *e2e.Testnet, height int64) (*ty clients = map[string]*rpchttp.HTTP{} lastHeight int64 lastIncrease = time.Now() - nodesAtHeight = map[string]struct{}{} + nodesAtHeight = map[string]int64{} numRunningNodes int ) if height == 0 { @@ -86,7 +86,7 @@ func waitForHeight(ctx context.Context, testnet *e2e.Testnet, height int64) (*ty // add this node to the set of target // height nodes - nodesAtHeight[node.Name] = struct{}{} + nodesAtHeight[node.Name] = result.SyncInfo.LatestBlockHeight // if not all of the nodes that we // have clients for have reached the From 3398f3797910f43f4b8b7394b3a122009ed6e2df Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jun 2022 18:25:19 +0200 Subject: [PATCH 08/58] cmd: add tool for compaction of goleveldb (backport #8564) (#8675) --- CHANGELOG_PENDING.md | 2 + cmd/tendermint/commands/compact.go | 69 ++++++++++++++++++++++++++++++ cmd/tendermint/main.go | 1 + go.mod | 1 + 4 files changed, 73 insertions(+) create mode 100644 cmd/tendermint/commands/compact.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index cbc727d49..9c518842b 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -22,6 +22,8 @@ Special thanks to external contributors on this release: ### FEATURES +- [cli] [\#8675] Add command to force compact goleveldb databases (@cmwaters) + ### IMPROVEMENTS ### BUG FIXES diff --git a/cmd/tendermint/commands/compact.go b/cmd/tendermint/commands/compact.go new file mode 100644 index 000000000..591788e58 --- /dev/null +++ b/cmd/tendermint/commands/compact.go @@ -0,0 +1,69 @@ +package commands + +import ( + "errors" + "path/filepath" + "sync" + + "github.com/spf13/cobra" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/opt" + "github.com/syndtr/goleveldb/leveldb/util" + "github.com/tendermint/tendermint/libs/log" +) + +func MakeCompactDBCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "experimental-compact-goleveldb", + Short: "force compacts the tendermint storage engine (only GoLevelDB supported)", + Long: ` +This is a temporary utility command that performs a force compaction on the state +and blockstores to reduce disk space for a pruning node. This should only be run +once the node has stopped. This command will likely be omitted in the future after +the planned refactor to the storage engine. + +Currently, only GoLevelDB is supported. + `, + RunE: func(cmd *cobra.Command, args []string) error { + if config.DBBackend != "goleveldb" { + return errors.New("compaction is currently only supported with goleveldb") + } + + compactGoLevelDBs(config.RootDir, logger) + return nil + }, + } + + return cmd +} + +func compactGoLevelDBs(rootDir string, logger log.Logger) { + dbNames := []string{"state", "blockstore"} + o := &opt.Options{ + DisableSeeksCompaction: true, + } + wg := sync.WaitGroup{} + + for _, dbName := range dbNames { + dbName := dbName + wg.Add(1) + go func() { + defer wg.Done() + dbPath := filepath.Join(rootDir, "data", dbName+".db") + store, err := leveldb.OpenFile(dbPath, o) + if err != nil { + logger.Error("failed to initialize tendermint db", "path", dbPath, "err", err) + return + } + defer store.Close() + + logger.Info("starting compaction...", "db", dbPath) + + err = store.CompactRange(util.Range{Start: nil, Limit: nil}) + if err != nil { + logger.Error("failed to compact tendermint db", "path", dbPath, "err", err) + } + }() + } + wg.Wait() +} diff --git a/cmd/tendermint/main.go b/cmd/tendermint/main.go index b0ca549e5..0aea8567c 100644 --- a/cmd/tendermint/main.go +++ b/cmd/tendermint/main.go @@ -32,6 +32,7 @@ func main() { cmd.InspectCmd, cmd.RollbackStateCmd, cmd.MakeKeyMigrateCommand(), + cmd.MakeCompactDBCommand(), debug.DebugCmd, cli.NewCompletionCmd(rootCmd, true), ) diff --git a/go.mod b/go.mod index a72269ebc..282818d4b 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/spf13/cobra v1.5.0 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.2 + github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca github.com/tendermint/tm-db v0.6.6 github.com/vektra/mockery/v2 v2.13.1 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e From 6f4ef729641e718515a785f728fcf867fd6653f6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jun 2022 13:21:46 -0400 Subject: [PATCH 09/58] p2p: track peers by address (#8841) (#8855) (cherry picked from commit 436a38f8768f32c2abf3668e586019dcc3defb04) Co-authored-by: Sam Kleinman --- internal/p2p/peermanager.go | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/internal/p2p/peermanager.go b/internal/p2p/peermanager.go index c5a431b7e..65d0847e1 100644 --- a/internal/p2p/peermanager.go +++ b/internal/p2p/peermanager.go @@ -575,6 +575,10 @@ func (m *PeerManager) TryDialNext() (NodeAddress, error) { continue } + if id, ok := m.store.Resolve(addressInfo.Address); ok && (m.isConnected(id) || m.dialing[id]) { + continue + } + // We now have an eligible address to dial. If we're full but have // upgrade capacity (as checked above), we find a lower-scored peer // we can replace and mark it as upgrading so noone else claims it. @@ -1263,6 +1267,7 @@ func (m *PeerManager) retryDelay(failures uint32, persistent bool) time.Duration type peerStore struct { db dbm.DB peers map[types.NodeID]*peerInfo + index map[NodeAddress]types.NodeID ranked []*peerInfo // cache for Ranked(), nil invalidates cache } @@ -1282,6 +1287,7 @@ func newPeerStore(db dbm.DB) (*peerStore, error) { // loadPeers loads all peers from the database into memory. func (s *peerStore) loadPeers() error { peers := map[types.NodeID]*peerInfo{} + addrs := map[NodeAddress]types.NodeID{} start, end := keyPeerInfoRange() iter, err := s.db.Iterator(start, end) @@ -1301,11 +1307,18 @@ func (s *peerStore) loadPeers() error { return fmt.Errorf("invalid peer data: %w", err) } peers[peer.ID] = peer + for addr := range peer.AddressInfo { + // TODO maybe check to see if we've seen this + // addr before for a different peer, there + // could be duplicates. + addrs[addr] = peer.ID + } } if iter.Error() != nil { return iter.Error() } s.peers = peers + s.index = addrs s.ranked = nil // invalidate cache if populated return nil } @@ -1317,6 +1330,12 @@ func (s *peerStore) Get(id types.NodeID) (peerInfo, bool) { return peer.Copy(), ok } +// Resolve returns the peer ID for a given node address if known. +func (s *peerStore) Resolve(addr NodeAddress) (types.NodeID, bool) { + id, ok := s.index[addr] + return id, ok +} + // Set stores peer data. The input data will be copied, and can safely be reused // by the caller. func (s *peerStore) Set(peer peerInfo) error { @@ -1345,20 +1364,29 @@ func (s *peerStore) Set(peer peerInfo) error { // update the existing pointer address. *current = peer } + for addr := range peer.AddressInfo { + s.index[addr] = peer.ID + } return nil } // Delete deletes a peer, or does nothing if it does not exist. func (s *peerStore) Delete(id types.NodeID) error { - if _, ok := s.peers[id]; !ok { + peer, ok := s.peers[id] + if !ok { return nil } - if err := s.db.Delete(keyPeerInfo(id)); err != nil { - return err + for _, addr := range peer.AddressInfo { + delete(s.index, addr.Address) } delete(s.peers, id) s.ranked = nil + + if err := s.db.Delete(keyPeerInfo(id)); err != nil { + return err + } + return nil } From 2df4c2b19d21c408ad0a08496acdd55a24aedf33 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jun 2022 14:46:10 -0400 Subject: [PATCH 10/58] e2e: add tolerance to peer discovery test (#8849) (#8857) (cherry picked from commit fb209136f85083e1d491ac2589fff41dd80518cb) Co-authored-by: Callum Waters Co-authored-by: Sam Kleinman --- test/e2e/tests/net_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/e2e/tests/net_test.go b/test/e2e/tests/net_test.go index 081dcfdfc..0daf0cfd4 100644 --- a/test/e2e/tests/net_test.go +++ b/test/e2e/tests/net_test.go @@ -17,7 +17,9 @@ func TestNet_Peers(t *testing.T) { netInfo, err := client.NetInfo(ctx) require.NoError(t, err) - expectedPeers := len(node.Testnet.Nodes) + // FIXME: https://github.com/tendermint/tendermint/issues/8848 + // We should be able to assert that we can discover all peers in a network + expectedPeers := len(node.Testnet.Nodes) - 1 // includes extra tolerance peers := make(map[string]*e2e.Node, 0) seen := map[string]bool{} for _, n := range node.Testnet.Nodes { @@ -30,7 +32,7 @@ func TestNet_Peers(t *testing.T) { seen[n.Name] = false } - require.Equal(t, expectedPeers, netInfo.NPeers, + require.GreaterOrEqual(t, netInfo.NPeers, expectedPeers, "node is not fully meshed with peers") for _, peerInfo := range netInfo.Peers { From 826f224c2dc1601eaaed6d70bc0f5a70d6d88871 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jun 2022 10:42:58 -0400 Subject: [PATCH 11/58] p2p: add eviction metrics and cleanup dialing error handling (backport #8819) (#8820) --- internal/p2p/metrics.go | 10 +++ internal/p2p/peermanager.go | 27 +++---- internal/p2p/peermanager_test.go | 132 +++++++++++-------------------- internal/p2p/router.go | 40 +++++----- internal/p2p/router_test.go | 7 -- test/e2e/runner/rpc.go | 2 +- 6 files changed, 91 insertions(+), 127 deletions(-) diff --git a/internal/p2p/metrics.go b/internal/p2p/metrics.go index 449f93bfc..35d8bb5ca 100644 --- a/internal/p2p/metrics.go +++ b/internal/p2p/metrics.go @@ -53,6 +53,9 @@ type Metrics struct { // this node. PeersConnectedIncoming metrics.Gauge + // Number of peers evicted by this node. + PeersEvicted metrics.Counter + // RouterPeerQueueRecv defines the time taken to read off of a peer's queue // before sending on the connection. RouterPeerQueueRecv metrics.Histogram @@ -110,6 +113,12 @@ func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { Name: "peers_connected_success", Help: "Number of successful peer connection attempts", }, labels).With(labelsAndValues...), + PeersEvicted: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "peers_evicted", + Help: "Number of connected peers evicted", + }, labels).With(labelsAndValues...), PeersConnectedFailure: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ Namespace: namespace, Subsystem: MetricsSubsystem, @@ -200,6 +209,7 @@ func NopMetrics() *Metrics { PeersConnectedIncoming: discard.NewGauge(), PeersConnectedOutgoing: discard.NewGauge(), PeersInactivated: discard.NewGauge(), + PeersEvicted: discard.NewCounter(), PeerReceiveBytesTotal: discard.NewCounter(), PeerSendBytesTotal: discard.NewCounter(), PeerPendingSendBytes: discard.NewGauge(), diff --git a/internal/p2p/peermanager.go b/internal/p2p/peermanager.go index 65d0847e1..f4213ce6f 100644 --- a/internal/p2p/peermanager.go +++ b/internal/p2p/peermanager.go @@ -534,12 +534,13 @@ func (m *PeerManager) HasDialedMaxPeers() bool { // returned peer. func (m *PeerManager) DialNext(ctx context.Context) (NodeAddress, error) { for { - address, err := m.TryDialNext() - if err != nil || (address != NodeAddress{}) { - return address, err + if address := m.TryDialNext(); (address != NodeAddress{}) { + return address, nil } + select { case <-m.dialWaker.Sleep(): + continue case <-ctx.Done(): return NodeAddress{}, ctx.Err() } @@ -548,21 +549,20 @@ func (m *PeerManager) DialNext(ctx context.Context) (NodeAddress, error) { // TryDialNext is equivalent to DialNext(), but immediately returns an empty // address if no peers or connection slots are available. -func (m *PeerManager) TryDialNext() (NodeAddress, error) { +func (m *PeerManager) TryDialNext() NodeAddress { m.mtx.Lock() defer m.mtx.Unlock() // We allow dialing MaxConnected+MaxConnectedUpgrade peers. Including // MaxConnectedUpgrade allows us to probe additional peers that have a // higher score than any other peers, and if successful evict it. - if m.options.MaxConnected > 0 && len(m.connected)+len(m.dialing) >= - int(m.options.MaxConnected)+int(m.options.MaxConnectedUpgrade) { - return NodeAddress{}, nil + if m.options.MaxConnected > 0 && len(m.connected)+len(m.dialing) >= int(m.options.MaxConnected)+int(m.options.MaxConnectedUpgrade) { + return NodeAddress{} } cinfo := m.getConnectedInfo() if m.options.MaxOutgoingConnections > 0 && cinfo.outgoing >= m.options.MaxOutgoingConnections { - return NodeAddress{}, nil + return NodeAddress{} } for _, peer := range m.store.Ranked() { @@ -589,16 +589,16 @@ func (m *PeerManager) TryDialNext() (NodeAddress, error) { if m.options.MaxConnected > 0 && len(m.connected) >= int(m.options.MaxConnected) { upgradeFromPeer := m.findUpgradeCandidate(peer.ID, peer.Score()) if upgradeFromPeer == "" { - return NodeAddress{}, nil + return NodeAddress{} } m.upgrading[upgradeFromPeer] = peer.ID } m.dialing[peer.ID] = true - return addressInfo.Address, nil + return addressInfo.Address } } - return NodeAddress{}, nil + return NodeAddress{} } // DialFailed reports a failed dial attempt. This will make the peer available @@ -706,8 +706,7 @@ func (m *PeerManager) Dialed(address NodeAddress) error { return err } - if upgradeFromPeer != "" && m.options.MaxConnected > 0 && - len(m.connected) >= int(m.options.MaxConnected) { + if upgradeFromPeer != "" && m.options.MaxConnected > 0 && len(m.connected) >= int(m.options.MaxConnected) { // Look for an even lower-scored peer that may have appeared since we // started the upgrade. if p, ok := m.store.Get(upgradeFromPeer); ok { @@ -716,11 +715,11 @@ func (m *PeerManager) Dialed(address NodeAddress) error { } } m.evict[upgradeFromPeer] = true + m.evictWaker.Wake() } m.metrics.PeersConnectedOutgoing.Add(1) m.connected[peer.ID] = peerConnectionOutgoing - m.evictWaker.Wake() return nil } diff --git a/internal/p2p/peermanager_test.go b/internal/p2p/peermanager_test.go index 372182cd0..7d4d8e027 100644 --- a/internal/p2p/peermanager_test.go +++ b/internal/p2p/peermanager_test.go @@ -378,16 +378,14 @@ func TestPeerManager_DialNext_WakeOnDialFailed(t *testing.T) { added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) // Add b. We shouldn't be able to dial it, due to MaxConnected. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Zero(t, dial) // Spawn a goroutine to fail a's dial attempt. @@ -415,8 +413,7 @@ func TestPeerManager_DialNext_WakeOnDialFailedRetry(t *testing.T) { added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) require.NoError(t, peerManager.DialFailed(dial)) failed := time.Now() @@ -443,8 +440,7 @@ func TestPeerManager_DialNext_WakeOnDisconnected(t *testing.T) { err = peerManager.Accepted(a.NodeID) require.NoError(t, err) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Zero(t, dial) go func() { @@ -473,8 +469,7 @@ func TestPeerManager_TryDialNext_MaxConnected(t *testing.T) { added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) require.NoError(t, peerManager.Dialed(a)) @@ -482,16 +477,14 @@ func TestPeerManager_TryDialNext_MaxConnected(t *testing.T) { added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Equal(t, b, dial) // At this point, adding c will not allow dialing it. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Zero(t, dial) } @@ -520,7 +513,7 @@ func TestPeerManager_TryDialNext_MaxConnectedUpgrade(t *testing.T) { added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() + dial := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.NoError(t, peerManager.Dialed(a)) @@ -529,8 +522,7 @@ func TestPeerManager_TryDialNext_MaxConnectedUpgrade(t *testing.T) { added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Equal(t, b, dial) // Even though we are at capacity, we should be allowed to dial c for an @@ -538,8 +530,7 @@ func TestPeerManager_TryDialNext_MaxConnectedUpgrade(t *testing.T) { added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Equal(t, c, dial) // However, since we're using all upgrade slots now, we can't add and dial @@ -547,16 +538,14 @@ func TestPeerManager_TryDialNext_MaxConnectedUpgrade(t *testing.T) { added, err = peerManager.Add(d) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Zero(t, dial) // We go through with c's upgrade. require.NoError(t, peerManager.Dialed(c)) // Still can't dial d. - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Zero(t, dial) // Now, if we disconnect a, we should be allowed to dial d because we have a @@ -572,8 +561,7 @@ func TestPeerManager_TryDialNext_MaxConnectedUpgrade(t *testing.T) { added, err = peerManager.Add(e) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Zero(t, dial) } @@ -593,8 +581,7 @@ func TestPeerManager_TryDialNext_UpgradeReservesPeer(t *testing.T) { added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) require.NoError(t, peerManager.Dialed(a)) @@ -602,8 +589,7 @@ func TestPeerManager_TryDialNext_UpgradeReservesPeer(t *testing.T) { added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Equal(t, b, dial) // Adding c and dialing it will fail, because a is the only connected @@ -611,8 +597,7 @@ func TestPeerManager_TryDialNext_UpgradeReservesPeer(t *testing.T) { added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Empty(t, dial) } @@ -633,22 +618,19 @@ func TestPeerManager_TryDialNext_DialingConnected(t *testing.T) { added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) // Adding a's TCP address will not dispense a, since it's already dialing. added, err = peerManager.Add(aTCP) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Zero(t, dial) // Marking a as dialed will still not dispense it. require.NoError(t, peerManager.Dialed(a)) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Zero(t, dial) // Adding b and accepting a connection from it will not dispense it either. @@ -656,8 +638,7 @@ func TestPeerManager_TryDialNext_DialingConnected(t *testing.T) { require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(bID)) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Zero(t, dial) } @@ -683,16 +664,14 @@ func TestPeerManager_TryDialNext_Multiple(t *testing.T) { // All addresses should be dispensed as long as dialing them has failed. dial := []p2p.NodeAddress{} for range addresses { - address, err := peerManager.TryDialNext() - require.NoError(t, err) + address := peerManager.TryDialNext() require.NotZero(t, address) require.NoError(t, peerManager.DialFailed(address)) dial = append(dial, address) } require.ElementsMatch(t, dial, addresses) - address, err := peerManager.TryDialNext() - require.NoError(t, err) + address := peerManager.TryDialNext() require.Zero(t, address) } @@ -714,15 +693,14 @@ func TestPeerManager_DialFailed(t *testing.T) { // Dialing and then calling DialFailed with a different address (same // NodeID) should unmark as dialing and allow us to dial the other address // again, but not register the failed address. - dial, err := peerManager.TryDialNext() + dial := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.NoError(t, peerManager.DialFailed(p2p.NodeAddress{ Protocol: "tcp", NodeID: aID, Hostname: "localhost"})) require.Equal(t, []p2p.NodeAddress{a}, peerManager.Addresses(aID)) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Equal(t, a, dial) // Calling DialFailed on same address twice should be fine. @@ -753,8 +731,7 @@ func TestPeerManager_DialFailed_UnreservePeer(t *testing.T) { added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) require.NoError(t, peerManager.Dialed(a)) @@ -762,8 +739,7 @@ func TestPeerManager_DialFailed_UnreservePeer(t *testing.T) { added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Equal(t, b, dial) // Adding c and dialing it will fail, even though it could upgrade a and we @@ -772,14 +748,12 @@ func TestPeerManager_DialFailed_UnreservePeer(t *testing.T) { added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Empty(t, dial) // Failing b's dial will now make c available for dialing. require.NoError(t, peerManager.DialFailed(b)) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Equal(t, c, dial) } @@ -794,8 +768,7 @@ func TestPeerManager_Dialed_Connected(t *testing.T) { added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) require.NoError(t, peerManager.Dialed(a)) @@ -805,8 +778,7 @@ func TestPeerManager_Dialed_Connected(t *testing.T) { added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Equal(t, b, dial) require.NoError(t, peerManager.Accepted(b.NodeID)) @@ -835,8 +807,7 @@ func TestPeerManager_Dialed_MaxConnected(t *testing.T) { added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) // Marking b as dialed in the meanwhile (even without TryDialNext) @@ -878,8 +849,7 @@ func TestPeerManager_Dialed_MaxConnectedUpgrade(t *testing.T) { added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, c, dial) require.NoError(t, peerManager.Dialed(c)) @@ -923,8 +893,7 @@ func TestPeerManager_Dialed_Upgrade(t *testing.T) { added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, b, dial) require.NoError(t, peerManager.Dialed(b)) @@ -933,8 +902,7 @@ func TestPeerManager_Dialed_Upgrade(t *testing.T) { added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Empty(t, dial) // a should now be evicted. @@ -977,8 +945,7 @@ func TestPeerManager_Dialed_UpgradeEvenLower(t *testing.T) { added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, c, dial) // In the meanwhile, a disconnects and d connects. d is even lower-scored @@ -1028,7 +995,7 @@ func TestPeerManager_Dialed_UpgradeNoEvict(t *testing.T) { added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() + dial := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, c, dial) @@ -1074,8 +1041,7 @@ func TestPeerManager_Accepted(t *testing.T) { added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, c, dial) require.NoError(t, peerManager.Accepted(c.NodeID)) require.Error(t, peerManager.Dialed(c)) @@ -1084,8 +1050,7 @@ func TestPeerManager_Accepted(t *testing.T) { added, err = peerManager.Add(d) require.NoError(t, err) require.True(t, added) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Equal(t, d, dial) require.NoError(t, peerManager.Dialed(d)) require.Error(t, peerManager.Accepted(d.NodeID)) @@ -1233,8 +1198,7 @@ func TestPeerManager_Accepted_UpgradeDialing(t *testing.T) { added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, b, dial) // a has already been claimed as an upgrade of a, so accepting @@ -1394,8 +1358,7 @@ func TestPeerManager_EvictNext_WakeOnUpgradeDialed(t *testing.T) { added, err := peerManager.Add(b) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, b, dial) require.NoError(t, peerManager.Dialed(b)) }() @@ -1521,13 +1484,11 @@ func TestPeerManager_Disconnected(t *testing.T) { // Disconnecting a dialing peer does not unmark it as dialing, to avoid // dialing it multiple times in parallel. - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) peerManager.Disconnected(a.NodeID) - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Zero(t, dial) } @@ -1595,8 +1556,7 @@ func TestPeerManager_Subscribe(t *testing.T) { require.Equal(t, p2p.PeerUpdate{NodeID: a.NodeID, Status: p2p.PeerStatusDown}, <-sub.Updates()) // Outbound connection with peer error and eviction. - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) require.Empty(t, sub.Updates()) @@ -1619,8 +1579,7 @@ func TestPeerManager_Subscribe(t *testing.T) { require.Equal(t, p2p.PeerUpdate{NodeID: a.NodeID, Status: p2p.PeerStatusDown}, <-sub.Updates()) // Outbound connection with dial failure. - dial, err = peerManager.TryDialNext() - require.NoError(t, err) + dial = peerManager.TryDialNext() require.Equal(t, a, dial) require.Empty(t, sub.Updates()) @@ -1716,8 +1675,7 @@ func TestPeerManager_Close(t *testing.T) { added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) - dial, err := peerManager.TryDialNext() - require.NoError(t, err) + dial := peerManager.TryDialNext() require.Equal(t, a, dial) require.NoError(t, peerManager.DialFailed(a)) diff --git a/internal/p2p/router.go b/internal/p2p/router.go index 81a8928a4..3e52f4c05 100644 --- a/internal/p2p/router.go +++ b/internal/p2p/router.go @@ -291,7 +291,7 @@ func NewRouter( router := &Router{ logger: logger, - metrics: metrics, + metrics: NopMetrics(), nodeInfo: nodeInfo, privKey: privKey, connTracker: newConnTracker( @@ -312,6 +312,10 @@ func NewRouter( router.BaseService = service.NewBaseService(logger, "router", router) + if metrics != nil { + router.metrics = metrics + } + qf, err := router.createQueueFactory() if err != nil { return nil, err @@ -422,11 +426,7 @@ func (r *Router) routeChannel( ) { for { select { - case envelope, ok := <-outCh: - if !ok { - return - } - + case envelope := <-outCh: // Mark the envelope with the channel ID to allow sendPeer() to pass // it on to Transport.SendMessage(). envelope.channelID = chID @@ -503,20 +503,22 @@ func (r *Router) routeChannel( } } - case peerError, ok := <-errCh: - if !ok { - return - } - - shouldEvict := peerError.Fatal || r.peerManager.HasMaxPeerCapacity() + case peerError := <-errCh: + maxPeerCapacity := r.peerManager.HasMaxPeerCapacity() r.logger.Error("peer error", "peer", peerError.NodeID, "err", peerError.Err, - "evicting", shouldEvict, + "disconnecting", peerError.Fatal || maxPeerCapacity, ) - if shouldEvict { + + if peerError.Fatal || maxPeerCapacity { + // if the error is fatal or all peer + // slots are in use, we can error + // (disconnect) from the peer. r.peerManager.Errored(peerError.NodeID, peerError.Err) } else { + // this just decrements the peer + // score. r.peerManager.processPeerEvent(PeerUpdate{ NodeID: peerError.NodeID, Status: PeerStatusBad, @@ -697,9 +699,8 @@ LOOP: case errors.Is(err, context.Canceled): r.logger.Debug("stopping dial routine") break LOOP - case err != nil: - r.logger.Error("failed to find next peer to dial", "err", err) - break LOOP + case address == NodeAddress{}: + continue LOOP } select { @@ -709,7 +710,7 @@ LOOP: // create connections too quickly. r.dialSleep(ctx) - continue + continue LOOP case <-ctx.Done(): close(addresses) break LOOP @@ -748,6 +749,7 @@ func (r *Router) connectPeer(ctx context.Context, address NodeAddress) { if err := r.runWithPeerMutex(func() error { return r.peerManager.Dialed(address) }); err != nil { r.logger.Error("failed to dial peer", "op", "outgoing/dialing", "peer", address.NodeID, "err", err) + r.peerManager.dialWaker.Wake() conn.Close() return } @@ -1035,6 +1037,8 @@ func (r *Router) evictPeers() { queue, ok := r.peerQueues[peerID] r.peerMtx.RUnlock() + r.metrics.PeersEvicted.Add(1) + if ok { queue.close() } diff --git a/internal/p2p/router_test.go b/internal/p2p/router_test.go index e8494fdf4..317750d03 100644 --- a/internal/p2p/router_test.go +++ b/internal/p2p/router_test.go @@ -133,13 +133,6 @@ func TestRouter_Channel_Basic(t *testing.T) { require.NoError(t, err) require.Contains(t, router.NodeInfo().Channels, chDesc2.ID) - // Closing the channel, then opening it again should be fine. - channel.Close() - time.Sleep(100 * time.Millisecond) // yes yes, but Close() is async... - - channel, err = router.OpenChannel(chDesc, &p2ptest.Message{}, 0) - require.NoError(t, err) - // We should be able to send on the channel, even though there are no peers. p2ptest.RequireSend(t, channel, p2p.Envelope{ To: types.NodeID(strings.Repeat("a", 40)), diff --git a/test/e2e/runner/rpc.go b/test/e2e/runner/rpc.go index f608742a8..0bf982480 100644 --- a/test/e2e/runner/rpc.go +++ b/test/e2e/runner/rpc.go @@ -111,7 +111,7 @@ func waitForHeight(ctx context.Context, testnet *e2e.Testnet, height int64) (*ty if len(clients) == 0 { return nil, nil, errors.New("unable to connect to any network nodes") } - if time.Since(lastIncrease) >= time.Minute { + if time.Since(lastIncrease) >= 90*time.Second { if lastHeight == 0 { return nil, nil, errors.New("chain stalled at unknown height (most likely upon starting)") } From 19b98c700585a4b329cf479f5cef81f726b6020e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jun 2022 13:22:26 -0400 Subject: [PATCH 12/58] e2e: disable another network test (#8862) (#8873) Follow up on: https://github.com/tendermint/tendermint/pull/8849 (cherry picked from commit c4d24eed7d0b8902c41d790e1de446baa0256e6b) Co-authored-by: Callum Waters --- test/e2e/tests/net_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/e2e/tests/net_test.go b/test/e2e/tests/net_test.go index 0daf0cfd4..9dafd00d3 100644 --- a/test/e2e/tests/net_test.go +++ b/test/e2e/tests/net_test.go @@ -19,7 +19,7 @@ func TestNet_Peers(t *testing.T) { // FIXME: https://github.com/tendermint/tendermint/issues/8848 // We should be able to assert that we can discover all peers in a network - expectedPeers := len(node.Testnet.Nodes) - 1 // includes extra tolerance + expectedPeers := len(node.Testnet.Nodes) peers := make(map[string]*e2e.Node, 0) seen := map[string]bool{} for _, n := range node.Testnet.Nodes { @@ -32,7 +32,7 @@ func TestNet_Peers(t *testing.T) { seen[n.Name] = false } - require.GreaterOrEqual(t, netInfo.NPeers, expectedPeers, + require.GreaterOrEqual(t, netInfo.NPeers, expectedPeers-1, "node is not fully meshed with peers") for _, peerInfo := range netInfo.Peers { @@ -44,8 +44,10 @@ func TestNet_Peers(t *testing.T) { seen[peer.Name] = true } - for name := range seen { - require.True(t, seen[name], "node %v not peered with %v", node.Name, name) - } + // FIXME: https://github.com/tendermint/tendermint/issues/8848 + // We should be able to assert that we can discover all peers in a network + // for name := range seen { + // require.True(t, seen[name], "node %v not peered with %v", node.Name, name) + // } }) } From f19e52e6f2b79760a0e0fc9b50d8146102cd1ee2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jun 2022 09:13:46 -0400 Subject: [PATCH 13/58] build(deps): Bump styfle/cancel-workflow-action from 0.9.1 to 0.10.0 (#8882) Bumps [styfle/cancel-workflow-action](https://github.com/styfle/cancel-workflow-action) from 0.9.1 to 0.10.0. - [Release notes](https://github.com/styfle/cancel-workflow-action/releases) - [Commits](https://github.com/styfle/cancel-workflow-action/compare/0.9.1...0.10.0) --- updated-dependencies: - dependency-name: styfle/cancel-workflow-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/janitor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/janitor.yml b/.github/workflows/janitor.yml index e6bc45ec1..ceb21941d 100644 --- a/.github/workflows/janitor.yml +++ b/.github/workflows/janitor.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 3 steps: - - uses: styfle/cancel-workflow-action@0.9.1 + - uses: styfle/cancel-workflow-action@0.10.0 with: workflow_id: 1041851,1401230,2837803 access_token: ${{ github.token }} From c4ef56607178852fe0e1f4caa37409533935c96b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 27 Jun 2022 10:49:51 -0400 Subject: [PATCH 14/58] p2p: remove dial sleep and provide disconnect cooldown (backport #8839) (#8875) (cherry picked from commit 52b6dc19badf50938ae2b2a1d2e22813614e5ad5) --- internal/p2p/p2ptest/network.go | 3 +-- internal/p2p/peermanager.go | 34 +++++++++++++++++++++++++++---- internal/p2p/router.go | 36 --------------------------------- internal/p2p/router_test.go | 2 -- node/setup.go | 23 +++++++++++---------- 5 files changed, 43 insertions(+), 55 deletions(-) diff --git a/internal/p2p/p2ptest/network.go b/internal/p2p/p2ptest/network.go index 9115b70ee..7f760e968 100644 --- a/internal/p2p/p2ptest/network.go +++ b/internal/p2p/p2ptest/network.go @@ -1,7 +1,6 @@ package p2ptest import ( - "context" "math/rand" "testing" "time" @@ -254,7 +253,7 @@ func (n *Network) MakeNode(t *testing.T, opts NodeOptions) *Node { privKey, peerManager, []p2p.Transport{transport}, - p2p.RouterOptions{DialSleep: func(_ context.Context) {}}, + p2p.RouterOptions{}, ) require.NoError(t, err) require.NoError(t, router.Start()) diff --git a/internal/p2p/peermanager.go b/internal/p2p/peermanager.go index f4213ce6f..52d1cfe9f 100644 --- a/internal/p2p/peermanager.go +++ b/internal/p2p/peermanager.go @@ -162,6 +162,10 @@ type PeerManagerOptions struct { // retry times, to avoid thundering herds. 0 disables jitter. RetryTimeJitter time.Duration + // DisconnectCooldownPeriod is the amount of time after we + // disconnect from a peer before we'll consider dialing a new peer + DisconnectCooldownPeriod time.Duration + // PeerScores sets fixed scores for specific peers. It is mainly used // for testing. A score of 0 is ignored. PeerScores map[types.NodeID]PeerScore @@ -570,6 +574,10 @@ func (m *PeerManager) TryDialNext() NodeAddress { continue } + if !peer.LastDisconnected.IsZero() && time.Since(peer.LastDisconnected) < m.options.DisconnectCooldownPeriod { + continue + } + for _, addressInfo := range peer.AddressInfo { if time.Since(addressInfo.LastDialFailure) < m.retryDelay(addressInfo.DialFailures, peer.Persistent) { continue @@ -888,6 +896,22 @@ func (m *PeerManager) Disconnected(peerID types.NodeID) { delete(m.evicting, peerID) delete(m.ready, peerID) + if peer, ok := m.store.Get(peerID); ok { + peer.LastDisconnected = time.Now() + _ = m.store.Set(peer) + // launch a thread to ping the dialWaker when the + // disconnected peer can be dialed again. + go func() { + timer := time.NewTimer(m.options.DisconnectCooldownPeriod) + defer timer.Stop() + select { + case <-timer.C: + m.dialWaker.Wake() + case <-m.closeCh: + } + }() + } + if ready { m.broadcast(PeerUpdate{ NodeID: peerID, @@ -1474,9 +1498,10 @@ func (s *peerStore) Size() int { // peerInfo contains peer information stored in a peerStore. type peerInfo struct { - ID types.NodeID - AddressInfo map[NodeAddress]*peerAddressInfo - LastConnected time.Time + ID types.NodeID + AddressInfo map[NodeAddress]*peerAddressInfo + LastConnected time.Time + LastDisconnected time.Time // These fields are ephemeral, i.e. not persisted to the database. Persistent bool @@ -1516,8 +1541,8 @@ func peerInfoFromProto(msg *p2pproto.PeerInfo) (*peerInfo, error) { func (p *peerInfo) ToProto() *p2pproto.PeerInfo { msg := &p2pproto.PeerInfo{ ID: string(p.ID), - LastConnected: &p.LastConnected, Inactive: p.Inactive, + LastConnected: &p.LastConnected, } for _, addressInfo := range p.AddressInfo { msg.AddressInfo = append(msg.AddressInfo, addressInfo.ToProto()) @@ -1525,6 +1550,7 @@ func (p *peerInfo) ToProto() *p2pproto.PeerInfo { if msg.LastConnected.IsZero() { msg.LastConnected = nil } + return msg } diff --git a/internal/p2p/router.go b/internal/p2p/router.go index 3e52f4c05..6b056ae53 100644 --- a/internal/p2p/router.go +++ b/internal/p2p/router.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "math/rand" "net" "runtime" "sync" @@ -160,12 +159,6 @@ type RouterOptions struct { // return an error to reject the peer. FilterPeerByID func(context.Context, types.NodeID) error - // DialSleep controls the amount of time that the router - // sleeps between dialing peers. If not set, a default value - // is used that sleeps for a (random) amount of time up to 3 - // seconds between submitting each peer to be dialed. - DialSleep func(context.Context) - // NumConcrruentDials controls how many parallel go routines // are used to dial peers. This defaults to the value of // runtime.NumCPU. @@ -554,30 +547,6 @@ func (r *Router) filterPeersID(ctx context.Context, id types.NodeID) error { return r.options.FilterPeerByID(ctx, id) } -func (r *Router) dialSleep(ctx context.Context) { - if r.options.DialSleep == nil { - const ( - maxDialerInterval = 500 - minDialerInterval = 100 - ) - - // nolint:gosec // G404: Use of weak random number generator - dur := time.Duration(rand.Int63n(maxDialerInterval-minDialerInterval+1) + minDialerInterval) - - timer := time.NewTimer(dur * time.Millisecond) - defer timer.Stop() - - select { - case <-ctx.Done(): - case <-timer.C: - } - - return - } - - r.options.DialSleep(ctx) -} - // acceptPeers accepts inbound connections from peers on the given transport, // and spawns goroutines that route messages to/from them. func (r *Router) acceptPeers(transport Transport) { @@ -705,11 +674,6 @@ LOOP: select { case addresses <- address: - // this jitters the frequency that we call - // DialNext and prevents us from attempting to - // create connections too quickly. - - r.dialSleep(ctx) continue LOOP case <-ctx.Done(): close(addresses) diff --git a/internal/p2p/router_test.go b/internal/p2p/router_test.go index 317750d03..8bdc21cbb 100644 --- a/internal/p2p/router_test.go +++ b/internal/p2p/router_test.go @@ -1,7 +1,6 @@ package p2p_test import ( - "context" "errors" "fmt" "io" @@ -664,7 +663,6 @@ func TestRouter_DialPeers_Parallel(t *testing.T) { peerManager, []p2p.Transport{mockTransport}, p2p.RouterOptions{ - DialSleep: func(_ context.Context) {}, NumConcurrentDials: func() int { ncpu := runtime.NumCPU() if ncpu <= 3 { diff --git a/node/setup.go b/node/setup.go index 28a61a0d4..26a07e9ef 100644 --- a/node/setup.go +++ b/node/setup.go @@ -504,17 +504,18 @@ func createPeerManager( const maxUpgradeConns = 4 options := p2p.PeerManagerOptions{ - SelfAddress: selfAddr, - MaxConnected: maxConns, - MaxOutgoingConnections: maxOutgoingConns, - MaxConnectedUpgrade: maxUpgradeConns, - MaxPeers: maxUpgradeConns + 4*maxConns, - MinRetryTime: 250 * time.Millisecond, - MaxRetryTime: 30 * time.Minute, - MaxRetryTimePersistent: 5 * time.Minute, - RetryTimeJitter: 5 * time.Second, - PrivatePeers: privatePeerIDs, - Metrics: metrics, + SelfAddress: selfAddr, + MaxConnected: maxConns, + MaxOutgoingConnections: maxOutgoingConns, + MaxConnectedUpgrade: maxUpgradeConns, + DisconnectCooldownPeriod: 2 * time.Second, + MaxPeers: maxUpgradeConns + 4*maxConns, + MinRetryTime: 250 * time.Millisecond, + MaxRetryTime: 30 * time.Minute, + MaxRetryTimePersistent: 5 * time.Minute, + RetryTimeJitter: 5 * time.Second, + PrivatePeers: privatePeerIDs, + Metrics: metrics, } peers := []p2p.NodeAddress{} From 978f754ad3f928d5e067bf13037759affa911e3c Mon Sep 17 00:00:00 2001 From: William Banfield <4561443+williambanfield@users.noreply.github.com> Date: Tue, 28 Jun 2022 16:07:15 -0400 Subject: [PATCH 15/58] p2p: set empty timeouts to configed values. (manual backport of #8847) (#8869) * regenerate mocks using newer style * p2p: set empty timeouts to small values. (#8847) These timeouts default to 'do not time out' if they are not set. This times up resources, potentially indefinitely. If node on the other side of the the handshake is up but unresponsive, the[ handshake call](https://github.com/tendermint/tendermint/blob/edec79448aa1d62b84683b1b22e12e145dbdda7c/internal/p2p/router.go#L720) will _never_ return. * fix light client select statement --- abci/client/mocks/client.go | 15 ++++++++ internal/consensus/mocks/cons_sync_reactor.go | 15 ++++++++ internal/evidence/mocks/block_store.go | 15 ++++++++ internal/p2p/mocks/connection.go | 35 ++++++++++++++----- internal/p2p/mocks/peer.go | 15 ++++++++ internal/p2p/mocks/transport.go | 15 ++++++++ internal/p2p/peer_test.go | 6 ++-- internal/p2p/router.go | 8 +---- internal/p2p/router_test.go | 14 ++++---- internal/p2p/switch.go | 6 ++-- internal/p2p/switch_test.go | 8 ++--- internal/p2p/test_util.go | 2 +- internal/p2p/transport.go | 3 +- internal/p2p/transport_mconn.go | 19 ++++++++-- internal/p2p/transport_memory.go | 8 +++++ internal/p2p/transport_test.go | 12 +++---- internal/proxy/mocks/app_conn_consensus.go | 15 ++++++++ internal/proxy/mocks/app_conn_mempool.go | 15 ++++++++ internal/proxy/mocks/app_conn_query.go | 15 ++++++++ internal/proxy/mocks/app_conn_snapshot.go | 15 ++++++++ internal/state/indexer/mocks/event_sink.go | 15 ++++++++ internal/state/mocks/block_store.go | 15 ++++++++ internal/state/mocks/evidence_pool.go | 15 ++++++++ internal/state/mocks/store.go | 15 ++++++++ internal/statesync/mocks/state_provider.go | 15 ++++++++ light/client.go | 11 +++--- light/provider/mocks/provider.go | 15 ++++++++ light/rpc/mocks/light_client.go | 15 ++++++++ node/node.go | 4 ++- rpc/client/mocks/client.go | 15 ++++++++ scripts/mockery_generate.sh | 14 +++++++- 31 files changed, 354 insertions(+), 51 deletions(-) diff --git a/abci/client/mocks/client.go b/abci/client/mocks/client.go index 664646e61..cfe5ea7af 100644 --- a/abci/client/mocks/client.go +++ b/abci/client/mocks/client.go @@ -801,3 +801,18 @@ func (_m *Client) String() string { func (_m *Client) Wait() { _m.Called() } + +type NewClientT interface { + mock.TestingT + Cleanup(func()) +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewClient(t NewClientT) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/consensus/mocks/cons_sync_reactor.go b/internal/consensus/mocks/cons_sync_reactor.go index 5ac592f0d..2c694742b 100644 --- a/internal/consensus/mocks/cons_sync_reactor.go +++ b/internal/consensus/mocks/cons_sync_reactor.go @@ -26,3 +26,18 @@ func (_m *ConsSyncReactor) SetStateSyncingMetrics(_a0 float64) { func (_m *ConsSyncReactor) SwitchToConsensus(_a0 state.State, _a1 bool) { _m.Called(_a0, _a1) } + +type NewConsSyncReactorT interface { + mock.TestingT + Cleanup(func()) +} + +// NewConsSyncReactor creates a new instance of ConsSyncReactor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewConsSyncReactor(t NewConsSyncReactorT) *ConsSyncReactor { + mock := &ConsSyncReactor{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/evidence/mocks/block_store.go b/internal/evidence/mocks/block_store.go index ef3346b2a..b0c67ff87 100644 --- a/internal/evidence/mocks/block_store.go +++ b/internal/evidence/mocks/block_store.go @@ -57,3 +57,18 @@ func (_m *BlockStore) LoadBlockMeta(height int64) *types.BlockMeta { return r0 } + +type NewBlockStoreT interface { + mock.TestingT + Cleanup(func()) +} + +// NewBlockStore creates a new instance of BlockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewBlockStore(t NewBlockStoreT) *BlockStore { + mock := &BlockStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/p2p/mocks/connection.go b/internal/p2p/mocks/connection.go index 6c6174117..e5ba9584a 100644 --- a/internal/p2p/mocks/connection.go +++ b/internal/p2p/mocks/connection.go @@ -13,6 +13,8 @@ import ( p2p "github.com/tendermint/tendermint/internal/p2p" + time "time" + types "github.com/tendermint/tendermint/types" ) @@ -49,20 +51,20 @@ func (_m *Connection) FlushClose() error { return r0 } -// Handshake provides a mock function with given fields: _a0, _a1, _a2 -func (_m *Connection) Handshake(_a0 context.Context, _a1 types.NodeInfo, _a2 crypto.PrivKey) (types.NodeInfo, crypto.PubKey, error) { - ret := _m.Called(_a0, _a1, _a2) +// Handshake provides a mock function with given fields: _a0, _a1, _a2, _a3 +func (_m *Connection) Handshake(_a0 context.Context, _a1 time.Duration, _a2 types.NodeInfo, _a3 crypto.PrivKey) (types.NodeInfo, crypto.PubKey, error) { + ret := _m.Called(_a0, _a1, _a2, _a3) var r0 types.NodeInfo - if rf, ok := ret.Get(0).(func(context.Context, types.NodeInfo, crypto.PrivKey) types.NodeInfo); ok { - r0 = rf(_a0, _a1, _a2) + if rf, ok := ret.Get(0).(func(context.Context, time.Duration, types.NodeInfo, crypto.PrivKey) types.NodeInfo); ok { + r0 = rf(_a0, _a1, _a2, _a3) } else { r0 = ret.Get(0).(types.NodeInfo) } var r1 crypto.PubKey - if rf, ok := ret.Get(1).(func(context.Context, types.NodeInfo, crypto.PrivKey) crypto.PubKey); ok { - r1 = rf(_a0, _a1, _a2) + if rf, ok := ret.Get(1).(func(context.Context, time.Duration, types.NodeInfo, crypto.PrivKey) crypto.PubKey); ok { + r1 = rf(_a0, _a1, _a2, _a3) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(crypto.PubKey) @@ -70,8 +72,8 @@ func (_m *Connection) Handshake(_a0 context.Context, _a1 types.NodeInfo, _a2 cry } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, types.NodeInfo, crypto.PrivKey) error); ok { - r2 = rf(_a0, _a1, _a2) + if rf, ok := ret.Get(2).(func(context.Context, time.Duration, types.NodeInfo, crypto.PrivKey) error); ok { + r2 = rf(_a0, _a1, _a2, _a3) } else { r2 = ret.Error(2) } @@ -206,3 +208,18 @@ func (_m *Connection) TrySendMessage(_a0 p2p.ChannelID, _a1 []byte) (bool, error return r0, r1 } + +type NewConnectionT interface { + mock.TestingT + Cleanup(func()) +} + +// NewConnection creates a new instance of Connection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewConnection(t NewConnectionT) *Connection { + mock := &Connection{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/p2p/mocks/peer.go b/internal/p2p/mocks/peer.go index b905c1156..021c905f2 100644 --- a/internal/p2p/mocks/peer.go +++ b/internal/p2p/mocks/peer.go @@ -332,3 +332,18 @@ func (_m *Peer) TrySend(_a0 byte, _a1 []byte) bool { func (_m *Peer) Wait() { _m.Called() } + +type NewPeerT interface { + mock.TestingT + Cleanup(func()) +} + +// NewPeer creates a new instance of Peer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewPeer(t NewPeerT) *Peer { + mock := &Peer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/p2p/mocks/transport.go b/internal/p2p/mocks/transport.go index 82bd670cb..46d825501 100644 --- a/internal/p2p/mocks/transport.go +++ b/internal/p2p/mocks/transport.go @@ -119,3 +119,18 @@ func (_m *Transport) String() string { return r0 } + +type NewTransportT interface { + mock.TestingT + Cleanup(func()) +} + +// NewTransport creates a new instance of Transport. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewTransport(t NewTransportT) *Transport { + mock := &Transport{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/p2p/peer_test.go b/internal/p2p/peer_test.go index dfe7bc798..dad7b98b5 100644 --- a/internal/p2p/peer_test.go +++ b/internal/p2p/peer_test.go @@ -90,7 +90,7 @@ func createOutboundPeerAndPerformHandshake( if err != nil { return nil, err } - peerInfo, _, err := pc.conn.Handshake(context.Background(), ourNodeInfo, pk) + peerInfo, _, err := pc.conn.Handshake(context.Background(), 0, ourNodeInfo, pk) if err != nil { return nil, err } @@ -187,7 +187,7 @@ func (rp *remotePeer) Dial(addr *NetAddress) (net.Conn, error) { if err != nil { return nil, err } - _, _, err = pc.conn.Handshake(context.Background(), rp.nodeInfo(), rp.PrivKey) + _, _, err = pc.conn.Handshake(context.Background(), 0, rp.nodeInfo(), rp.PrivKey) if err != nil { return nil, err } @@ -213,7 +213,7 @@ func (rp *remotePeer) accept() { if err != nil { golog.Printf("Failed to create a peer: %+v", err) } - _, _, err = pc.conn.Handshake(context.Background(), rp.nodeInfo(), rp.PrivKey) + _, _, err = pc.conn.Handshake(context.Background(), 0, rp.nodeInfo(), rp.PrivKey) if err != nil { golog.Printf("Failed to handshake a peer: %+v", err) } diff --git a/internal/p2p/router.go b/internal/p2p/router.go index 6b056ae53..7247642ca 100644 --- a/internal/p2p/router.go +++ b/internal/p2p/router.go @@ -795,13 +795,7 @@ func (r *Router) handshakePeer( expectID types.NodeID, ) (types.NodeInfo, crypto.PubKey, error) { - if r.options.HandshakeTimeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, r.options.HandshakeTimeout) - defer cancel() - } - - peerInfo, peerKey, err := conn.Handshake(ctx, r.nodeInfo, r.privKey) + peerInfo, peerKey, err := conn.Handshake(ctx, r.options.HandshakeTimeout, r.nodeInfo, r.privKey) if err != nil { return peerInfo, peerKey, err } diff --git a/internal/p2p/router_test.go b/internal/p2p/router_test.go index 8bdc21cbb..556067d1f 100644 --- a/internal/p2p/router_test.go +++ b/internal/p2p/router_test.go @@ -344,7 +344,7 @@ func TestRouter_AcceptPeers(t *testing.T) { closer := tmsync.NewCloser() mockConnection := &mocks.Connection{} mockConnection.On("String").Maybe().Return("mock") - mockConnection.On("Handshake", mock.Anything, selfInfo, selfKey). + mockConnection.On("Handshake", mock.Anything, mock.Anything, selfInfo, selfKey). Return(tc.peerInfo, tc.peerKey, nil) mockConnection.On("Close").Run(func(_ mock.Arguments) { closer.Close() }).Return(nil) mockConnection.On("RemoteEndpoint").Return(p2p.Endpoint{}) @@ -454,7 +454,7 @@ func TestRouter_AcceptPeers_HeadOfLineBlocking(t *testing.T) { mockConnection := &mocks.Connection{} mockConnection.On("String").Maybe().Return("mock") - mockConnection.On("Handshake", mock.Anything, selfInfo, selfKey). + mockConnection.On("Handshake", mock.Anything, mock.Anything, selfInfo, selfKey). WaitUntil(closeCh).Return(types.NodeInfo{}, nil, io.EOF) mockConnection.On("Close").Return(nil) mockConnection.On("RemoteEndpoint").Return(p2p.Endpoint{}) @@ -535,7 +535,7 @@ func TestRouter_DialPeers(t *testing.T) { mockConnection := &mocks.Connection{} mockConnection.On("String").Maybe().Return("mock") if tc.dialErr == nil { - mockConnection.On("Handshake", mock.Anything, selfInfo, selfKey). + mockConnection.On("Handshake", mock.Anything, mock.Anything, selfInfo, selfKey). Return(tc.peerInfo, tc.peerKey, nil) mockConnection.On("Close").Run(func(_ mock.Arguments) { closer.Close() }).Return(nil) } @@ -622,7 +622,7 @@ func TestRouter_DialPeers_Parallel(t *testing.T) { mockConnection := &mocks.Connection{} mockConnection.On("String").Maybe().Return("mock") - mockConnection.On("Handshake", mock.Anything, selfInfo, selfKey). + mockConnection.On("Handshake", mock.Anything, mock.Anything, selfInfo, selfKey). WaitUntil(closeCh).Return(types.NodeInfo{}, nil, io.EOF) mockConnection.On("Close").Return(nil) @@ -701,7 +701,7 @@ func TestRouter_EvictPeers(t *testing.T) { mockConnection := &mocks.Connection{} mockConnection.On("String").Maybe().Return("mock") - mockConnection.On("Handshake", mock.Anything, selfInfo, selfKey). + mockConnection.On("Handshake", mock.Anything, mock.Anything, selfInfo, selfKey). Return(peerInfo, peerKey.PubKey(), nil) mockConnection.On("ReceiveMessage").WaitUntil(closeCh).Return(chID, nil, io.EOF) mockConnection.On("RemoteEndpoint").Return(p2p.Endpoint{}) @@ -770,7 +770,7 @@ func TestRouter_ChannelCompatability(t *testing.T) { mockConnection := &mocks.Connection{} mockConnection.On("String").Maybe().Return("mock") - mockConnection.On("Handshake", mock.Anything, selfInfo, selfKey). + mockConnection.On("Handshake", mock.Anything, mock.Anything, selfInfo, selfKey). Return(incompatiblePeer, peerKey.PubKey(), nil) mockConnection.On("RemoteEndpoint").Return(p2p.Endpoint{}) mockConnection.On("Close").Return(nil) @@ -819,7 +819,7 @@ func TestRouter_DontSendOnInvalidChannel(t *testing.T) { mockConnection := &mocks.Connection{} mockConnection.On("String").Maybe().Return("mock") - mockConnection.On("Handshake", mock.Anything, selfInfo, selfKey). + mockConnection.On("Handshake", mock.Anything, mock.Anything, selfInfo, selfKey). Return(peer, peerKey.PubKey(), nil) mockConnection.On("RemoteEndpoint").Return(p2p.Endpoint{}) mockConnection.On("Close").Return(nil) diff --git a/internal/p2p/switch.go b/internal/p2p/switch.go index 60c0c7deb..9e1b0311b 100644 --- a/internal/p2p/switch.go +++ b/internal/p2p/switch.go @@ -865,11 +865,11 @@ func (sw *Switch) handshakePeer( c Connection, expectPeerID types.NodeID, ) (types.NodeInfo, crypto.PubKey, error) { - // Moved from transport and hardcoded until legacy P2P stack removal. - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithCancel(context.Background()) defer cancel() - peerInfo, peerKey, err := c.Handshake(ctx, sw.nodeInfo, sw.nodeKey.PrivKey) + // Moved timeout from transport and hardcoded until legacy P2P stack removal. + peerInfo, peerKey, err := c.Handshake(ctx, 5*time.Second, sw.nodeInfo, sw.nodeKey.PrivKey) if err != nil { return peerInfo, peerKey, ErrRejected{ conn: c.(*mConnConnection).conn, diff --git a/internal/p2p/switch_test.go b/internal/p2p/switch_test.go index 8cb755c9f..c68cfceaf 100644 --- a/internal/p2p/switch_test.go +++ b/internal/p2p/switch_test.go @@ -267,7 +267,7 @@ func TestSwitchPeerFilter(t *testing.T) { if err != nil { t.Fatal(err) } - peerInfo, _, err := c.Handshake(ctx, sw.nodeInfo, sw.nodeKey.PrivKey) + peerInfo, _, err := c.Handshake(ctx, 0, sw.nodeInfo, sw.nodeKey.PrivKey) if err != nil { t.Fatal(err) } @@ -324,7 +324,7 @@ func TestSwitchPeerFilterTimeout(t *testing.T) { if err != nil { t.Fatal(err) } - peerInfo, _, err := c.Handshake(ctx, sw.nodeInfo, sw.nodeKey.PrivKey) + peerInfo, _, err := c.Handshake(ctx, 0, sw.nodeInfo, sw.nodeKey.PrivKey) if err != nil { t.Fatal(err) } @@ -360,7 +360,7 @@ func TestSwitchPeerFilterDuplicate(t *testing.T) { if err != nil { t.Fatal(err) } - peerInfo, _, err := c.Handshake(ctx, sw.nodeInfo, sw.nodeKey.PrivKey) + peerInfo, _, err := c.Handshake(ctx, 0, sw.nodeInfo, sw.nodeKey.PrivKey) if err != nil { t.Fatal(err) } @@ -415,7 +415,7 @@ func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { if err != nil { t.Fatal(err) } - peerInfo, _, err := c.Handshake(ctx, sw.nodeInfo, sw.nodeKey.PrivKey) + peerInfo, _, err := c.Handshake(ctx, 0, sw.nodeInfo, sw.nodeKey.PrivKey) if err != nil { t.Fatal(err) } diff --git a/internal/p2p/test_util.go b/internal/p2p/test_util.go index b2851646d..ae21ba4d7 100644 --- a/internal/p2p/test_util.go +++ b/internal/p2p/test_util.go @@ -126,7 +126,7 @@ func (sw *Switch) addPeerWithConnection(conn net.Conn) error { } return err } - peerNodeInfo, _, err := pc.conn.Handshake(context.Background(), sw.nodeInfo, sw.nodeKey.PrivKey) + peerNodeInfo, _, err := pc.conn.Handshake(context.Background(), 0, sw.nodeInfo, sw.nodeKey.PrivKey) if err != nil { if err := conn.Close(); err != nil { sw.Logger.Error("Error closing connection", "err", err) diff --git a/internal/p2p/transport.go b/internal/p2p/transport.go index a3245dfc8..2e4d26abd 100644 --- a/internal/p2p/transport.go +++ b/internal/p2p/transport.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "time" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/internal/p2p/conn" @@ -84,7 +85,7 @@ type Connection interface { // FIXME: The handshake should really be the Router's responsibility, but // that requires the connection interface to be byte-oriented rather than // message-oriented (see comment above). - Handshake(context.Context, types.NodeInfo, crypto.PrivKey) (types.NodeInfo, crypto.PubKey, error) + Handshake(context.Context, time.Duration, types.NodeInfo, crypto.PrivKey) (types.NodeInfo, crypto.PubKey, error) // ReceiveMessage returns the next message received on the connection, // blocking until one is available. Returns io.EOF if closed. diff --git a/internal/p2p/transport_mconn.go b/internal/p2p/transport_mconn.go index eca261476..4d18d896b 100644 --- a/internal/p2p/transport_mconn.go +++ b/internal/p2p/transport_mconn.go @@ -9,6 +9,7 @@ import ( "net" "strconv" "sync" + "time" "golang.org/x/net/netutil" @@ -255,6 +256,7 @@ func newMConnConnection( // Handshake implements Connection. func (c *mConnConnection) Handshake( ctx context.Context, + timeout time.Duration, nodeInfo types.NodeInfo, privKey crypto.PrivKey, ) (types.NodeInfo, crypto.PubKey, error) { @@ -264,6 +266,12 @@ func (c *mConnConnection) Handshake( peerKey crypto.PubKey errCh = make(chan error, 1) ) + handshakeCtx := ctx + if timeout > 0 { + var cancel context.CancelFunc + handshakeCtx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } // To handle context cancellation, we need to do the handshake in a // goroutine and abort the blocking network calls by closing the connection // when the context is canceled. @@ -276,12 +284,17 @@ func (c *mConnConnection) Handshake( } }() var err error - mconn, peerInfo, peerKey, err = c.handshake(ctx, nodeInfo, privKey) - errCh <- err + mconn, peerInfo, peerKey, err = c.handshake(handshakeCtx, nodeInfo, privKey) + + select { + case errCh <- err: + case <-handshakeCtx.Done(): + } + }() select { - case <-ctx.Done(): + case <-handshakeCtx.Done(): _ = c.Close() return types.NodeInfo{}, nil, ctx.Err() diff --git a/internal/p2p/transport_memory.go b/internal/p2p/transport_memory.go index 09a387254..f2d1d0c72 100644 --- a/internal/p2p/transport_memory.go +++ b/internal/p2p/transport_memory.go @@ -7,6 +7,7 @@ import ( "io" "net" "sync" + "time" "github.com/tendermint/tendermint/crypto" tmsync "github.com/tendermint/tendermint/internal/libs/sync" @@ -270,9 +271,16 @@ func (c *MemoryConnection) Status() conn.ConnectionStatus { // Handshake implements Connection. func (c *MemoryConnection) Handshake( ctx context.Context, + timeout time.Duration, nodeInfo types.NodeInfo, privKey crypto.PrivKey, ) (types.NodeInfo, crypto.PubKey, error) { + if timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } + select { case c.sendCh <- memoryMessage{nodeInfo: &nodeInfo, pubKey: privKey.PubKey()}: c.logger.Debug("sent handshake", "nodeInfo", nodeInfo) diff --git a/internal/p2p/transport_test.go b/internal/p2p/transport_test.go index 1b8ab77f5..de7405177 100644 --- a/internal/p2p/transport_test.go +++ b/internal/p2p/transport_test.go @@ -265,7 +265,7 @@ func TestConnection_Handshake(t *testing.T) { errCh := make(chan error, 1) go func() { // Must use assert due to goroutine. - peerInfo, peerKey, err := ba.Handshake(ctx, bInfo, bKey) + peerInfo, peerKey, err := ba.Handshake(ctx, 0, bInfo, bKey) if err == nil { assert.Equal(t, aInfo, peerInfo) assert.Equal(t, aKey.PubKey(), peerKey) @@ -273,7 +273,7 @@ func TestConnection_Handshake(t *testing.T) { errCh <- err }() - peerInfo, peerKey, err := ab.Handshake(ctx, aInfo, aKey) + peerInfo, peerKey, err := ab.Handshake(ctx, 0, aInfo, aKey) require.NoError(t, err) require.Equal(t, bInfo, peerInfo) require.Equal(t, bKey.PubKey(), peerKey) @@ -291,7 +291,7 @@ func TestConnection_HandshakeCancel(t *testing.T) { ab, ba := dialAccept(t, a, b) timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Minute) cancel() - _, _, err := ab.Handshake(timeoutCtx, types.NodeInfo{}, ed25519.GenPrivKey()) + _, _, err := ab.Handshake(timeoutCtx, 0, types.NodeInfo{}, ed25519.GenPrivKey()) require.Error(t, err) require.Equal(t, context.Canceled, err) _ = ab.Close() @@ -301,7 +301,7 @@ func TestConnection_HandshakeCancel(t *testing.T) { ab, ba = dialAccept(t, a, b) timeoutCtx, cancel = context.WithTimeout(ctx, 200*time.Millisecond) defer cancel() - _, _, err = ab.Handshake(timeoutCtx, types.NodeInfo{}, ed25519.GenPrivKey()) + _, _, err = ab.Handshake(timeoutCtx, 0, types.NodeInfo{}, ed25519.GenPrivKey()) require.Error(t, err) require.Equal(t, context.DeadlineExceeded, err) _ = ab.Close() @@ -630,13 +630,13 @@ func dialAcceptHandshake(t *testing.T, a, b p2p.Transport) (p2p.Connection, p2p. go func() { privKey := ed25519.GenPrivKey() nodeInfo := types.NodeInfo{NodeID: types.NodeIDFromPubKey(privKey.PubKey())} - _, _, err := ba.Handshake(ctx, nodeInfo, privKey) + _, _, err := ba.Handshake(ctx, 0, nodeInfo, privKey) errCh <- err }() privKey := ed25519.GenPrivKey() nodeInfo := types.NodeInfo{NodeID: types.NodeIDFromPubKey(privKey.PubKey())} - _, _, err := ab.Handshake(ctx, nodeInfo, privKey) + _, _, err := ab.Handshake(ctx, 0, nodeInfo, privKey) require.NoError(t, err) timer := time.NewTimer(2 * time.Second) diff --git a/internal/proxy/mocks/app_conn_consensus.go b/internal/proxy/mocks/app_conn_consensus.go index fa93b0931..bff6bf6a4 100644 --- a/internal/proxy/mocks/app_conn_consensus.go +++ b/internal/proxy/mocks/app_conn_consensus.go @@ -150,3 +150,18 @@ func (_m *AppConnConsensus) InitChainSync(_a0 context.Context, _a1 types.Request func (_m *AppConnConsensus) SetResponseCallback(_a0 abciclient.Callback) { _m.Called(_a0) } + +type NewAppConnConsensusT interface { + mock.TestingT + Cleanup(func()) +} + +// NewAppConnConsensus creates a new instance of AppConnConsensus. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAppConnConsensus(t NewAppConnConsensusT) *AppConnConsensus { + mock := &AppConnConsensus{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/proxy/mocks/app_conn_mempool.go b/internal/proxy/mocks/app_conn_mempool.go index 5429d8f90..a17deb7ca 100644 --- a/internal/proxy/mocks/app_conn_mempool.go +++ b/internal/proxy/mocks/app_conn_mempool.go @@ -118,3 +118,18 @@ func (_m *AppConnMempool) FlushSync(_a0 context.Context) error { func (_m *AppConnMempool) SetResponseCallback(_a0 abciclient.Callback) { _m.Called(_a0) } + +type NewAppConnMempoolT interface { + mock.TestingT + Cleanup(func()) +} + +// NewAppConnMempool creates a new instance of AppConnMempool. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAppConnMempool(t NewAppConnMempoolT) *AppConnMempool { + mock := &AppConnMempool{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/proxy/mocks/app_conn_query.go b/internal/proxy/mocks/app_conn_query.go index 47ac5bef9..9bde883a0 100644 --- a/internal/proxy/mocks/app_conn_query.go +++ b/internal/proxy/mocks/app_conn_query.go @@ -97,3 +97,18 @@ func (_m *AppConnQuery) QuerySync(_a0 context.Context, _a1 types.RequestQuery) ( return r0, r1 } + +type NewAppConnQueryT interface { + mock.TestingT + Cleanup(func()) +} + +// NewAppConnQuery creates a new instance of AppConnQuery. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAppConnQuery(t NewAppConnQueryT) *AppConnQuery { + mock := &AppConnQuery{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/proxy/mocks/app_conn_snapshot.go b/internal/proxy/mocks/app_conn_snapshot.go index 0b6f10ce1..36df29781 100644 --- a/internal/proxy/mocks/app_conn_snapshot.go +++ b/internal/proxy/mocks/app_conn_snapshot.go @@ -120,3 +120,18 @@ func (_m *AppConnSnapshot) OfferSnapshotSync(_a0 context.Context, _a1 types.Requ return r0, r1 } + +type NewAppConnSnapshotT interface { + mock.TestingT + Cleanup(func()) +} + +// NewAppConnSnapshot creates a new instance of AppConnSnapshot. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAppConnSnapshot(t NewAppConnSnapshotT) *AppConnSnapshot { + mock := &AppConnSnapshot{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/state/indexer/mocks/event_sink.go b/internal/state/indexer/mocks/event_sink.go index 98b32e935..ec5e279a8 100644 --- a/internal/state/indexer/mocks/event_sink.go +++ b/internal/state/indexer/mocks/event_sink.go @@ -165,3 +165,18 @@ func (_m *EventSink) Type() indexer.EventSinkType { return r0 } + +type NewEventSinkT interface { + mock.TestingT + Cleanup(func()) +} + +// NewEventSink creates a new instance of EventSink. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewEventSink(t NewEventSinkT) *EventSink { + mock := &EventSink{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/state/mocks/block_store.go b/internal/state/mocks/block_store.go index 563183437..b4d0c7f99 100644 --- a/internal/state/mocks/block_store.go +++ b/internal/state/mocks/block_store.go @@ -208,3 +208,18 @@ func (_m *BlockStore) Size() int64 { return r0 } + +type NewBlockStoreT interface { + mock.TestingT + Cleanup(func()) +} + +// NewBlockStore creates a new instance of BlockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewBlockStore(t NewBlockStoreT) *BlockStore { + mock := &BlockStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/state/mocks/evidence_pool.go b/internal/state/mocks/evidence_pool.go index 8bf4a9b64..8bc208289 100644 --- a/internal/state/mocks/evidence_pool.go +++ b/internal/state/mocks/evidence_pool.go @@ -68,3 +68,18 @@ func (_m *EvidencePool) PendingEvidence(maxBytes int64) ([]types.Evidence, int64 func (_m *EvidencePool) Update(_a0 state.State, _a1 types.EvidenceList) { _m.Called(_a0, _a1) } + +type NewEvidencePoolT interface { + mock.TestingT + Cleanup(func()) +} + +// NewEvidencePool creates a new instance of EvidencePool. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewEvidencePool(t NewEvidencePoolT) *EvidencePool { + mock := &EvidencePool{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/state/mocks/store.go b/internal/state/mocks/store.go index 02c69d3e0..d08ba4c9e 100644 --- a/internal/state/mocks/store.go +++ b/internal/state/mocks/store.go @@ -186,3 +186,18 @@ func (_m *Store) SaveValidatorSets(_a0 int64, _a1 int64, _a2 *types.ValidatorSet return r0 } + +type NewStoreT interface { + mock.TestingT + Cleanup(func()) +} + +// NewStore creates a new instance of Store. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewStore(t NewStoreT) *Store { + mock := &Store{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/statesync/mocks/state_provider.go b/internal/statesync/mocks/state_provider.go index b8d681631..17ddb54ac 100644 --- a/internal/statesync/mocks/state_provider.go +++ b/internal/statesync/mocks/state_provider.go @@ -82,3 +82,18 @@ func (_m *StateProvider) State(ctx context.Context, height uint64) (state.State, return r0, r1 } + +type NewStateProviderT interface { + mock.TestingT + Cleanup(func()) +} + +// NewStateProvider creates a new instance of StateProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewStateProvider(t NewStateProviderT) *StateProvider { + mock := &StateProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/light/client.go b/light/client.go index 5bff76894..32d4c669b 100644 --- a/light/client.go +++ b/light/client.go @@ -1018,7 +1018,12 @@ func (c *Client) findNewPrimary(ctx context.Context, height int64, remove bool) // process all the responses as they come in for i := 0; i < cap(witnessResponsesC); i++ { - response := <-witnessResponsesC + var response witnessResponse + select { + case response = <-witnessResponsesC: + case <-ctx.Done(): + return nil, ctx.Err() + } switch response.err { // success! We have found a new primary case nil: @@ -1047,10 +1052,6 @@ func (c *Client) findNewPrimary(ctx context.Context, height int64, remove bool) // return the light block that new primary responded with return response.lb, nil - // catch canceled contexts or deadlines - case context.Canceled, context.DeadlineExceeded: - return nil, response.err - // process benign errors by logging them only case provider.ErrNoResponse, provider.ErrLightBlockNotFound, provider.ErrHeightTooHigh: lastError = response.err diff --git a/light/provider/mocks/provider.go b/light/provider/mocks/provider.go index aa36fa2d3..9a5789ce9 100644 --- a/light/provider/mocks/provider.go +++ b/light/provider/mocks/provider.go @@ -51,3 +51,18 @@ func (_m *Provider) ReportEvidence(_a0 context.Context, _a1 types.Evidence) erro return r0 } + +type NewProviderT interface { + mock.TestingT + Cleanup(func()) +} + +// NewProvider creates a new instance of Provider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewProvider(t NewProviderT) *Provider { + mock := &Provider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/light/rpc/mocks/light_client.go b/light/rpc/mocks/light_client.go index cc32cf649..25e101c86 100644 --- a/light/rpc/mocks/light_client.go +++ b/light/rpc/mocks/light_client.go @@ -99,3 +99,18 @@ func (_m *LightClient) VerifyLightBlockAtHeight(ctx context.Context, height int6 return r0, r1 } + +type NewLightClientT interface { + mock.TestingT + Cleanup(func()) +} + +// NewLightClient creates a new instance of LightClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewLightClient(t NewLightClientT) *LightClient { + mock := &LightClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/node/node.go b/node/node.go index cd0f31396..e1de314d1 100644 --- a/node/node.go +++ b/node/node.go @@ -1246,7 +1246,9 @@ func createAndStartPrivValidatorGRPCClient( func getRouterConfig(conf *config.Config, proxyApp proxy.AppConns) p2p.RouterOptions { opts := p2p.RouterOptions{ - QueueType: conf.P2P.QueueType, + QueueType: conf.P2P.QueueType, + HandshakeTimeout: conf.P2P.HandshakeTimeout, + DialTimeout: conf.P2P.DialTimeout, } if conf.P2P.MaxNumInboundPeers > 0 { diff --git a/rpc/client/mocks/client.go b/rpc/client/mocks/client.go index 0a83ef201..0f1581f70 100644 --- a/rpc/client/mocks/client.go +++ b/rpc/client/mocks/client.go @@ -800,3 +800,18 @@ func (_m *Client) Validators(ctx context.Context, height *int64, page *int, perP return r0, r1 } + +type NewClientT interface { + mock.TestingT + Cleanup(func()) +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewClient(t NewClientT) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/scripts/mockery_generate.sh b/scripts/mockery_generate.sh index 382c277bb..2d6f40e63 100755 --- a/scripts/mockery_generate.sh +++ b/scripts/mockery_generate.sh @@ -1,3 +1,15 @@ #!/bin/sh +# +# Invoke Mockery v2 to update generated mocks for the given type. +# +# This script runs a locally-installed "mockery" if available, otherwise it +# runs the published Docker container. This legerdemain is so that the CI build +# and a local build can work off the same script. +# +if ! which mockery ; then + mockery() { + docker run --rm -v "$PWD":/w --workdir=/w vektra/mockery:v2.12.3 + } +fi -go run github.com/vektra/mockery/v2 --disable-version-string --case underscore --name $* +mockery --disable-version-string --case underscore --name "$@" From 486370ac68c05b2996383c25634e43296532558e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:26:28 -0400 Subject: [PATCH 16/58] log: do not pre-process log results (backport #8895) (#8896) (cherry picked from commit 37f9d59969b03d49dc1ff4191b4a5ddac2bc8d13) Co-authored-by: Sam Kleinman --- libs/log/default.go | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/libs/log/default.go b/libs/log/default.go index ca48fcd72..e66043ae7 100644 --- a/libs/log/default.go +++ b/libs/log/default.go @@ -75,7 +75,7 @@ func MustNewDefaultLogger(format, level string, trace bool) Logger { } func (l defaultLogger) Info(msg string, keyVals ...interface{}) { - l.Logger.Info().Fields(getLogFields(keyVals...)).Msg(msg) + l.Logger.Info().Fields(keyVals).Msg(msg) } func (l defaultLogger) Error(msg string, keyVals ...interface{}) { @@ -84,29 +84,16 @@ func (l defaultLogger) Error(msg string, keyVals ...interface{}) { e = e.Stack() } - e.Fields(getLogFields(keyVals...)).Msg(msg) + e.Fields(keyVals).Msg(msg) } func (l defaultLogger) Debug(msg string, keyVals ...interface{}) { - l.Logger.Debug().Fields(getLogFields(keyVals...)).Msg(msg) + l.Logger.Debug().Fields(keyVals).Msg(msg) } func (l defaultLogger) With(keyVals ...interface{}) Logger { return defaultLogger{ - Logger: l.Logger.With().Fields(getLogFields(keyVals...)).Logger(), + Logger: l.Logger.With().Fields(keyVals).Logger(), trace: l.trace, } } - -func getLogFields(keyVals ...interface{}) map[string]interface{} { - if len(keyVals)%2 != 0 { - return nil - } - - fields := make(map[string]interface{}, len(keyVals)) - for i := 0; i < len(keyVals); i += 2 { - fields[fmt.Sprint(keyVals[i])] = keyVals[i+1] - } - - return fields -} From 204281fa666b3ecc5df355748180f03709b0e803 Mon Sep 17 00:00:00 2001 From: Sam Kleinman Date: Wed, 29 Jun 2022 22:12:36 -0400 Subject: [PATCH 17/58] node: always start blocksync and avoid misconfiguration (#8902) --- node/node.go | 15 ++++++--------- node/setup.go | 5 +---- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/node/node.go b/node/node.go index e1de314d1..bea42b338 100644 --- a/node/node.go +++ b/node/node.go @@ -700,10 +700,8 @@ func (n *nodeImpl) OnStart() error { } if n.config.Mode != config.ModeSeed { - if n.config.BlockSync.Version == config.BlockSyncV0 { - if err := n.bcReactor.Start(); err != nil { - return err - } + if err := n.bcReactor.Start(); err != nil { + return err } // Start the real consensus reactor separately since the switch uses the shim. @@ -830,11 +828,10 @@ func (n *nodeImpl) OnStop() { if n.config.Mode != config.ModeSeed { // now stop the reactors - if n.config.BlockSync.Version == config.BlockSyncV0 { - // Stop the real blockchain reactor separately since the switch uses the shim. - if err := n.bcReactor.Stop(); err != nil { - n.Logger.Error("failed to stop the blockchain reactor", "err", err) - } + + // Stop the real blockchain reactor separately since the switch uses the shim. + if err := n.bcReactor.Stop(); err != nil { + n.Logger.Error("failed to stop the blockchain reactor", "err", err) } // Stop the real consensus reactor separately since the switch uses the shim. diff --git a/node/setup.go b/node/setup.go index 26a07e9ef..e94e85c78 100644 --- a/node/setup.go +++ b/node/setup.go @@ -17,7 +17,6 @@ import ( "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/crypto" bcv0 "github.com/tendermint/tendermint/internal/blocksync/v0" - bcv2 "github.com/tendermint/tendermint/internal/blocksync/v2" "github.com/tendermint/tendermint/internal/consensus" "github.com/tendermint/tendermint/internal/evidence" "github.com/tendermint/tendermint/internal/mempool" @@ -746,10 +745,8 @@ func makeNodeInfo( switch cfg.BlockSync.Version { case config.BlockSyncV0: bcChannel = byte(bcv0.BlockSyncChannel) - case config.BlockSyncV2: - bcChannel = bcv2.BlockchainChannel - + return types.NodeInfo{}, fmt.Errorf("unsupported blocksync version %s", cfg.BlockSync.Version) default: return types.NodeInfo{}, fmt.Errorf("unknown blocksync version %s", cfg.BlockSync.Version) } From e2d2c04aacb04b7c78de1854f7d04ca631eff6d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jun 2022 08:33:13 -0700 Subject: [PATCH 18/58] build(deps): Bump github.com/stretchr/testify from 1.7.2 to 1.8.0 (#8908) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.2 to 1.8.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.7.2...v1.8.0) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 282818d4b..5d5fba1ed 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa github.com/spf13/cobra v1.5.0 github.com/spf13/viper v1.12.0 - github.com/stretchr/testify v1.7.2 + github.com/stretchr/testify v1.8.0 github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca github.com/tendermint/tm-db v0.6.6 github.com/vektra/mockery/v2 v2.13.1 diff --git a/go.sum b/go.sum index ba72ebf98..a6dee3ad0 100644 --- a/go.sum +++ b/go.sum @@ -1042,8 +1042,9 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3 github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -1053,8 +1054,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= From 01984cb3b205c50ce5f1c9e3310a1e0aba09e82b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 30 Jun 2022 17:15:32 -0400 Subject: [PATCH 19/58] p2p: set outgoing connections to around 20% of total connections (#8913) (#8914) (cherry picked from commit 47cb30fc1d647183d500b0ecc31248df5dfc678c) Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com> --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index bebbf8d50..c4182fe48 100644 --- a/config/config.go +++ b/config/config.go @@ -778,7 +778,7 @@ func DefaultP2PConfig() *P2PConfig { MaxNumInboundPeers: 40, MaxNumOutboundPeers: 10, MaxConnections: 64, - MaxOutgoingConnections: 32, + MaxOutgoingConnections: 12, MaxIncomingConnectionAttempts: 100, PersistentPeersMaxDialPeriod: 0 * time.Second, FlushThrottleTimeout: 100 * time.Millisecond, From 6a646f366e1a6c5dc902cc0cfbbb5eb484b0f20b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Jul 2022 12:15:22 -0400 Subject: [PATCH 20/58] build(deps): Bump github.com/vektra/mockery/v2 from 2.13.1 to 2.14.0 (#8925) Bumps [github.com/vektra/mockery/v2](https://github.com/vektra/mockery) from 2.13.1 to 2.14.0. - [Release notes](https://github.com/vektra/mockery/releases) - [Changelog](https://github.com/vektra/mockery/blob/master/.goreleaser.yml) - [Commits](https://github.com/vektra/mockery/compare/v2.13.1...v2.14.0) --- updated-dependencies: - dependency-name: github.com/vektra/mockery/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5d5fba1ed..60dac969d 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/stretchr/testify v1.8.0 github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca github.com/tendermint/tm-db v0.6.6 - github.com/vektra/mockery/v2 v2.13.1 + github.com/vektra/mockery/v2 v2.14.0 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 diff --git a/go.sum b/go.sum index a6dee3ad0..d340e335c 100644 --- a/go.sum +++ b/go.sum @@ -1106,8 +1106,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/vektra/mockery/v2 v2.13.1 h1:Lqs7aZiC7TwZO76fJ/4Zsb3NaO4F7cuuz0mZLYeNwtQ= -github.com/vektra/mockery/v2 v2.13.1/go.mod h1:bnD1T8tExSgPD1ripLkDbr60JA9VtQeu12P3wgLZd7M= +github.com/vektra/mockery/v2 v2.14.0 h1:KZ1p5Hrn8tiY+LErRMr14HHle6khxo+JKOXLBW/yfqs= +github.com/vektra/mockery/v2 v2.14.0/go.mod h1:bnD1T8tExSgPD1ripLkDbr60JA9VtQeu12P3wgLZd7M= github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= From e414d0a87817229d4ab0c9cfb711d1278f7c14ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Jul 2022 12:19:03 +0200 Subject: [PATCH 21/58] build(deps): Bump github.com/libp2p/go-buffer-pool from 0.0.2 to 0.1.0 (#8931) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 60dac969d..66ce029a0 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/lib/pq v1.10.6 - github.com/libp2p/go-buffer-pool v0.0.2 + github.com/libp2p/go-buffer-pool v0.1.0 github.com/minio/highwayhash v1.0.2 github.com/mroth/weightedrand v0.4.1 github.com/oasisprotocol/curve25519-voi v0.0.0-20210609091139-0a56a4bca00b diff --git a/go.sum b/go.sum index d340e335c..7afca0a73 100644 --- a/go.sum +++ b/go.sum @@ -696,8 +696,8 @@ github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= -github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/lufeee/execinquery v1.0.0 h1:1XUTuLIVPDlFvUU3LXmmZwHDsolsxXnY67lzhpeqe0I= github.com/lufeee/execinquery v1.0.0/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= From 49788adde55cc7dc3d6090140e831737265700f0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 5 Jul 2022 13:36:26 -0400 Subject: [PATCH 22/58] p2p: use correct context error (#8916) (#8920) handshakeCtx is the internal context carrying the timeout. Its error should be used for the error return. (cherry picked from commit 921530c352d64229d0e78f6e1e14fe186d00b0db) Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com> Co-authored-by: Sam Kleinman Co-authored-by: Callum Waters --- internal/p2p/transport_mconn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/p2p/transport_mconn.go b/internal/p2p/transport_mconn.go index 4d18d896b..dbe9ab1c8 100644 --- a/internal/p2p/transport_mconn.go +++ b/internal/p2p/transport_mconn.go @@ -296,7 +296,7 @@ func (c *mConnConnection) Handshake( select { case <-handshakeCtx.Done(): _ = c.Close() - return types.NodeInfo{}, nil, ctx.Err() + return types.NodeInfo{}, nil, handshakeCtx.Err() case err := <-errCh: if err != nil { From 047d7c927bf4c1eb65fd38f8a011b3775aeae3c0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 5 Jul 2022 19:11:38 -0400 Subject: [PATCH 23/58] p2p: fix flakey test due to disconnect cooldown (#8917) (#8918) This test was made flakey by #8839. The cooldown period means that the node in the test will not try to reconnect as quickly as the test expects. This change makes the cooldown shorter in the test so that the node quickly reconnects. (cherry picked from commit 5274f80de437795c4782b52f73dcec76616eefe2) Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com> Co-authored-by: Sam Kleinman --- internal/p2p/p2ptest/network.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/p2p/p2ptest/network.go b/internal/p2p/p2ptest/network.go index 7f760e968..3aaa8d135 100644 --- a/internal/p2p/p2ptest/network.go +++ b/internal/p2p/p2ptest/network.go @@ -237,12 +237,13 @@ func (n *Network) MakeNode(t *testing.T, opts NodeOptions) *Node { require.Len(t, transport.Endpoints(), 1, "transport not listening on 1 endpoint") peerManager, err := p2p.NewPeerManager(nodeID, dbm.NewMemDB(), p2p.PeerManagerOptions{ - MinRetryTime: 10 * time.Millisecond, - MaxRetryTime: 100 * time.Millisecond, - RetryTimeJitter: time.Millisecond, - MaxPeers: opts.MaxPeers, - MaxConnected: opts.MaxConnected, - Metrics: p2p.NopMetrics(), + MinRetryTime: 10 * time.Millisecond, + DisconnectCooldownPeriod: 10 * time.Millisecond, + MaxRetryTime: 100 * time.Millisecond, + RetryTimeJitter: time.Millisecond, + MaxPeers: opts.MaxPeers, + MaxConnected: opts.MaxConnected, + Metrics: p2p.NopMetrics(), }) require.NoError(t, err) From da83edc5881846ece6705088165f514c0b460eab Mon Sep 17 00:00:00 2001 From: William Banfield <4561443+williambanfield@users.noreply.github.com> Date: Wed, 6 Jul 2022 10:41:55 -0400 Subject: [PATCH 24/58] p2p: return from conn send on stopped mconn (#8904) Co-authored-by: Sam Kleinman --- internal/p2p/conn/connection.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/p2p/conn/connection.go b/internal/p2p/conn/connection.go index 6a80417ad..0c05d8724 100644 --- a/internal/p2p/conn/connection.go +++ b/internal/p2p/conn/connection.go @@ -807,6 +807,8 @@ func (ch *Channel) sendBytes(bytes []byte) bool { return true case <-time.After(defaultSendTimeout): return false + case <-ch.conn.Quit(): + return false } } From 9b0209482736bf012df08ea9633fd542d7e52ceb Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Thu, 7 Jul 2022 07:15:08 -0700 Subject: [PATCH 25/58] Fix unbounded heap growth in the priority mempool. (#8944) The primary effect of this change is to simplify the implementation of the priority mempool to eliminate an unbounded heap growth observed by Vega team when it was enabled in their testnet. It updates and fixes #8775. The main body of this change is to remove the auxiliary indexing structures, and use only the concurrent list structure (the same as the legacy mempool) to maintain both gossip order and priority. This means that operations that require priority information, such as block updates and insert-time evictions, require a linear scan over the mempool. This tradeoff greatly simplifies the code and eliminates the long-term heap load, at the cost of some extra CPU and short-lived working memory during CheckTx and Update calls. Rough benchmark results: - This PR: BenchmarkTxMempool_CheckTx-10 486373 2271 ns/op - Original priority mempool implementation: BenchmarkTxMempool_CheckTx-10 500302 2113 ns/op - Legacy (v0) mempool: BenchmarkCheckTx-10 364591 3571 ns/op These benchmarks are not a good proxy for production load, but at least suggest that the overhead of the implementation changes are not cause for concern. In addition: - Rework synchronization so that access to shared data structures is safe. Previously shared locks were used to exclude block updates during calls that update mempool state. Now access is properly exclusive where necessary. - Fix a bug in the recheck flow, where priority updates from the application were not correctly reflected in the index structures. - Eliminate the need for separate recheck cursors during block update. This avoids the need to explicitly invalidate elements of the concurrent list, which averts the dependency cycle that led to objects being pinned. - Clean up, clarify, and fix inaccuracies in documentation comments throughout the package. Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com> --- internal/mempool/v1/mempool.go | 993 ++++++++++----------- internal/mempool/v1/mempool_test.go | 4 - internal/mempool/v1/priority_queue.go | 159 ---- internal/mempool/v1/priority_queue_test.go | 176 ---- internal/mempool/v1/reactor.go | 13 +- internal/mempool/v1/reactor_test.go | 2 + internal/mempool/v1/tx.go | 304 ++----- internal/mempool/v1/tx_test.go | 230 ----- 8 files changed, 519 insertions(+), 1362 deletions(-) delete mode 100644 internal/mempool/v1/priority_queue.go delete mode 100644 internal/mempool/v1/priority_queue_test.go delete mode 100644 internal/mempool/v1/tx_test.go diff --git a/internal/mempool/v1/mempool.go b/internal/mempool/v1/mempool.go index 82107cb22..9c4122d8e 100644 --- a/internal/mempool/v1/mempool.go +++ b/internal/mempool/v1/mempool.go @@ -1,22 +1,20 @@ package v1 import ( - "bytes" "context" - "errors" "fmt" "reflect" + "sort" + "sync" "sync/atomic" "time" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/internal/libs/clist" - tmsync "github.com/tendermint/tendermint/internal/libs/sync" "github.com/tendermint/tendermint/internal/mempool" "github.com/tendermint/tendermint/internal/proxy" "github.com/tendermint/tendermint/libs/log" - tmmath "github.com/tendermint/tendermint/libs/math" "github.com/tendermint/tendermint/types" ) @@ -25,73 +23,42 @@ var _ mempool.Mempool = (*TxMempool)(nil) // TxMempoolOption sets an optional parameter on the TxMempool. type TxMempoolOption func(*TxMempool) -// TxMempool defines a prioritized mempool data structure used by the v1 mempool -// reactor. It keeps a thread-safe priority queue of transactions that is used -// when a block proposer constructs a block and a thread-safe linked-list that -// is used to gossip transactions to peers in a FIFO manner. +// TxMempool implemements the Mempool interface and allows the application to +// set priority values on transactions in the CheckTx response. When selecting +// transactions to include in a block, higher-priority transactions are chosen +// first. When evicting transactions from the mempool for size constraints, +// lower-priority transactions are evicted sooner. +// +// Within the mempool, transactions are ordered by time of arrival, and are +// gossiped to the rest of the network based on that order (gossip order does +// not take priority into account). type TxMempool struct { + // Immutable fields logger log.Logger - metrics *mempool.Metrics config *config.MempoolConfig proxyAppConn proxy.AppConnMempool + metrics *mempool.Metrics + cache mempool.TxCache // seen transactions - // txsAvailable fires once for each height when the mempool is not empty - txsAvailable chan struct{} + // Atomically-updated fields + txsBytes int64 // atomic: the total size of all transactions in the mempool, in bytes + txRecheck int64 // atomic: the number of pending recheck calls + + // Synchronized fields, protected by mtx. + mtx *sync.RWMutex notifiedTxsAvailable bool + txsAvailable chan struct{} // one value sent per height when mempool is not empty + preCheck mempool.PreCheckFunc + postCheck mempool.PostCheckFunc + height int64 // the latest height passed to Update - // height defines the last block height process during Update() - height int64 - - // sizeBytes defines the total size of the mempool (sum of all tx bytes) - sizeBytes int64 - - // cache defines a fixed-size cache of already seen transactions as this - // reduces pressure on the proxyApp. - cache mempool.TxCache - - // txStore defines the main storage of valid transactions. Indexes are built - // on top of this store. - txStore *TxStore - - // gossipIndex defines the gossiping index of valid transactions via a - // thread-safe linked-list. We also use the gossip index as a cursor for - // rechecking transactions already in the mempool. - gossipIndex *clist.CList - - // recheckCursor and recheckEnd are used as cursors based on the gossip index - // to recheck transactions that are already in the mempool. Iteration is not - // thread-safe and transaction may be mutated in serial order. - // - // XXX/TODO: It might be somewhat of a codesmell to use the gossip index for - // iterator and cursor management when rechecking transactions. If the gossip - // index changes or is removed in a future refactor, this will have to be - // refactored. Instead, we should consider just keeping a slice of a snapshot - // of the mempool's current transactions during Update and an integer cursor - // into that slice. This, however, requires additional O(n) space complexity. - recheckCursor *clist.CElement // next expected response - recheckEnd *clist.CElement // re-checking stops here - - // priorityIndex defines the priority index of valid transactions via a - // thread-safe priority queue. - priorityIndex *TxPriorityQueue - - // heightIndex defines a height-based, in ascending order, transaction index. - // i.e. older transactions are first. - heightIndex *WrappedTxList - - // timestampIndex defines a timestamp-based, in ascending order, transaction - // index. i.e. older transactions are first. - timestampIndex *WrappedTxList - - // A read/write lock is used to safe guard updates, insertions and deletions - // from the mempool. A read-lock is implicitly acquired when executing CheckTx, - // however, a caller must explicitly grab a write-lock via Lock when updating - // the mempool via Update(). - mtx tmsync.RWMutex - preCheck mempool.PreCheckFunc - postCheck mempool.PostCheckFunc + txs *clist.CList // valid transactions (passed CheckTx) + txByKey map[types.TxKey]*clist.CElement + txBySender map[string]*clist.CElement // for sender != "" } +// NewTxMempool constructs a new, empty priority mempool at the specified +// initial height and using the given config and options. func NewTxMempool( logger log.Logger, cfg *config.MempoolConfig, @@ -101,28 +68,22 @@ func NewTxMempool( ) *TxMempool { txmp := &TxMempool{ - logger: logger, - config: cfg, - proxyAppConn: proxyAppConn, - height: height, - cache: mempool.NopTxCache{}, - metrics: mempool.NopMetrics(), - txStore: NewTxStore(), - gossipIndex: clist.New(), - priorityIndex: NewTxPriorityQueue(), - heightIndex: NewWrappedTxList(func(wtx1, wtx2 *WrappedTx) bool { - return wtx1.height >= wtx2.height - }), - timestampIndex: NewWrappedTxList(func(wtx1, wtx2 *WrappedTx) bool { - return wtx1.timestamp.After(wtx2.timestamp) || wtx1.timestamp.Equal(wtx2.timestamp) - }), + logger: logger, + config: cfg, + proxyAppConn: proxyAppConn, + metrics: mempool.NopMetrics(), + cache: mempool.NopTxCache{}, + txs: clist.New(), + mtx: new(sync.RWMutex), + height: height, + txByKey: make(map[types.TxKey]*clist.CElement), + txBySender: make(map[string]*clist.CElement), } - if cfg.CacheSize > 0 { txmp.cache = mempool.NewLRUTxCache(cfg.CacheSize) } - proxyAppConn.SetResponseCallback(txmp.defaultTxCallback) + proxyAppConn.SetResponseCallback(txmp.recheckTxCallback) for _, opt := range options { opt(txmp) @@ -152,47 +113,27 @@ func WithMetrics(metrics *mempool.Metrics) TxMempoolOption { // Lock obtains a write-lock on the mempool. A caller must be sure to explicitly // release the lock when finished. -func (txmp *TxMempool) Lock() { - txmp.mtx.Lock() -} +func (txmp *TxMempool) Lock() { txmp.mtx.Lock() } // Unlock releases a write-lock on the mempool. -func (txmp *TxMempool) Unlock() { - txmp.mtx.Unlock() -} +func (txmp *TxMempool) Unlock() { txmp.mtx.Unlock() } // Size returns the number of valid transactions in the mempool. It is // thread-safe. -func (txmp *TxMempool) Size() int { - return txmp.txStore.Size() -} +func (txmp *TxMempool) Size() int { return txmp.txs.Len() } // SizeBytes return the total sum in bytes of all the valid transactions in the // mempool. It is thread-safe. -func (txmp *TxMempool) SizeBytes() int64 { - return atomic.LoadInt64(&txmp.sizeBytes) -} +func (txmp *TxMempool) SizeBytes() int64 { return atomic.LoadInt64(&txmp.txsBytes) } // FlushAppConn executes FlushSync on the mempool's proxyAppConn. // -// NOTE: The caller must obtain a write-lock via Lock() prior to execution. +// The caller must hold an exclusive mempool lock (by calling txmp.Lock) before +// calling FlushAppConn. func (txmp *TxMempool) FlushAppConn() error { return txmp.proxyAppConn.FlushSync(context.Background()) } -// WaitForNextTx returns a blocking channel that will be closed when the next -// valid transaction is available to gossip. It is thread-safe. -func (txmp *TxMempool) WaitForNextTx() <-chan struct{} { - return txmp.gossipIndex.WaitChan() -} - -// NextGossipTx returns the next valid transaction to gossip. A caller must wait -// for WaitForNextTx to signal a transaction is available to gossip first. It is -// thread-safe. -func (txmp *TxMempool) NextGossipTx() *clist.CElement { - return txmp.gossipIndex.Front() -} - // EnableTxsAvailable enables the mempool to trigger events when transactions // are available on a block by block basis. func (txmp *TxMempool) EnableTxsAvailable() { @@ -204,30 +145,28 @@ func (txmp *TxMempool) EnableTxsAvailable() { // TxsAvailable returns a channel which fires once for every height, and only // when transactions are available in the mempool. It is thread-safe. -func (txmp *TxMempool) TxsAvailable() <-chan struct{} { - return txmp.txsAvailable -} +func (txmp *TxMempool) TxsAvailable() <-chan struct{} { return txmp.txsAvailable } -// CheckTx executes the ABCI CheckTx method for a given transaction. It acquires -// a read-lock attempts to execute the application's CheckTx ABCI method via -// CheckTxAsync. We return an error if any of the following happen: +// CheckTx adds the given transaction to the mempool if it fits and passes the +// application's ABCI CheckTx method. // -// - The CheckTxAsync execution fails. -// - The transaction already exists in the cache and we've already received the -// transaction from the peer. Otherwise, if it solely exists in the cache, we -// return nil. -// - The transaction size exceeds the maximum transaction size as defined by the -// configuration provided to the mempool. -// - The transaction fails Pre-Check (if it is defined). -// - The proxyAppConn fails, e.g. the buffer is full. +// CheckTx reports an error without adding tx if: // -// If the mempool is full, we still execute CheckTx and attempt to find a lower -// priority transaction to evict. If such a transaction exists, we remove the -// lower priority transaction and add the new one with higher priority. +// - The size of tx exceeds the configured maximum transaction size. +// - The pre-check hook is defined and reports an error for tx. +// - The transaction already exists in the cache. +// - The proxy connection to the application fails. // -// NOTE: -// - The applications' CheckTx implementation may panic. -// - The caller is not to explicitly require any locks for executing CheckTx. +// If tx passes all of the above conditions, it is passed (asynchronously) to +// the application's ABCI CheckTx method and this CheckTx method returns nil. +// If cb != nil, it is called when the ABCI request completes to report the +// application response. +// +// If the application accepts the transaction and the mempool is full, the +// mempool evicts one or more of the lowest-priority transaction whose priority +// is (strictly) lower than the priority of tx and whose size together exceeds +// the size of tx, and adds tx instead. If no such transactions exist, tx is +// discarded. func (txmp *TxMempool) CheckTx( ctx context.Context, tx types.Tx, @@ -235,202 +174,218 @@ func (txmp *TxMempool) CheckTx( txInfo mempool.TxInfo, ) error { + // During the initial phase of CheckTx, we do not need to modify any state. + // A transaction will not actually be added to the mempool until it survives + // a call to the ABCI CheckTx method and size constraint checks. txmp.mtx.RLock() defer txmp.mtx.RUnlock() - txSize := len(tx) - if txSize > txmp.config.MaxTxBytes { - return types.ErrTxTooLarge{ - Max: txmp.config.MaxTxBytes, - Actual: txSize, - } + // Reject transactions in excess of the configured maximum transaction size. + if len(tx) > txmp.config.MaxTxBytes { + return types.ErrTxTooLarge{Max: txmp.config.MaxTxBytes, Actual: len(tx)} } + // If a precheck hook is defined, call it before invoking the application. if txmp.preCheck != nil { if err := txmp.preCheck(tx); err != nil { - return types.ErrPreCheck{ - Reason: err, - } + return types.ErrPreCheck{Reason: err} } } + // Early exit if the proxy connection has an error. if err := txmp.proxyAppConn.Error(); err != nil { return err } - txHash := tx.Key() + txKey := tx.Key() - // We add the transaction to the mempool's cache and if the - // transaction is already present in the cache, i.e. false is returned, then we - // check if we've seen this transaction and error if we have. + // Check for the transaction in the cache. if !txmp.cache.Push(tx) { - txmp.txStore.GetOrSetPeerByTxHash(txHash, txInfo.SenderID) + // If the cached transaction is also in the pool, record its sender. + if elt, ok := txmp.txByKey[txKey]; ok { + w := elt.Value.(*WrappedTx) + w.SetPeer(txInfo.SenderID) + } return types.ErrTxInCache } - if ctx == nil { - ctx = context.Background() - } + // Initiate an ABCI CheckTx for this transaction. The callback is + // responsible for adding the transaction to the pool if it survives. + return func() error { + // N.B.: We have to issue the call outside the lock. In a local client, + // even an "async" call invokes its callback immediately which will make + // the callback deadlock trying to acquire the same lock. This isn't a + // problem with out-of-process calls, but this has to work for both. + height := txmp.height + txmp.mtx.RUnlock() + defer txmp.mtx.RLock() - reqRes, err := txmp.proxyAppConn.CheckTxAsync(ctx, abci.RequestCheckTx{Tx: tx}) - if err != nil { - txmp.cache.Remove(tx) - return err - } - - reqRes.SetCallback(func(res *abci.Response) { - if txmp.recheckCursor != nil { - panic("recheck cursor is non-nil in CheckTx callback") + reqRes, err := txmp.proxyAppConn.CheckTxAsync(ctx, abci.RequestCheckTx{Tx: tx}) + if err != nil { + txmp.cache.Remove(tx) + return err } - - wtx := &WrappedTx{ - tx: tx, - hash: txHash, - timestamp: time.Now().UTC(), - height: txmp.height, - } - txmp.initTxCallback(wtx, res, txInfo) - - if cb != nil { - cb(res) - } - }) - - return nil + reqRes.SetCallback(func(res *abci.Response) { + wtx := &WrappedTx{ + tx: tx, + hash: txKey, + timestamp: time.Now().UTC(), + height: height, + } + wtx.SetPeer(txInfo.SenderID) + txmp.initialTxCallback(wtx, res) + if cb != nil { + cb(res) + } + }) + return nil + }() } +// RemoveTxByKey removes the transaction with the specified key from the +// mempool. It reports an error if no such transaction exists. This operation +// does not remove the transaction from the cache. func (txmp *TxMempool) RemoveTxByKey(txKey types.TxKey) error { - txmp.Lock() - defer txmp.Unlock() + txmp.mtx.Lock() + defer txmp.mtx.Unlock() + return txmp.removeTxByKey(txKey) +} - // remove the committed transaction from the transaction store and indexes - if wtx := txmp.txStore.GetTxByHash(txKey); wtx != nil { - txmp.removeTx(wtx, false) +// removeTxByKey removes the specified transaction key from the mempool. +// The caller must hold txmp.mtx excluxively. +func (txmp *TxMempool) removeTxByKey(key types.TxKey) error { + if elt, ok := txmp.txByKey[key]; ok { + w := elt.Value.(*WrappedTx) + delete(txmp.txByKey, key) + delete(txmp.txBySender, w.sender) + txmp.txs.Remove(elt) + elt.DetachPrev() + elt.DetachNext() + atomic.AddInt64(&txmp.txsBytes, -w.Size()) return nil } - - return errors.New("transaction not found") + return fmt.Errorf("transaction %x not found", key) } -// Flush flushes out the mempool. It acquires a read-lock, fetches all the -// transactions currently in the transaction store and removes each transaction -// from the store and all indexes and finally resets the cache. -// -// NOTE: -// - Flushing the mempool may leave the mempool in an inconsistent state. +// removeTxByElement removes the specified transaction element from the mempool. +// The caller must hold txmp.mtx exclusively. +func (txmp *TxMempool) removeTxByElement(elt *clist.CElement) { + w := elt.Value.(*WrappedTx) + delete(txmp.txByKey, w.tx.Key()) + delete(txmp.txBySender, w.sender) + txmp.txs.Remove(elt) + elt.DetachPrev() + elt.DetachNext() + atomic.AddInt64(&txmp.txsBytes, -w.Size()) +} + +// Flush purges the contents of the mempool and the cache, leaving both empty. +// The current height is not modified by this operation. func (txmp *TxMempool) Flush() { - txmp.mtx.RLock() - defer txmp.mtx.RUnlock() + txmp.mtx.Lock() + defer txmp.mtx.Unlock() - txmp.heightIndex.Reset() - txmp.timestampIndex.Reset() - - for _, wtx := range txmp.txStore.GetAllTxs() { - txmp.removeTx(wtx, false) + // Remove all the transactions in the list explicitly, so that the sizes + // and indexes get updated properly. + cur := txmp.txs.Front() + for cur != nil { + next := cur.Next() + txmp.removeTxByElement(cur) + cur = next } - - atomic.SwapInt64(&txmp.sizeBytes, 0) txmp.cache.Reset() + + // Discard any pending recheck calls that may be in flight. The calls will + // still complete, but will have no effect on the mempool. + atomic.StoreInt64(&txmp.txRecheck, 0) } -// ReapMaxBytesMaxGas returns a list of transactions within the provided size -// and gas constraints. Transaction are retrieved in priority order. +// allEntriesSorted returns a slice of all the transactions currently in the +// mempool, sorted in nonincreasing order by priority with ties broken by +// increasing order of arrival time. +func (txmp *TxMempool) allEntriesSorted() []*WrappedTx { + txmp.mtx.RLock() + defer txmp.mtx.RUnlock() + + all := make([]*WrappedTx, 0, len(txmp.txByKey)) + for _, tx := range txmp.txByKey { + all = append(all, tx.Value.(*WrappedTx)) + } + sort.Slice(all, func(i, j int) bool { + if all[i].priority == all[j].priority { + return all[i].timestamp.Before(all[j].timestamp) + } + return all[i].priority > all[j].priority // N.B. higher priorities first + }) + return all +} + +// ReapMaxBytesMaxGas returns a slice of valid transactions that fit within the +// size and gas constraints. The results are ordered by nonincreasing priority, +// with ties broken by increasing order of arrival. Reaping transactions does +// not remove them from the mempool. // -// NOTE: -// - A read-lock is acquired. -// - Transactions returned are not actually removed from the mempool transaction -// store or indexes. +// If maxBytes < 0, no limit is set on the total size in bytes. +// If maxGas < 0, no limit is set on the total gas cost. +// +// If the mempool is empty or has no transactions fitting within the given +// constraints, the result will also be empty. func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs { - txmp.mtx.RLock() - defer txmp.mtx.RUnlock() + var totalGas, totalBytes int64 - var ( - totalGas int64 - totalSize int64 - ) - - // wTxs contains a list of *WrappedTx retrieved from the priority queue that - // need to be re-enqueued prior to returning. - wTxs := make([]*WrappedTx, 0, txmp.priorityIndex.NumTxs()) - defer func() { - for _, wtx := range wTxs { - txmp.priorityIndex.PushTx(wtx) + var keep []types.Tx //nolint:prealloc + for _, w := range txmp.allEntriesSorted() { + // N.B. When computing byte size, we need to include the overhead for + // encoding as protobuf to send to the application. + totalGas += w.gasWanted + totalBytes += types.ComputeProtoSizeForTxs([]types.Tx{w.tx}) + if (maxGas >= 0 && totalGas > maxGas) || (maxBytes >= 0 && totalBytes > maxBytes) { + break } - }() - - txs := make([]types.Tx, 0, txmp.priorityIndex.NumTxs()) - for txmp.priorityIndex.NumTxs() > 0 { - wtx := txmp.priorityIndex.PopTx() - txs = append(txs, wtx.tx) - wTxs = append(wTxs, wtx) - size := types.ComputeProtoSizeForTxs([]types.Tx{wtx.tx}) - - // Ensure we have capacity for the transaction with respect to the - // transaction size. - if maxBytes > -1 && totalSize+size > maxBytes { - return txs[:len(txs)-1] - } - - totalSize += size - - // ensure we have capacity for the transaction with respect to total gas - gas := totalGas + wtx.gasWanted - if maxGas > -1 && gas > maxGas { - return txs[:len(txs)-1] - } - - totalGas = gas + keep = append(keep, w.tx) } - - return txs + return keep } -// ReapMaxTxs returns a list of transactions within the provided number of -// transactions bound. Transaction are retrieved in priority order. +// TxsWaitChan returns a channel that is closed when there is at least one +// transaction available to be gossiped. +func (txmp *TxMempool) TxsWaitChan() <-chan struct{} { return txmp.txs.WaitChan() } + +// TxsFront returns the frontmost element of the pending transaction list. +// It will be nil if the mempool is empty. +func (txmp *TxMempool) TxsFront() *clist.CElement { return txmp.txs.Front() } + +// ReapMaxTxs returns up to max transactions from the mempool. The results are +// ordered by nonincreasing priority with ties broken by increasing order of +// arrival. Reaping transactions does not remove them from the mempool. // -// NOTE: -// - A read-lock is acquired. -// - Transactions returned are not actually removed from the mempool transaction -// store or indexes. +// If max < 0, all transactions in the mempool are reaped. +// +// The result may have fewer than max elements (possibly zero) if the mempool +// does not have that many transactions available. func (txmp *TxMempool) ReapMaxTxs(max int) types.Txs { - txmp.mtx.RLock() - defer txmp.mtx.RUnlock() + var keep []types.Tx //nolint:prealloc - numTxs := txmp.priorityIndex.NumTxs() - if max < 0 { - max = numTxs - } - - cap := tmmath.MinInt(numTxs, max) - - // wTxs contains a list of *WrappedTx retrieved from the priority queue that - // need to be re-enqueued prior to returning. - wTxs := make([]*WrappedTx, 0, cap) - defer func() { - for _, wtx := range wTxs { - txmp.priorityIndex.PushTx(wtx) + for _, w := range txmp.allEntriesSorted() { + if max >= 0 && len(keep) >= max { + break } - }() - - txs := make([]types.Tx, 0, cap) - for txmp.priorityIndex.NumTxs() > 0 && len(txs) < max { - wtx := txmp.priorityIndex.PopTx() - txs = append(txs, wtx.tx) - wTxs = append(wTxs, wtx) + keep = append(keep, w.tx) } - - return txs + return keep } -// Update iterates over all the transactions provided by the caller, i.e. the -// block producer, and removes them from the cache (if applicable) and removes -// the transactions from the main transaction store and associated indexes. -// Finally, if there are trainsactions remaining in the mempool, we initiate a -// re-CheckTx for them (if applicable), otherwise, we notify the caller more -// transactions are available. +// Update removes all the given transactions from the mempool and the cache, +// and updates the current block height. The blockTxs and deliverTxResponses +// must have the same length with each response corresponding to the tx at the +// same offset. // -// NOTE: -// - The caller must explicitly acquire a write-lock via Lock(). +// If the configuration enables recheck, Update sends each remaining +// transaction after removing blockTxs to the ABCI CheckTx method. Any +// transactions marked as invalid during recheck are also removed. +// +// The caller must hold an exclusive mempool lock (by calling txmp.Lock) before +// calling Update. func (txmp *TxMempool) Update( blockHeight int64, blockTxs types.Txs, @@ -438,6 +393,17 @@ func (txmp *TxMempool) Update( newPreFn mempool.PreCheckFunc, newPostFn mempool.PostCheckFunc, ) error { + // TODO(creachadair): This would be a nice safety check but requires Go 1.18. + // // Safety check: The caller is required to hold the lock. + // if txmp.mtx.TryLock() { + // txmp.mtx.Unlock() + // panic("mempool: Update caller does not hold the lock") + // } + // Safety check: Transactions and responses must match in number. + if len(blockTxs) != len(deliverTxResponses) { + panic(fmt.Sprintf("mempool: got %d transactions but %d DeliverTx responses", + len(blockTxs), len(deliverTxResponses))) + } txmp.height = blockHeight txmp.notifiedTxsAvailable = false @@ -450,18 +416,17 @@ func (txmp *TxMempool) Update( } for i, tx := range blockTxs { + // Add successful committed transactions to the cache (if they are not + // already present). Transactions that failed to commit are removed from + // the cache unless the operator has explicitly requested we keep them. if deliverTxResponses[i].Code == abci.CodeTypeOK { - // add the valid committed transaction to the cache (if missing) _ = txmp.cache.Push(tx) } else if !txmp.config.KeepInvalidTxsInCache { - // allow invalid transactions to be re-submitted txmp.cache.Remove(tx) } - // remove the committed transaction from the transaction store and indexes - if wtx := txmp.txStore.GetTxByHash(tx.Key()); wtx != nil { - txmp.removeTx(wtx, false) - } + // Regardless of success, remove the transaction from the mempool. + _ = txmp.removeTxByKey(tx.Key()) } txmp.purgeExpiredTxs(blockHeight) @@ -469,306 +434,316 @@ func (txmp *TxMempool) Update( // If there any uncommitted transactions left in the mempool, we either // initiate re-CheckTx per remaining transaction or notify that remaining // transactions are left. - if txmp.Size() > 0 { + size := txmp.Size() + txmp.metrics.Size.Set(float64(size)) + if size > 0 { if txmp.config.Recheck { - txmp.logger.Debug( - "executing re-CheckTx for all remaining transactions", - "num_txs", txmp.Size(), - "height", blockHeight, - ) - txmp.updateReCheckTxs() + txmp.recheckTransactions() } else { txmp.notifyTxsAvailable() } } - - txmp.metrics.Size.Set(float64(txmp.Size())) return nil } -// initTxCallback performs the initial, i.e. the first, callback after CheckTx -// has been executed by the ABCI application. In other words, initTxCallback is -// called after executing CheckTx when we see a unique transaction for the first -// time. CheckTx can be called again for the same transaction at a later point -// in time when re-checking, however, this callback will not be called. +// initialTxCallback handles the ABCI CheckTx response for the first time a +// transaction is added to the mempool. A recheck after a block is committed +// goes to the default callback (see recheckTxCallback). // -// After the ABCI application executes CheckTx, initTxCallback is called with -// the ABCI *Response object and TxInfo. If postCheck is defined on the mempool, -// we execute that first. If there is no error from postCheck (if defined) and -// the ABCI CheckTx response code is OK, we attempt to insert the transaction. +// If either the application rejected the transaction or a post-check hook is +// defined and rejects the transaction, it is discarded. // -// When attempting to insert the transaction, we first check if there is -// sufficient capacity. If there is sufficient capacity, the transaction is -// inserted into the txStore and indexed across all indexes. Otherwise, if the -// mempool is full, we attempt to find a lower priority transaction to evict in -// place of the new incoming transaction. If no such transaction exists, the -// new incoming transaction is rejected. +// Otherwise, if the mempool is full, check for lower-priority transactions +// that can be evicted to make room for the new one. If no such transactions +// exist, this transaction is logged and dropped; otherwise the selected +// transactions are evicted. // -// If the new incoming transaction fails CheckTx or postCheck fails, we reject -// the new incoming transaction. -// -// NOTE: -// - An explicit lock is NOT required. -func (txmp *TxMempool) initTxCallback(wtx *WrappedTx, res *abci.Response, txInfo mempool.TxInfo) { +// Finally, the new transaction is added and size stats updated. +func (txmp *TxMempool) initialTxCallback(wtx *WrappedTx, res *abci.Response) { checkTxRes, ok := res.Value.(*abci.Response_CheckTx) if !ok { return } + txmp.mtx.Lock() + defer txmp.mtx.Unlock() + var err error if txmp.postCheck != nil { err = txmp.postCheck(wtx.tx, checkTxRes.CheckTx) } if err != nil || checkTxRes.CheckTx.Code != abci.CodeTypeOK { - // ignore bad transactions txmp.logger.Info( "rejected bad transaction", - "priority", wtx.priority, + "priority", wtx.Priority(), "tx", fmt.Sprintf("%X", wtx.tx.Hash()), - "peer_id", txInfo.SenderNodeID, + "peer_id", wtx.peers, "code", checkTxRes.CheckTx.Code, "post_check_err", err, ) txmp.metrics.FailedTxs.Add(1) + // Remove the invalid transaction from the cache, unless the operator has + // instructed us to keep invalid transactions. if !txmp.config.KeepInvalidTxsInCache { txmp.cache.Remove(wtx.tx) } + + // If there was a post-check error, record its text in the result for + // debugging purposes. if err != nil { checkTxRes.CheckTx.MempoolError = err.Error() } return } - sender := checkTxRes.CheckTx.Sender priority := checkTxRes.CheckTx.Priority + sender := checkTxRes.CheckTx.Sender - if len(sender) > 0 { - if wtx := txmp.txStore.GetTxBySender(sender); wtx != nil { - txmp.logger.Error( - "rejected incoming good transaction; tx already exists for sender", - "tx", fmt.Sprintf("%X", wtx.tx.Hash()), + // Disallow multiple concurrent transactions from the same sender assigned + // by the ABCI application. As a special case, an empty sender is not + // restricted. + if sender != "" { + elt, ok := txmp.txBySender[sender] + if ok { + w := elt.Value.(*WrappedTx) + txmp.logger.Debug( + "rejected valid incoming transaction; tx already exists for sender", + "tx", fmt.Sprintf("%X", w.tx.Hash()), "sender", sender, ) + checkTxRes.CheckTx.MempoolError = + fmt.Sprintf("rejected valid incoming transaction; tx already exists for sender %q (%X)", + sender, w.tx.Hash()) txmp.metrics.RejectedTxs.Add(1) return } } + // At this point the application has ruled the transaction valid, but the + // mempool might be full. If so, find the lowest-priority items with lower + // priority than the application assigned to this new one, and evict as many + // of them as necessary to make room for tx. If no such items exist, we + // discard tx. + if err := txmp.canAddTx(wtx); err != nil { - evictTxs := txmp.priorityIndex.GetEvictableTxs( - priority, - int64(wtx.Size()), - txmp.SizeBytes(), - txmp.config.MaxTxsBytes, - ) - if len(evictTxs) == 0 { - // No room for the new incoming transaction so we just remove it from - // the cache. + var victims []*clist.CElement // eligible transactions for eviction + var victimBytes int64 // total size of victims + for cur := txmp.txs.Front(); cur != nil; cur = cur.Next() { + cw := cur.Value.(*WrappedTx) + if cw.priority < priority { + victims = append(victims, cur) + victimBytes += cw.Size() + } + } + + // If there are no suitable eviction candidates, or the total size of + // those candidates is not enough to make room for the new transaction, + // drop the new one. + if len(victims) == 0 || victimBytes < wtx.Size() { txmp.cache.Remove(wtx.tx) txmp.logger.Error( - "rejected incoming good transaction; mempool full", + "rejected valid incoming transaction; mempool is full", "tx", fmt.Sprintf("%X", wtx.tx.Hash()), "err", err.Error(), ) + checkTxRes.CheckTx.MempoolError = + fmt.Sprintf("rejected valid incoming transaction; mempool is full (%X)", + wtx.tx.Hash()) txmp.metrics.RejectedTxs.Add(1) return } - // evict an existing transaction(s) - // - // NOTE: - // - The transaction, toEvict, can be removed while a concurrent - // reCheckTx callback is being executed for the same transaction. - for _, toEvict := range evictTxs { - txmp.removeTx(toEvict, true) + txmp.logger.Debug("evicting lower-priority transactions", + "new_tx", fmt.Sprintf("%X", wtx.tx.Hash()), + "new_priority", priority, + ) + + // Sort lowest priority items first so they will be evicted first. Break + // ties in favor of newer items (to maintain FIFO semantics in a group). + sort.Slice(victims, func(i, j int) bool { + iw := victims[i].Value.(*WrappedTx) + jw := victims[j].Value.(*WrappedTx) + if iw.Priority() == jw.Priority() { + return iw.timestamp.After(jw.timestamp) + } + return iw.Priority() < jw.Priority() + }) + + // Evict as many of the victims as necessary to make room. + var evictedBytes int64 + for _, vic := range victims { + w := vic.Value.(*WrappedTx) + txmp.logger.Debug( - "evicted existing good transaction; mempool full", - "old_tx", fmt.Sprintf("%X", toEvict.tx.Hash()), - "old_priority", toEvict.priority, - "new_tx", fmt.Sprintf("%X", wtx.tx.Hash()), - "new_priority", wtx.priority, + "evicted valid existing transaction; mempool full", + "old_tx", fmt.Sprintf("%X", w.tx.Hash()), + "old_priority", w.priority, ) + txmp.removeTxByElement(vic) txmp.metrics.EvictedTxs.Add(1) + + // We may not need to evict all the eligible transactions. Bail out + // early if we have made enough room. + evictedBytes += w.Size() + if evictedBytes >= wtx.Size() { + break + } } } - wtx.gasWanted = checkTxRes.CheckTx.GasWanted - wtx.priority = priority - wtx.sender = sender - wtx.peers = map[uint16]struct{}{ - txInfo.SenderID: {}, - } + wtx.SetGasWanted(checkTxRes.CheckTx.GasWanted) + wtx.SetPriority(priority) + wtx.SetSender(sender) + txmp.insertTx(wtx) txmp.metrics.TxSizeBytes.Observe(float64(wtx.Size())) txmp.metrics.Size.Set(float64(txmp.Size())) - - txmp.insertTx(wtx) txmp.logger.Debug( - "inserted good transaction", - "priority", wtx.priority, + "inserted new valid transaction", + "priority", wtx.Priority(), "tx", fmt.Sprintf("%X", wtx.tx.Hash()), "height", txmp.height, "num_txs", txmp.Size(), ) txmp.notifyTxsAvailable() - } -// defaultTxCallback performs the default CheckTx application callback. This is -// NOT executed when a transaction is first seen/received. Instead, this callback -// is executed during re-checking transactions (if enabled). A caller, i.e a -// block proposer, acquires a mempool write-lock via Lock() and when executing -// Update(), if the mempool is non-empty and Recheck is enabled, then all -// remaining transactions will be rechecked via CheckTxAsync. The order in which -// they are rechecked must be the same order in which this callback is called -// per transaction. -func (txmp *TxMempool) defaultTxCallback(req *abci.Request, res *abci.Response) { - if txmp.recheckCursor == nil { - return +func (txmp *TxMempool) insertTx(wtx *WrappedTx) { + elt := txmp.txs.PushBack(wtx) + txmp.txByKey[wtx.tx.Key()] = elt + if s := wtx.Sender(); s != "" { + txmp.txBySender[s] = elt } - txmp.metrics.RecheckTimes.Add(1) + atomic.AddInt64(&txmp.txsBytes, wtx.Size()) +} +// recheckTxCallback handles the responses from ABCI CheckTx calls issued +// during the recheck phase of a block Update. It updates the recheck counter +// and removes any transactions invalidated by the application. +// +// This callback is NOT executed for the initial CheckTx on a new transaction; +// that case is handled by initialTxCallback instead. +func (txmp *TxMempool) recheckTxCallback(req *abci.Request, res *abci.Response) { checkTxRes, ok := res.Value.(*abci.Response_CheckTx) if !ok { - txmp.logger.Error("received incorrect type in mempool callback", + txmp.logger.Error("mempool: received incorrect result type in CheckTx callback", "expected", reflect.TypeOf(&abci.Response_CheckTx{}).Name(), "got", reflect.TypeOf(res.Value).Name(), ) return } - tx := req.GetCheckTx().Tx - wtx := txmp.recheckCursor.Value.(*WrappedTx) - // Search through the remaining list of tx to recheck for a transaction that matches - // the one we received from the ABCI application. - for { - if bytes.Equal(tx, wtx.tx) { - // We've found a tx in the recheck list that matches the tx that we - // received from the ABCI application. - // Break, and use this transaction for further checks. - break - } - - txmp.logger.Error( - "re-CheckTx transaction mismatch", - "got", wtx.tx.Hash(), - "expected", types.Tx(tx).Key(), - ) - - if txmp.recheckCursor == txmp.recheckEnd { - // we reached the end of the recheckTx list without finding a tx - // matching the one we received from the ABCI application. - // Return without processing any tx. - txmp.recheckCursor = nil - return - } - - txmp.recheckCursor = txmp.recheckCursor.Next() - wtx = txmp.recheckCursor.Value.(*WrappedTx) + // Check whether we are expecting recheck responses at this point. + // If not, we will ignore the response, this usually means the mempool was Flushed. + // If this is the "last" pending recheck, trigger a notification when it's been processed. + numLeft := atomic.AddInt64(&txmp.txRecheck, -1) + if numLeft == 0 { + defer txmp.notifyTxsAvailable() // notify waiters on return, if mempool is non-empty + } else if numLeft < 0 { + return } - // Only evaluate transactions that have not been removed. This can happen - // if an existing transaction is evicted during CheckTx and while this - // callback is being executed for the same evicted transaction. - if !txmp.txStore.IsTxRemoved(wtx.hash) { - var err error - if txmp.postCheck != nil { - err = txmp.postCheck(tx, checkTxRes.CheckTx) - } + txmp.metrics.RecheckTimes.Add(1) + tx := types.Tx(req.GetCheckTx().Tx) - if checkTxRes.CheckTx.Code == abci.CodeTypeOK && err == nil { - wtx.priority = checkTxRes.CheckTx.Priority - } else { - txmp.logger.Debug( - "existing transaction no longer valid; failed re-CheckTx callback", - "priority", wtx.priority, - "tx", fmt.Sprintf("%X", wtx.tx.Hash()), - "err", err, - "code", checkTxRes.CheckTx.Code, - ) + txmp.mtx.Lock() + defer txmp.mtx.Unlock() - if wtx.gossipEl != txmp.recheckCursor { - panic("corrupted reCheckTx cursor") - } + // Find the transaction reported by the ABCI callback. It is possible the + // transaction was evicted during the recheck, in which case the transaction + // will be gone. + elt, ok := txmp.txByKey[tx.Key()] + if !ok { + return + } + wtx := elt.Value.(*WrappedTx) - txmp.removeTx(wtx, !txmp.config.KeepInvalidTxsInCache) - } + // If a postcheck hook is defined, call it before checking the result. + var err error + if txmp.postCheck != nil { + err = txmp.postCheck(tx, checkTxRes.CheckTx) } - // move reCheckTx cursor to next element - if txmp.recheckCursor == txmp.recheckEnd { - txmp.recheckCursor = nil - } else { - txmp.recheckCursor = txmp.recheckCursor.Next() + if checkTxRes.CheckTx.Code == abci.CodeTypeOK && err == nil { + wtx.SetPriority(checkTxRes.CheckTx.Priority) + return // N.B. Size of mempool did not change } - if txmp.recheckCursor == nil { - txmp.logger.Debug("finished rechecking transactions") - - if txmp.Size() > 0 { - txmp.notifyTxsAvailable() - } + txmp.logger.Debug( + "existing transaction no longer valid; failed re-CheckTx callback", + "priority", wtx.Priority(), + "tx", fmt.Sprintf("%X", wtx.tx.Hash()), + "err", err, + "code", checkTxRes.CheckTx.Code, + ) + txmp.removeTxByElement(elt) + txmp.metrics.FailedTxs.Add(1) + if !txmp.config.KeepInvalidTxsInCache { + txmp.cache.Remove(wtx.tx) } - txmp.metrics.Size.Set(float64(txmp.Size())) } -// updateReCheckTxs updates the recheck cursors by using the gossipIndex. For -// each transaction, it executes CheckTxAsync. The global callback defined on -// the proxyAppConn will be executed for each transaction after CheckTx is -// executed. +// recheckTransactions initiates re-CheckTx ABCI calls for all the transactions +// currently in the mempool. It reports the number of recheck calls that were +// successfully initiated. // -// NOTE: -// - The caller must have a write-lock when executing updateReCheckTxs. -func (txmp *TxMempool) updateReCheckTxs() { +// Precondition: The mempool is not empty. +// The caller must hold txmp.mtx exclusively. +func (txmp *TxMempool) recheckTransactions() { if txmp.Size() == 0 { - panic("attempted to update re-CheckTx txs when mempool is empty") + panic("mempool: cannot run recheck on an empty mempool") } + txmp.logger.Debug( + "executing re-CheckTx for all remaining transactions", + "num_txs", txmp.Size(), + "height", txmp.height, + ) + // N.B.: We have to issue the calls outside the lock. In a local client, + // even an "async" call invokes its callback immediately which will make the + // callback deadlock trying to acquire the same lock. This isn't a problem + // with out-of-process calls, but this has to work for both. + txmp.mtx.Unlock() + defer txmp.mtx.Lock() - txmp.recheckCursor = txmp.gossipIndex.Front() - txmp.recheckEnd = txmp.gossipIndex.Back() - ctx := context.Background() - - for e := txmp.gossipIndex.Front(); e != nil; e = e.Next() { + ctx := context.TODO() + atomic.StoreInt64(&txmp.txRecheck, int64(txmp.txs.Len())) + for e := txmp.txs.Front(); e != nil; e = e.Next() { wtx := e.Value.(*WrappedTx) - // Only execute CheckTx if the transaction is not marked as removed which - // could happen if the transaction was evicted. - if !txmp.txStore.IsTxRemoved(wtx.hash) { - _, err := txmp.proxyAppConn.CheckTxAsync(ctx, abci.RequestCheckTx{ - Tx: wtx.tx, - Type: abci.CheckTxType_Recheck, - }) - if err != nil { - // no need in retrying since the tx will be rechecked after the next block - txmp.logger.Error("failed to execute CheckTx during rechecking", "err", err) - } + // The response for this CheckTx is handled by the default recheckTxCallback. + _, err := txmp.proxyAppConn.CheckTxAsync(ctx, abci.RequestCheckTx{ + Tx: wtx.tx, + Type: abci.CheckTxType_Recheck, + }) + if err != nil { + txmp.logger.Error("failed to execute CheckTx during recheck", + "err", err, "hash", fmt.Sprintf("%x", wtx.tx.Hash())) + atomic.AddInt64(&txmp.txRecheck, -1) } } if _, err := txmp.proxyAppConn.FlushAsync(ctx); err != nil { - txmp.logger.Error("failed to flush transactions during rechecking", "err", err) + txmp.logger.Error("failed to flush transactions during recheck", "err", err) } } // canAddTx returns an error if we cannot insert the provided *WrappedTx into -// the mempool due to mempool configured constraints. Otherwise, nil is returned -// and the transaction can be inserted into the mempool. +// the mempool due to mempool configured constraints. Otherwise, nil is +// returned and the transaction can be inserted into the mempool. func (txmp *TxMempool) canAddTx(wtx *WrappedTx) error { - var ( - numTxs = txmp.Size() - sizeBytes = txmp.SizeBytes() - ) + numTxs := txmp.Size() + txBytes := txmp.SizeBytes() - if numTxs >= txmp.config.Size || int64(wtx.Size())+sizeBytes > txmp.config.MaxTxsBytes { + if numTxs >= txmp.config.Size || wtx.Size()+txBytes > txmp.config.MaxTxsBytes { return types.ErrMempoolIsFull{ NumTxs: numTxs, MaxTxs: txmp.config.Size, - TxsBytes: sizeBytes, + TxsBytes: txBytes, MaxTxsBytes: txmp.config.MaxTxsBytes, } } @@ -776,96 +751,38 @@ func (txmp *TxMempool) canAddTx(wtx *WrappedTx) error { return nil } -func (txmp *TxMempool) insertTx(wtx *WrappedTx) { - txmp.txStore.SetTx(wtx) - txmp.priorityIndex.PushTx(wtx) - txmp.heightIndex.Insert(wtx) - txmp.timestampIndex.Insert(wtx) - - // Insert the transaction into the gossip index and mark the reference to the - // linked-list element, which will be needed at a later point when the - // transaction is removed. - gossipEl := txmp.gossipIndex.PushBack(wtx) - wtx.gossipEl = gossipEl - - atomic.AddInt64(&txmp.sizeBytes, int64(wtx.Size())) -} - -func (txmp *TxMempool) removeTx(wtx *WrappedTx, removeFromCache bool) { - if txmp.txStore.IsTxRemoved(wtx.hash) { - return - } - - txmp.txStore.RemoveTx(wtx) - txmp.priorityIndex.RemoveTx(wtx) - txmp.heightIndex.Remove(wtx) - txmp.timestampIndex.Remove(wtx) - - // Remove the transaction from the gossip index and cleanup the linked-list - // element so it can be garbage collected. - txmp.gossipIndex.Remove(wtx.gossipEl) - wtx.gossipEl.DetachPrev() - - atomic.AddInt64(&txmp.sizeBytes, int64(-wtx.Size())) - - if removeFromCache { - txmp.cache.Remove(wtx.tx) - } -} - -// purgeExpiredTxs removes all transactions that have exceeded their respective -// height and/or time based TTLs from their respective indexes. Every expired -// transaction will be removed from the mempool entirely, except for the cache. +// purgeExpiredTxs removes all transactions from the mempool that have exceeded +// their respective height or time-based limits as of the given blockHeight. +// Transactions removed by this operation are not removed from the cache. // -// NOTE: purgeExpiredTxs must only be called during TxMempool#Update in which -// the caller has a write-lock on the mempool and so we can safely iterate over -// the height and time based indexes. +// The caller must hold txmp.mtx exclusively. func (txmp *TxMempool) purgeExpiredTxs(blockHeight int64) { + if txmp.config.TTLNumBlocks == 0 && txmp.config.TTLDuration == 0 { + return // nothing to do + } + now := time.Now() - expiredTxs := make(map[types.TxKey]*WrappedTx) + cur := txmp.txs.Front() + for cur != nil { + // N.B. Grab the next element first, since if we remove cur its successor + // will be invalidated. + next := cur.Next() - if txmp.config.TTLNumBlocks > 0 { - purgeIdx := -1 - for i, wtx := range txmp.heightIndex.txs { - if (blockHeight - wtx.height) > txmp.config.TTLNumBlocks { - expiredTxs[wtx.tx.Key()] = wtx - purgeIdx = i - } else { - // since the index is sorted, we know no other txs can be be purged - break - } + w := cur.Value.(*WrappedTx) + if txmp.config.TTLNumBlocks > 0 && (blockHeight-w.height) > txmp.config.TTLNumBlocks { + txmp.removeTxByElement(cur) + txmp.metrics.EvictedTxs.Add(1) + } else if txmp.config.TTLDuration > 0 && now.Sub(w.timestamp) > txmp.config.TTLDuration { + txmp.removeTxByElement(cur) + txmp.metrics.EvictedTxs.Add(1) } - - if purgeIdx >= 0 { - txmp.heightIndex.txs = txmp.heightIndex.txs[purgeIdx+1:] - } - } - - if txmp.config.TTLDuration > 0 { - purgeIdx := -1 - for i, wtx := range txmp.timestampIndex.txs { - if now.Sub(wtx.timestamp) > txmp.config.TTLDuration { - expiredTxs[wtx.tx.Key()] = wtx - purgeIdx = i - } else { - // since the index is sorted, we know no other txs can be be purged - break - } - } - - if purgeIdx >= 0 { - txmp.timestampIndex.txs = txmp.timestampIndex.txs[purgeIdx+1:] - } - } - - for _, wtx := range expiredTxs { - txmp.removeTx(wtx, false) + cur = next } } func (txmp *TxMempool) notifyTxsAvailable() { if txmp.Size() == 0 { - panic("attempt to notify txs available but mempool is empty!") + return // nothing to do } if txmp.txsAvailable != nil && !txmp.notifiedTxsAvailable { diff --git a/internal/mempool/v1/mempool_test.go b/internal/mempool/v1/mempool_test.go index 72a72861c..09f431853 100644 --- a/internal/mempool/v1/mempool_test.go +++ b/internal/mempool/v1/mempool_test.go @@ -445,7 +445,6 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { tTxs := checkTxs(t, txmp, 100, 0) require.Equal(t, len(tTxs), txmp.Size()) - require.Equal(t, 100, txmp.heightIndex.Size()) // reap 5 txs at the next height -- no txs should expire reapedTxs := txmp.ReapMaxTxs(5) @@ -459,12 +458,10 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { txmp.Unlock() require.Equal(t, 95, txmp.Size()) - require.Equal(t, 95, txmp.heightIndex.Size()) // check more txs at height 101 _ = checkTxs(t, txmp, 50, 1) require.Equal(t, 145, txmp.Size()) - require.Equal(t, 145, txmp.heightIndex.Size()) // Reap 5 txs at a height that would expire all the transactions from before // the previous Update (height 100). @@ -485,7 +482,6 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { txmp.Unlock() require.GreaterOrEqual(t, txmp.Size(), 45) - require.GreaterOrEqual(t, txmp.heightIndex.Size(), 45) } func TestTxMempool_CheckTxPostCheckError(t *testing.T) { diff --git a/internal/mempool/v1/priority_queue.go b/internal/mempool/v1/priority_queue.go deleted file mode 100644 index df74a92d3..000000000 --- a/internal/mempool/v1/priority_queue.go +++ /dev/null @@ -1,159 +0,0 @@ -package v1 - -import ( - "container/heap" - "sort" - - tmsync "github.com/tendermint/tendermint/internal/libs/sync" -) - -var _ heap.Interface = (*TxPriorityQueue)(nil) - -// TxPriorityQueue defines a thread-safe priority queue for valid transactions. -type TxPriorityQueue struct { - mtx tmsync.RWMutex - txs []*WrappedTx -} - -func NewTxPriorityQueue() *TxPriorityQueue { - pq := &TxPriorityQueue{ - txs: make([]*WrappedTx, 0), - } - - heap.Init(pq) - - return pq -} - -// GetEvictableTxs attempts to find and return a list of *WrappedTx than can be -// evicted to make room for another *WrappedTx with higher priority. If no such -// list of *WrappedTx exists, nil will be returned. The returned list of *WrappedTx -// indicate that these transactions can be removed due to them being of lower -// priority and that their total sum in size allows room for the incoming -// transaction according to the mempool's configured limits. -func (pq *TxPriorityQueue) GetEvictableTxs(priority, txSize, totalSize, cap int64) []*WrappedTx { - pq.mtx.RLock() - defer pq.mtx.RUnlock() - - txs := make([]*WrappedTx, len(pq.txs)) - copy(txs, pq.txs) - - sort.Slice(txs, func(i, j int) bool { - return txs[i].priority < txs[j].priority - }) - - var ( - toEvict []*WrappedTx - i int - ) - - currSize := totalSize - - // Loop over all transactions in ascending priority order evaluating those - // that are only of less priority than the provided argument. We continue - // evaluating transactions until there is sufficient capacity for the new - // transaction (size) as defined by txSize. - for i < len(txs) && txs[i].priority < priority { - toEvict = append(toEvict, txs[i]) - currSize -= int64(txs[i].Size()) - - if currSize+txSize <= cap { - return toEvict - } - - i++ - } - - return nil -} - -// NumTxs returns the number of transactions in the priority queue. It is -// thread safe. -func (pq *TxPriorityQueue) NumTxs() int { - pq.mtx.RLock() - defer pq.mtx.RUnlock() - - return len(pq.txs) -} - -// RemoveTx removes a specific transaction from the priority queue. -func (pq *TxPriorityQueue) RemoveTx(tx *WrappedTx) { - pq.mtx.Lock() - defer pq.mtx.Unlock() - - if tx.heapIndex < len(pq.txs) { - heap.Remove(pq, tx.heapIndex) - } -} - -// PushTx adds a valid transaction to the priority queue. It is thread safe. -func (pq *TxPriorityQueue) PushTx(tx *WrappedTx) { - pq.mtx.Lock() - defer pq.mtx.Unlock() - - heap.Push(pq, tx) -} - -// PopTx removes the top priority transaction from the queue. It is thread safe. -func (pq *TxPriorityQueue) PopTx() *WrappedTx { - pq.mtx.Lock() - defer pq.mtx.Unlock() - - x := heap.Pop(pq) - if x != nil { - return x.(*WrappedTx) - } - - return nil -} - -// Push implements the Heap interface. -// -// NOTE: A caller should never call Push. Use PushTx instead. -func (pq *TxPriorityQueue) Push(x interface{}) { - n := len(pq.txs) - item := x.(*WrappedTx) - item.heapIndex = n - pq.txs = append(pq.txs, item) -} - -// Pop implements the Heap interface. -// -// NOTE: A caller should never call Pop. Use PopTx instead. -func (pq *TxPriorityQueue) Pop() interface{} { - old := pq.txs - n := len(old) - item := old[n-1] - old[n-1] = nil // avoid memory leak - item.heapIndex = -1 // for safety - pq.txs = old[0 : n-1] - return item -} - -// Len implements the Heap interface. -// -// NOTE: A caller should never call Len. Use NumTxs instead. -func (pq *TxPriorityQueue) Len() int { - return len(pq.txs) -} - -// Less implements the Heap interface. It returns true if the transaction at -// position i in the queue is of less priority than the transaction at position j. -func (pq *TxPriorityQueue) Less(i, j int) bool { - // If there exists two transactions with the same priority, consider the one - // that we saw the earliest as the higher priority transaction. - if pq.txs[i].priority == pq.txs[j].priority { - return pq.txs[i].timestamp.Before(pq.txs[j].timestamp) - } - - // We want Pop to give us the highest, not lowest, priority so we use greater - // than here. - return pq.txs[i].priority > pq.txs[j].priority -} - -// Swap implements the Heap interface. It swaps two transactions in the queue. -func (pq *TxPriorityQueue) Swap(i, j int) { - pq.txs[i], pq.txs[j] = pq.txs[j], pq.txs[i] - pq.txs[i].heapIndex = i - pq.txs[j].heapIndex = j -} diff --git a/internal/mempool/v1/priority_queue_test.go b/internal/mempool/v1/priority_queue_test.go deleted file mode 100644 index c0048f388..000000000 --- a/internal/mempool/v1/priority_queue_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package v1 - -import ( - "math/rand" - "sort" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestTxPriorityQueue(t *testing.T) { - pq := NewTxPriorityQueue() - numTxs := 1000 - - priorities := make([]int, numTxs) - - var wg sync.WaitGroup - for i := 1; i <= numTxs; i++ { - priorities[i-1] = i - wg.Add(1) - - go func(i int) { - pq.PushTx(&WrappedTx{ - priority: int64(i), - timestamp: time.Now(), - }) - - wg.Done() - }(i) - } - - sort.Sort(sort.Reverse(sort.IntSlice(priorities))) - - wg.Wait() - require.Equal(t, numTxs, pq.NumTxs()) - - // Wait a second and push a tx with a duplicate priority - time.Sleep(time.Second) - now := time.Now() - pq.PushTx(&WrappedTx{ - priority: 1000, - timestamp: now, - }) - require.Equal(t, 1001, pq.NumTxs()) - - tx := pq.PopTx() - require.Equal(t, 1000, pq.NumTxs()) - require.Equal(t, int64(1000), tx.priority) - require.NotEqual(t, now, tx.timestamp) - - gotPriorities := make([]int, 0) - for pq.NumTxs() > 0 { - gotPriorities = append(gotPriorities, int(pq.PopTx().priority)) - } - - require.Equal(t, priorities, gotPriorities) -} - -func TestTxPriorityQueue_GetEvictableTxs(t *testing.T) { - pq := NewTxPriorityQueue() - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - - values := make([]int, 1000) - - for i := 0; i < 1000; i++ { - tx := make([]byte, 5) // each tx is 5 bytes - _, err := rng.Read(tx) - require.NoError(t, err) - - x := rng.Intn(100000) - pq.PushTx(&WrappedTx{ - tx: tx, - priority: int64(x), - }) - - values[i] = x - } - - sort.Ints(values) - - max := values[len(values)-1] - min := values[0] - totalSize := int64(len(values) * 5) - - testCases := []struct { - name string - priority, txSize, totalSize, cap int64 - expectedLen int - }{ - { - name: "larest priority; single tx", - priority: int64(max + 1), - txSize: 5, - totalSize: totalSize, - cap: totalSize, - expectedLen: 1, - }, - { - name: "larest priority; multi tx", - priority: int64(max + 1), - txSize: 17, - totalSize: totalSize, - cap: totalSize, - expectedLen: 4, - }, - { - name: "larest priority; out of capacity", - priority: int64(max + 1), - txSize: totalSize + 1, - totalSize: totalSize, - cap: totalSize, - expectedLen: 0, - }, - { - name: "smallest priority; no tx", - priority: int64(min - 1), - txSize: 5, - totalSize: totalSize, - cap: totalSize, - expectedLen: 0, - }, - { - name: "small priority; no tx", - priority: int64(min), - txSize: 5, - totalSize: totalSize, - cap: totalSize, - expectedLen: 0, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - evictTxs := pq.GetEvictableTxs(tc.priority, tc.txSize, tc.totalSize, tc.cap) - require.Len(t, evictTxs, tc.expectedLen) - }) - } -} - -func TestTxPriorityQueue_RemoveTx(t *testing.T) { - pq := NewTxPriorityQueue() - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - numTxs := 1000 - - values := make([]int, numTxs) - - for i := 0; i < numTxs; i++ { - x := rng.Intn(100000) - pq.PushTx(&WrappedTx{ - priority: int64(x), - }) - - values[i] = x - } - - require.Equal(t, numTxs, pq.NumTxs()) - - sort.Ints(values) - max := values[len(values)-1] - - wtx := pq.txs[pq.NumTxs()/2] - pq.RemoveTx(wtx) - require.Equal(t, numTxs-1, pq.NumTxs()) - require.Equal(t, int64(max), pq.PopTx().priority) - require.Equal(t, numTxs-2, pq.NumTxs()) - - require.NotPanics(t, func() { - pq.RemoveTx(&WrappedTx{heapIndex: numTxs}) - pq.RemoveTx(&WrappedTx{heapIndex: numTxs + 1}) - }) - require.Equal(t, numTxs-2, pq.NumTxs()) -} diff --git a/internal/mempool/v1/reactor.go b/internal/mempool/v1/reactor.go index 747b35206..c6bb46dce 100644 --- a/internal/mempool/v1/reactor.go +++ b/internal/mempool/v1/reactor.go @@ -308,9 +308,6 @@ func (r *Reactor) processPeerUpdates() { } func (r *Reactor) broadcastTxRoutine(peerID types.NodeID, closer *tmsync.Closer) { - peerMempoolID := r.ids.GetForPeer(peerID) - var nextGossipTx *clist.CElement - // remove the peer ID from the map of routines and mark the waitgroup as done defer func() { r.mtx.Lock() @@ -329,6 +326,8 @@ func (r *Reactor) broadcastTxRoutine(peerID types.NodeID, closer *tmsync.Closer) } }() + peerMempoolID := r.ids.GetForPeer(peerID) + var nextGossipTx *clist.CElement for { if !r.IsRunning() { return @@ -339,8 +338,8 @@ func (r *Reactor) broadcastTxRoutine(peerID types.NodeID, closer *tmsync.Closer) // start from the beginning. if nextGossipTx == nil { select { - case <-r.mempool.WaitForNextTx(): // wait until a tx is available - if nextGossipTx = r.mempool.NextGossipTx(); nextGossipTx == nil { + case <-r.mempool.TxsWaitChan(): // wait until a tx is available + if nextGossipTx = r.mempool.TxsFront(); nextGossipTx == nil { continue } @@ -358,9 +357,11 @@ func (r *Reactor) broadcastTxRoutine(peerID types.NodeID, closer *tmsync.Closer) memTx := nextGossipTx.Value.(*WrappedTx) + // Send the transaction to a peer if we didn't receive it from that peer. + // // NOTE: Transaction batching was disabled due to: // https://github.com/tendermint/tendermint/issues/5796 - if ok := r.mempool.txStore.TxHasPeer(memTx.hash, peerMempoolID); !ok { + if !memTx.HasPeer(peerMempoolID) { // Send the mempool tx to the corresponding peer. Note, the peer may be // behind and thus would not be able to process the mempool tx correctly. r.mempoolCh.Out <- p2p.Envelope{ diff --git a/internal/mempool/v1/reactor_test.go b/internal/mempool/v1/reactor_test.go index 0454ad9c5..db6efc6be 100644 --- a/internal/mempool/v1/reactor_test.go +++ b/internal/mempool/v1/reactor_test.go @@ -134,7 +134,9 @@ func TestReactorBroadcastDoesNotPanic(t *testing.T) { wg.Add(1) go func() { defer wg.Done() + primaryMempool.Lock() primaryMempool.insertTx(next) + primaryMempool.Unlock() }() } diff --git a/internal/mempool/v1/tx.go b/internal/mempool/v1/tx.go index c5b7ca82f..88522a8a7 100644 --- a/internal/mempool/v1/tx.go +++ b/internal/mempool/v1/tx.go @@ -1,281 +1,87 @@ package v1 import ( - "sort" + "sync" "time" - "github.com/tendermint/tendermint/internal/libs/clist" - tmsync "github.com/tendermint/tendermint/internal/libs/sync" "github.com/tendermint/tendermint/types" ) // WrappedTx defines a wrapper around a raw transaction with additional metadata // that is used for indexing. type WrappedTx struct { - // tx represents the raw binary transaction data - tx types.Tx + tx types.Tx // the original transaction data + hash types.TxKey // the transaction hash + height int64 // height when this transaction was initially checked (for expiry) + timestamp time.Time // time when transaction was entered (for TTL) - // hash defines the transaction hash and the primary key used in the mempool - hash types.TxKey - - // height defines the height at which the transaction was validated at - height int64 - - // gasWanted defines the amount of gas the transaction sender requires - gasWanted int64 - - // priority defines the transaction's priority as specified by the application - // in the ResponseCheckTx response. - priority int64 - - // sender defines the transaction's sender as specified by the application in - // the ResponseCheckTx response. - sender string - - // timestamp is the time at which the node first received the transaction from - // a peer. It is used as a second dimension is prioritizing transactions when - // two transactions have the same priority. - timestamp time.Time - - // peers records a mapping of all peers that sent a given transaction - peers map[uint16]struct{} - - // heapIndex defines the index of the item in the heap - heapIndex int - - // gossipEl references the linked-list element in the gossip index - gossipEl *clist.CElement - - // removed marks the transaction as removed from the mempool. This is set - // during RemoveTx and is needed due to the fact that a given existing - // transaction in the mempool can be evicted when it is simultaneously having - // a reCheckTx callback executed. - removed bool + mtx sync.Mutex + gasWanted int64 // app: gas required to execute this transaction + priority int64 // app: priority value for this transaction + sender string // app: assigned sender label + peers map[uint16]bool // peer IDs who have sent us this transaction } -func (wtx *WrappedTx) Size() int { - return len(wtx.tx) -} +// Size reports the size of the raw transaction in bytes. +func (w *WrappedTx) Size() int64 { return int64(len(w.tx)) } -// TxStore implements a thread-safe mapping of valid transaction(s). -// -// NOTE: -// - Concurrent read-only access to a *WrappedTx object is OK. However, mutative -// access is not allowed. Regardless, it is not expected for the mempool to -// need mutative access. -type TxStore struct { - mtx tmsync.RWMutex - hashTxs map[types.TxKey]*WrappedTx // primary index - senderTxs map[string]*WrappedTx // sender is defined by the ABCI application -} - -func NewTxStore() *TxStore { - return &TxStore{ - senderTxs: make(map[string]*WrappedTx), - hashTxs: make(map[types.TxKey]*WrappedTx), +// SetPeer adds the specified peer ID as a sender of w. +func (w *WrappedTx) SetPeer(id uint16) { + w.mtx.Lock() + defer w.mtx.Unlock() + if w.peers == nil { + w.peers = map[uint16]bool{id: true} + } else { + w.peers[id] = true } } -// Size returns the total number of transactions in the store. -func (txs *TxStore) Size() int { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - return len(txs.hashTxs) -} - -// GetAllTxs returns all the transactions currently in the store. -func (txs *TxStore) GetAllTxs() []*WrappedTx { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - wTxs := make([]*WrappedTx, len(txs.hashTxs)) - i := 0 - for _, wtx := range txs.hashTxs { - wTxs[i] = wtx - i++ - } - - return wTxs -} - -// GetTxBySender returns a *WrappedTx by the transaction's sender property -// defined by the ABCI application. -func (txs *TxStore) GetTxBySender(sender string) *WrappedTx { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - return txs.senderTxs[sender] -} - -// GetTxByHash returns a *WrappedTx by the transaction's hash. -func (txs *TxStore) GetTxByHash(hash types.TxKey) *WrappedTx { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - return txs.hashTxs[hash] -} - -// IsTxRemoved returns true if a transaction by hash is marked as removed and -// false otherwise. -func (txs *TxStore) IsTxRemoved(hash types.TxKey) bool { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - wtx, ok := txs.hashTxs[hash] - if ok { - return wtx.removed - } - - return false -} - -// SetTx stores a *WrappedTx by it's hash. If the transaction also contains a -// non-empty sender, we additionally store the transaction by the sender as -// defined by the ABCI application. -func (txs *TxStore) SetTx(wtx *WrappedTx) { - txs.mtx.Lock() - defer txs.mtx.Unlock() - - if len(wtx.sender) > 0 { - txs.senderTxs[wtx.sender] = wtx - } - - txs.hashTxs[wtx.tx.Key()] = wtx -} - -// RemoveTx removes a *WrappedTx from the transaction store. It deletes all -// indexes of the transaction. -func (txs *TxStore) RemoveTx(wtx *WrappedTx) { - txs.mtx.Lock() - defer txs.mtx.Unlock() - - if len(wtx.sender) > 0 { - delete(txs.senderTxs, wtx.sender) - } - - delete(txs.hashTxs, wtx.tx.Key()) - wtx.removed = true -} - -// TxHasPeer returns true if a transaction by hash has a given peer ID and false -// otherwise. If the transaction does not exist, false is returned. -func (txs *TxStore) TxHasPeer(hash types.TxKey, peerID uint16) bool { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - wtx := txs.hashTxs[hash] - if wtx == nil { - return false - } - - _, ok := wtx.peers[peerID] +// HasPeer reports whether the specified peer ID is a sender of w. +func (w *WrappedTx) HasPeer(id uint16) bool { + w.mtx.Lock() + defer w.mtx.Unlock() + _, ok := w.peers[id] return ok } -// GetOrSetPeerByTxHash looks up a WrappedTx by transaction hash and adds the -// given peerID to the WrappedTx's set of peers that sent us this transaction. -// We return true if we've already recorded the given peer for this transaction -// and false otherwise. If the transaction does not exist by hash, we return -// (nil, false). -func (txs *TxStore) GetOrSetPeerByTxHash(hash types.TxKey, peerID uint16) (*WrappedTx, bool) { - txs.mtx.Lock() - defer txs.mtx.Unlock() - - wtx := txs.hashTxs[hash] - if wtx == nil { - return nil, false - } - - if wtx.peers == nil { - wtx.peers = make(map[uint16]struct{}) - } - - if _, ok := wtx.peers[peerID]; ok { - return wtx, true - } - - wtx.peers[peerID] = struct{}{} - return wtx, false +// SetGasWanted sets the application-assigned gas requirement of w. +func (w *WrappedTx) SetGasWanted(gas int64) { + w.mtx.Lock() + defer w.mtx.Unlock() + w.gasWanted = gas } -// WrappedTxList implements a thread-safe list of *WrappedTx objects that can be -// used to build generic transaction indexes in the mempool. It accepts a -// comparator function, less(a, b *WrappedTx) bool, that compares two WrappedTx -// references which is used during Insert in order to determine sorted order. If -// less returns true, a <= b. -type WrappedTxList struct { - mtx tmsync.RWMutex - txs []*WrappedTx - less func(*WrappedTx, *WrappedTx) bool +// GasWanted reports the application-assigned gas requirement of w. +func (w *WrappedTx) GasWanted() int64 { + w.mtx.Lock() + defer w.mtx.Unlock() + return w.gasWanted } -func NewWrappedTxList(less func(*WrappedTx, *WrappedTx) bool) *WrappedTxList { - return &WrappedTxList{ - txs: make([]*WrappedTx, 0), - less: less, - } +// SetSender sets the application-assigned sender of w. +func (w *WrappedTx) SetSender(sender string) { + w.mtx.Lock() + defer w.mtx.Unlock() + w.sender = sender } -// Size returns the number of WrappedTx objects in the list. -func (wtl *WrappedTxList) Size() int { - wtl.mtx.RLock() - defer wtl.mtx.RUnlock() - - return len(wtl.txs) +// Sender reports the application-assigned sender of w. +func (w *WrappedTx) Sender() string { + w.mtx.Lock() + defer w.mtx.Unlock() + return w.sender } -// Reset resets the list of transactions to an empty list. -func (wtl *WrappedTxList) Reset() { - wtl.mtx.Lock() - defer wtl.mtx.Unlock() - - wtl.txs = make([]*WrappedTx, 0) +// SetPriority sets the application-assigned priority of w. +func (w *WrappedTx) SetPriority(p int64) { + w.mtx.Lock() + defer w.mtx.Unlock() + w.priority = p } -// Insert inserts a WrappedTx reference into the sorted list based on the list's -// comparator function. -func (wtl *WrappedTxList) Insert(wtx *WrappedTx) { - wtl.mtx.Lock() - defer wtl.mtx.Unlock() - - i := sort.Search(len(wtl.txs), func(i int) bool { - return wtl.less(wtl.txs[i], wtx) - }) - - if i == len(wtl.txs) { - // insert at the end - wtl.txs = append(wtl.txs, wtx) - return - } - - // Make space for the inserted element by shifting values at the insertion - // index up one index. - // - // NOTE: The call to append does not allocate memory when cap(wtl.txs) > len(wtl.txs). - wtl.txs = append(wtl.txs[:i+1], wtl.txs[i:]...) - wtl.txs[i] = wtx -} - -// Remove attempts to remove a WrappedTx from the sorted list. -func (wtl *WrappedTxList) Remove(wtx *WrappedTx) { - wtl.mtx.Lock() - defer wtl.mtx.Unlock() - - i := sort.Search(len(wtl.txs), func(i int) bool { - return wtl.less(wtl.txs[i], wtx) - }) - - // Since the list is sorted, we evaluate all elements starting at i. Note, if - // the element does not exist, we may potentially evaluate the entire remainder - // of the list. However, a caller should not be expected to call Remove with a - // non-existing element. - for i < len(wtl.txs) { - if wtl.txs[i] == wtx { - wtl.txs = append(wtl.txs[:i], wtl.txs[i+1:]...) - return - } - - i++ - } +// Priority reports the application-assigned priority of w. +func (w *WrappedTx) Priority() int64 { + w.mtx.Lock() + defer w.mtx.Unlock() + return w.priority } diff --git a/internal/mempool/v1/tx_test.go b/internal/mempool/v1/tx_test.go deleted file mode 100644 index fb4beafab..000000000 --- a/internal/mempool/v1/tx_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package v1 - -import ( - "fmt" - "math/rand" - "sort" - "testing" - "time" - - "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/types" -) - -func TestTxStore_GetTxBySender(t *testing.T) { - txs := NewTxStore() - wtx := &WrappedTx{ - tx: []byte("test_tx"), - sender: "foo", - priority: 1, - timestamp: time.Now(), - } - - res := txs.GetTxBySender(wtx.sender) - require.Nil(t, res) - - txs.SetTx(wtx) - - res = txs.GetTxBySender(wtx.sender) - require.NotNil(t, res) - require.Equal(t, wtx, res) -} - -func TestTxStore_GetTxByHash(t *testing.T) { - txs := NewTxStore() - wtx := &WrappedTx{ - tx: []byte("test_tx"), - sender: "foo", - priority: 1, - timestamp: time.Now(), - } - - key := wtx.tx.Key() - res := txs.GetTxByHash(key) - require.Nil(t, res) - - txs.SetTx(wtx) - - res = txs.GetTxByHash(key) - require.NotNil(t, res) - require.Equal(t, wtx, res) -} - -func TestTxStore_SetTx(t *testing.T) { - txs := NewTxStore() - wtx := &WrappedTx{ - tx: []byte("test_tx"), - priority: 1, - timestamp: time.Now(), - } - - key := wtx.tx.Key() - txs.SetTx(wtx) - - res := txs.GetTxByHash(key) - require.NotNil(t, res) - require.Equal(t, wtx, res) - - wtx.sender = "foo" - txs.SetTx(wtx) - - res = txs.GetTxByHash(key) - require.NotNil(t, res) - require.Equal(t, wtx, res) -} - -func TestTxStore_GetOrSetPeerByTxHash(t *testing.T) { - txs := NewTxStore() - wtx := &WrappedTx{ - tx: []byte("test_tx"), - priority: 1, - timestamp: time.Now(), - } - - key := wtx.tx.Key() - txs.SetTx(wtx) - - res, ok := txs.GetOrSetPeerByTxHash(types.Tx([]byte("test_tx_2")).Key(), 15) - require.Nil(t, res) - require.False(t, ok) - - res, ok = txs.GetOrSetPeerByTxHash(key, 15) - require.NotNil(t, res) - require.False(t, ok) - - res, ok = txs.GetOrSetPeerByTxHash(key, 15) - require.NotNil(t, res) - require.True(t, ok) - - require.True(t, txs.TxHasPeer(key, 15)) - require.False(t, txs.TxHasPeer(key, 16)) -} - -func TestTxStore_RemoveTx(t *testing.T) { - txs := NewTxStore() - wtx := &WrappedTx{ - tx: []byte("test_tx"), - priority: 1, - timestamp: time.Now(), - } - - txs.SetTx(wtx) - - key := wtx.tx.Key() - res := txs.GetTxByHash(key) - require.NotNil(t, res) - - txs.RemoveTx(res) - - res = txs.GetTxByHash(key) - require.Nil(t, res) -} - -func TestTxStore_Size(t *testing.T) { - txStore := NewTxStore() - numTxs := 1000 - - for i := 0; i < numTxs; i++ { - txStore.SetTx(&WrappedTx{ - tx: []byte(fmt.Sprintf("test_tx_%d", i)), - priority: int64(i), - timestamp: time.Now(), - }) - } - - require.Equal(t, numTxs, txStore.Size()) -} - -func TestWrappedTxList_Reset(t *testing.T) { - list := NewWrappedTxList(func(wtx1, wtx2 *WrappedTx) bool { - return wtx1.height >= wtx2.height - }) - - require.Zero(t, list.Size()) - - for i := 0; i < 100; i++ { - list.Insert(&WrappedTx{height: int64(i)}) - } - - require.Equal(t, 100, list.Size()) - - list.Reset() - require.Zero(t, list.Size()) -} - -func TestWrappedTxList_Insert(t *testing.T) { - list := NewWrappedTxList(func(wtx1, wtx2 *WrappedTx) bool { - return wtx1.height >= wtx2.height - }) - - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - - var expected []int - for i := 0; i < 100; i++ { - height := rng.Int63n(10000) - expected = append(expected, int(height)) - list.Insert(&WrappedTx{height: height}) - - if i%10 == 0 { - list.Insert(&WrappedTx{height: height}) - expected = append(expected, int(height)) - } - } - - got := make([]int, list.Size()) - for i, wtx := range list.txs { - got[i] = int(wtx.height) - } - - sort.Ints(expected) - require.Equal(t, expected, got) -} - -func TestWrappedTxList_Remove(t *testing.T) { - list := NewWrappedTxList(func(wtx1, wtx2 *WrappedTx) bool { - return wtx1.height >= wtx2.height - }) - - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - - var txs []*WrappedTx - for i := 0; i < 100; i++ { - height := rng.Int63n(10000) - tx := &WrappedTx{height: height} - - txs = append(txs, tx) - list.Insert(tx) - - if i%10 == 0 { - tx = &WrappedTx{height: height} - list.Insert(tx) - txs = append(txs, tx) - } - } - - // remove a tx that does not exist - list.Remove(&WrappedTx{height: 20000}) - - // remove a tx that exists (by height) but not referenced - list.Remove(&WrappedTx{height: txs[0].height}) - - // remove a few existing txs - for i := 0; i < 25; i++ { - j := rng.Intn(len(txs)) - list.Remove(txs[j]) - txs = append(txs[:j], txs[j+1:]...) - } - - expected := make([]int, len(txs)) - for i, tx := range txs { - expected[i] = int(tx.height) - } - - got := make([]int, list.Size()) - for i, wtx := range list.txs { - got[i] = int(wtx.height) - } - - sort.Ints(expected) - require.Equal(t, expected, got) -} From bc49f66c353de4cbcd43a7add35727ec18eeef67 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Thu, 7 Jul 2022 14:56:34 -0700 Subject: [PATCH 26/58] Add more unit tests for the priority mempool. (#8961) - Add a test for time-based (TTL) expiration. - Add tests for eviction based on size and priority. --- internal/mempool/v1/mempool_test.go | 127 ++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/internal/mempool/v1/mempool_test.go b/internal/mempool/v1/mempool_test.go index 09f431853..b338301b8 100644 --- a/internal/mempool/v1/mempool_test.go +++ b/internal/mempool/v1/mempool_test.go @@ -95,6 +95,18 @@ func setup(t testing.TB, cacheSize int, options ...TxMempoolOption) *TxMempool { return NewTxMempool(log.TestingLogger().With("test", t.Name()), cfg.Mempool, appConnMem, 0, options...) } +// mustCheckTx invokes txmp.CheckTx for the given transaction and waits until +// its callback has finished executing. It fails t if CheckTx fails. +func mustCheckTx(t *testing.T, txmp *TxMempool, spec string) { + done := make(chan struct{}) + if err := txmp.CheckTx(context.Background(), []byte(spec), func(*abci.Response) { + close(done) + }, mempool.TxInfo{}); err != nil { + t.Fatalf("CheckTx for %q failed: %v", spec, err) + } + <-done +} + func checkTxs(t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx { txs := make([]testTx, numTxs) txInfo := mempool.TxInfo{SenderID: peerID} @@ -196,6 +208,76 @@ func TestTxMempool_Size(t *testing.T) { require.Equal(t, int64(2850), txmp.SizeBytes()) } +func TestTxMempool_Eviction(t *testing.T) { + txmp := setup(t, 0) + txmp.config.Size = 5 + txmp.config.MaxTxsBytes = 60 + txExists := func(spec string) bool { + txmp.Lock() + defer txmp.Unlock() + key := types.Tx(spec).Key() + _, ok := txmp.txByKey[key] + return ok + } + + // A transaction bigger than the mempool should be rejected even when there + // are slots available. + mustCheckTx(t, txmp, "big=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef=1") + require.Equal(t, 0, txmp.Size()) + + // Nearly-fill the mempool with a low-priority transaction, to show that it + // is evicted even when slots are available for a higher-priority tx. + const bigTx = "big=0123456789abcdef0123456789abcdef0123456789abcdef01234=2" + mustCheckTx(t, txmp, bigTx) + require.Equal(t, 1, txmp.Size()) // bigTx is the only element + require.True(t, txExists(bigTx)) + require.Equal(t, int64(len(bigTx)), txmp.SizeBytes()) + + // The next transaction should evict bigTx, because it is higher priority + // but does not fit on size. + mustCheckTx(t, txmp, "key1=0000=25") + require.True(t, txExists("key1=0000=25")) + require.False(t, txExists(bigTx)) + require.Equal(t, int64(len("key1=0000=25")), txmp.SizeBytes()) + + // Now fill up the rest of the slots with other transactions. + mustCheckTx(t, txmp, "key2=0001=5") + mustCheckTx(t, txmp, "key3=0002=10") + mustCheckTx(t, txmp, "key4=0003=3") + mustCheckTx(t, txmp, "key5=0004=3") + + // A new transaction with low priority should be discarded. + mustCheckTx(t, txmp, "key6=0005=1") + require.False(t, txExists("key6=0005=1")) + + // A new transaction with higher priority should evict key5, which is the + // newest of the two transactions with lowest priority. + mustCheckTx(t, txmp, "key7=0006=7") + require.True(t, txExists("key7=0006=7")) // new transaction added + require.False(t, txExists("key5=0004=3")) // newest low-priority tx evicted + require.True(t, txExists("key4=0003=3")) // older low-priority tx retained + + // Another new transaction evicts the other low-priority element. + mustCheckTx(t, txmp, "key8=0007=20") + require.True(t, txExists("key8=0007=20")) + require.False(t, txExists("key4=0003=3")) + + // Now the lowest-priority tx is 5, so that should be the next to go. + mustCheckTx(t, txmp, "key9=0008=9") + require.True(t, txExists("key9=0008=9")) + require.False(t, txExists("k3y2=0001=5")) + + // Add a transaction that requires eviction of multiple lower-priority + // entries, in order to fit the size of the element. + mustCheckTx(t, txmp, "key10=0123456789abcdef=11") // evict 10, 9, 7; keep 25, 20, 11 + require.True(t, txExists("key1=0000=25")) + require.True(t, txExists("key8=0007=20")) + require.True(t, txExists("key10=0123456789abcdef=11")) + require.False(t, txExists("key3=0002=10")) + require.False(t, txExists("key9=0008=9")) + require.False(t, txExists("key7=0006=7")) +} + func TestTxMempool_Flush(t *testing.T) { txmp := setup(t, 0) txs := checkTxs(t, txmp, 100, 0) @@ -438,6 +520,51 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) { require.Zero(t, txmp.SizeBytes()) } +func TestTxMempool_ExpiredTxs_Timestamp(t *testing.T) { + txmp := setup(t, 50) + txmp.config.TTLDuration = 5 * time.Millisecond + + added1 := checkTxs(t, txmp, 25, 0) + require.Equal(t, len(added1), txmp.Size()) + + // Wait a while, then add some more transactions that should not be expired + // when the first batch TTLs out. + // + // ms: 0 1 2 3 4 5 6 + // ^ ^ ^ ^ + // | | | +-- Update (triggers pruning) + // | | +------ first batch expires + // | +-------------- second batch added + // +-------------------------- first batch added + // + // The exact intervals are not important except that the delta should be + // large relative to the cost of CheckTx (ms vs. ns is fine here). + time.Sleep(3 * time.Millisecond) + added2 := checkTxs(t, txmp, 25, 1) + + // Wait a while longer, so that the first batch will expire. + time.Sleep(3 * time.Millisecond) + + // Trigger an update so that pruning will occur. + txmp.Lock() + defer txmp.Unlock() + require.NoError(t, txmp.Update(txmp.height+1, nil, nil, nil, nil)) + + // All the transactions in the original set should have been purged. + for _, tx := range added1 { + if _, ok := txmp.txByKey[tx.tx.Key()]; ok { + t.Errorf("Transaction %X should have been purged for TTL", tx.tx.Key()) + } + } + + // All the transactions added later should still be around. + for _, tx := range added2 { + if _, ok := txmp.txByKey[tx.tx.Key()]; !ok { + t.Errorf("Transaction %X should still be in the mempool, but is not", tx.tx.Key()) + } + } +} + func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { txmp := setup(t, 500) txmp.height = 100 From 156c305b08b17d83e28c03431632ec909fc04ca6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 8 Jul 2022 09:59:57 -0400 Subject: [PATCH 27/58] p2p: delete cruft (#8958) (#8959) I think the decision in #8806 is that we shouldn't do this yet, so I think it's best to just drop this. (cherry picked from commit 636320f9010be75c0ae804714ddce62b46f17930) Co-authored-by: Sam Kleinman --- internal/p2p/peermanager.go | 41 ------------------------------------- 1 file changed, 41 deletions(-) diff --git a/internal/p2p/peermanager.go b/internal/p2p/peermanager.go index 52d1cfe9f..469e761de 100644 --- a/internal/p2p/peermanager.go +++ b/internal/p2p/peermanager.go @@ -1446,47 +1446,6 @@ func (s *peerStore) Ranked() []*peerInfo { } sort.Slice(s.ranked, func(i, j int) bool { return s.ranked[i].Score() > s.ranked[j].Score() - // TODO: reevaluate more wholistic sorting, perhaps as follows: - - // // sort inactive peers after active peers - // if s.ranked[i].Inactive && !s.ranked[j].Inactive { - // return false - // } else if !s.ranked[i].Inactive && s.ranked[j].Inactive { - // return true - // } - - // iLastDialed, iLastDialSuccess := s.ranked[i].LastDialed() - // jLastDialed, jLastDialSuccess := s.ranked[j].LastDialed() - - // // sort peers who our most recent dialing attempt was - // // successful ahead of peers with recent dialing - // // failures - // switch { - // case iLastDialSuccess && jLastDialSuccess: - // // if both peers were (are?) successfully - // // connected, convey their score, but give the - // // one we dialed successfully most recently a bonus - - // iScore := s.ranked[i].Score() - // jScore := s.ranked[j].Score() - // if jLastDialed.Before(iLastDialed) { - // jScore++ - // } else { - // iScore++ - // } - - // return iScore > jScore - // case iLastDialSuccess: - // return true - // case jLastDialSuccess: - // return false - // default: - // // if both peers were not successful in their - // // most recent dialing attempt, fall back to - // // peer score. - - // return s.ranked[i].Score() > s.ranked[j].Score() - // } }) return s.ranked } From 1daf7b939df7df5040a28d91491e7ea15a7f92ea Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 8 Jul 2022 12:32:12 -0400 Subject: [PATCH 28/58] p2p: make peer gossiping coinflip safer (#8949) (#8963) Closes #8948 (cherry picked from commit 61ce384d752233c5f236d0b14c2685a259f0557a) Co-authored-by: Sam Kleinman --- internal/p2p/peermanager.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/p2p/peermanager.go b/internal/p2p/peermanager.go index 469e761de..2e15fc8d2 100644 --- a/internal/p2p/peermanager.go +++ b/internal/p2p/peermanager.go @@ -975,7 +975,7 @@ func (m *PeerManager) Advertise(peerID types.NodeID, limit uint16) []NodeAddress } var numAddresses int - var totalScore int + var totalAbsScore int ranked := m.store.Ranked() seenAddresses := map[NodeAddress]struct{}{} scores := map[types.NodeID]int{} @@ -986,8 +986,12 @@ func (m *PeerManager) Advertise(peerID types.NodeID, limit uint16) []NodeAddress continue } score := int(peer.Score()) + if score < 0 { + totalAbsScore += -score + } else { + totalAbsScore += score + } - totalScore += score scores[peer.ID] = score for addr := range peer.AddressInfo { if _, ok := m.options.PrivatePeers[addr.NodeID]; !ok { @@ -996,6 +1000,8 @@ func (m *PeerManager) Advertise(peerID types.NodeID, limit uint16) []NodeAddress } } + meanAbsScore := (totalAbsScore + 1) / (len(scores) + 1) + var attempts uint16 var addedLastIteration bool @@ -1044,7 +1050,7 @@ func (m *PeerManager) Advertise(peerID types.NodeID, limit uint16) []NodeAddress // peer. // nolint:gosec // G404: Use of weak random number generator - if numAddresses <= int(limit) || rand.Intn(totalScore+1) <= scores[peer.ID]+1 || rand.Intn((idx+1)*10) <= idx+1 { + if numAddresses <= int(limit) || rand.Intn((meanAbsScore*2)+1) <= scores[peer.ID]+1 || rand.Intn((idx+1)*10) <= idx+1 { addresses = append(addresses, addressInfo.Address) addedLastIteration = true seenAddresses[addressInfo.Address] = struct{}{} From 6a354a1e8d6046922a3ca210afc0d26122b70957 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Fri, 8 Jul 2022 09:54:50 -0700 Subject: [PATCH 29/58] Update pending changelog. (#8965) --- CHANGELOG_PENDING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 9c518842b..265833deb 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -27,3 +27,5 @@ Special thanks to external contributors on this release: ### IMPROVEMENTS ### BUG FIXES + +- [mempool] \#8944 Fix unbounded heap growth in the priority mempool. (@creachadair) From e3292a48e3224aa00fa9db9ec798948230460bb3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 8 Jul 2022 13:29:42 -0400 Subject: [PATCH 30/58] p2p: simpler priority queue (backport #8929) (#8956) --- internal/p2p/pqueue.go | 14 +++-- internal/p2p/router.go | 21 +++++-- internal/p2p/rqueue.go | 112 ++++++++++++++++++++++++++++++++++++ internal/p2p/rqueue_test.go | 47 +++++++++++++++ 4 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 internal/p2p/rqueue.go create mode 100644 internal/p2p/rqueue_test.go diff --git a/internal/p2p/pqueue.go b/internal/p2p/pqueue.go index d5051325f..dc63b319e 100644 --- a/internal/p2p/pqueue.go +++ b/internal/p2p/pqueue.go @@ -29,8 +29,16 @@ func (pq priorityQueue) get(i int) *pqEnvelope { return pq[i] } func (pq priorityQueue) Len() int { return len(pq) } func (pq priorityQueue) Less(i, j int) bool { - // if both elements have the same priority, prioritize based on most recent + // if both elements have the same priority, prioritize based + // on most recent and largest if pq[i].priority == pq[j].priority { + diff := pq[i].timestamp.Sub(pq[j].timestamp) + if diff < 0 { + diff *= -1 + } + if diff < 10*time.Millisecond { + return pq[i].size > pq[j].size + } return pq[i].timestamp.After(pq[j].timestamp) } @@ -272,12 +280,10 @@ func (s *pqScheduler) process() { } func (s *pqScheduler) push(pqEnv *pqEnvelope) { - chIDStr := strconv.Itoa(int(pqEnv.envelope.channelID)) - // enqueue the incoming Envelope heap.Push(s.pq, pqEnv) s.size += pqEnv.size - s.metrics.PeerQueueMsgSize.With("ch_id", chIDStr).Add(float64(pqEnv.size)) + s.metrics.PeerQueueMsgSize.With("ch_id", strconv.Itoa(int(pqEnv.envelope.channelID))).Add(float64(pqEnv.size)) // Update the cumulative sizes by adding the Envelope's size to every // priority less than or equal to it. diff --git a/internal/p2p/router.go b/internal/p2p/router.go index 7247642ca..56558a80f 100644 --- a/internal/p2p/router.go +++ b/internal/p2p/router.go @@ -40,6 +40,10 @@ type Envelope struct { channelID ChannelID } +func (e Envelope) IsZero() bool { + return e.From == "" && e.To == "" && e.Message == nil +} + // PeerError is a peer error reported via Channel.Error. // // FIXME: This currently just disconnects the peer, which is too simplistic. @@ -166,9 +170,10 @@ type RouterOptions struct { } const ( - queueTypeFifo = "fifo" - queueTypePriority = "priority" - queueTypeWDRR = "wdrr" + queueTypeFifo = "fifo" + queueTypePriority = "priority" + queueTypeWDRR = "wdrr" + queueTypeSimplePriority = "simple-priority" ) // Validate validates router options. @@ -176,8 +181,8 @@ func (o *RouterOptions) Validate() error { switch o.QueueType { case "": o.QueueType = queueTypeFifo - case queueTypeFifo, queueTypeWDRR, queueTypePriority: - // passI me + case queueTypeFifo, queueTypeWDRR, queueTypePriority, queueTypeSimplePriority: + // pass default: return fmt.Errorf("queue type %q is not supported", o.QueueType) } @@ -354,6 +359,9 @@ func (r *Router) createQueueFactory() (func(int) queue, error) { return q }, nil + case queueTypeSimplePriority: + return func(size int) queue { return newSimplePriorityQueue(r.stopCtx(), size, r.chDescs) }, nil + default: return nil, fmt.Errorf("cannot construct queue of type %q", r.options.QueueType) } @@ -420,6 +428,9 @@ func (r *Router) routeChannel( for { select { case envelope := <-outCh: + if envelope.IsZero() { + continue + } // Mark the envelope with the channel ID to allow sendPeer() to pass // it on to Transport.SendMessage(). envelope.channelID = chID diff --git a/internal/p2p/rqueue.go b/internal/p2p/rqueue.go new file mode 100644 index 000000000..02826bfe9 --- /dev/null +++ b/internal/p2p/rqueue.go @@ -0,0 +1,112 @@ +package p2p + +import ( + "container/heap" + "context" + "sort" + "time" + + "github.com/gogo/protobuf/proto" +) + +type simpleQueue struct { + input chan Envelope + output chan Envelope + closeFn func() + closeCh <-chan struct{} + + maxSize int + chDescs []ChannelDescriptor +} + +func newSimplePriorityQueue(ctx context.Context, size int, chDescs []ChannelDescriptor) *simpleQueue { + if size%2 != 0 { + size++ + } + + ctx, cancel := context.WithCancel(ctx) + q := &simpleQueue{ + input: make(chan Envelope, size*2), + output: make(chan Envelope, size/2), + maxSize: size * size, + closeCh: ctx.Done(), + closeFn: cancel, + } + + go q.run(ctx) + return q +} + +func (q *simpleQueue) enqueue() chan<- Envelope { return q.input } +func (q *simpleQueue) dequeue() <-chan Envelope { return q.output } +func (q *simpleQueue) close() { q.closeFn() } +func (q *simpleQueue) closed() <-chan struct{} { return q.closeCh } + +func (q *simpleQueue) run(ctx context.Context) { + defer q.closeFn() + + var chPriorities = make(map[ChannelID]uint, len(q.chDescs)) + for _, chDesc := range q.chDescs { + chID := ChannelID(chDesc.ID) + chPriorities[chID] = uint(chDesc.Priority) + } + + pq := make(priorityQueue, 0, q.maxSize) + heap.Init(&pq) + ticker := time.NewTicker(10 * time.Millisecond) + // must have a buffer of exactly one because both sides of + // this channel are used in this loop, and simply signals adds + // to the heap + signal := make(chan struct{}, 1) + for { + select { + case <-ctx.Done(): + return + case <-q.closeCh: + return + case e := <-q.input: + // enqueue the incoming Envelope + heap.Push(&pq, &pqEnvelope{ + envelope: e, + size: uint(proto.Size(e.Message)), + priority: chPriorities[e.channelID], + timestamp: time.Now().UTC(), + }) + + select { + case signal <- struct{}{}: + default: + if len(pq) > q.maxSize { + sort.Sort(pq) + pq = pq[:q.maxSize] + } + } + + case <-ticker.C: + if len(pq) > q.maxSize { + sort.Sort(pq) + pq = pq[:q.maxSize] + } + if len(pq) > 0 { + select { + case signal <- struct{}{}: + default: + } + } + case <-signal: + SEND: + for len(pq) > 0 { + select { + case <-ctx.Done(): + return + case <-q.closeCh: + return + case q.output <- heap.Pop(&pq).(*pqEnvelope).envelope: + continue SEND + default: + break SEND + } + } + } + } +} diff --git a/internal/p2p/rqueue_test.go b/internal/p2p/rqueue_test.go new file mode 100644 index 000000000..43c4066e5 --- /dev/null +++ b/internal/p2p/rqueue_test.go @@ -0,0 +1,47 @@ +package p2p + +import ( + "context" + "testing" + "time" +) + +func TestSimpleQueue(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // set up a small queue with very small buffers so we can + // watch it shed load, then send a bunch of messages to the + // queue, most of which we'll watch it drop. + sq := newSimplePriorityQueue(ctx, 1, nil) + for i := 0; i < 100; i++ { + sq.enqueue() <- Envelope{From: "merlin"} + } + + seen := 0 + +RETRY: + for seen <= 2 { + select { + case e := <-sq.dequeue(): + if e.From != "merlin" { + continue + } + seen++ + case <-time.After(10 * time.Millisecond): + break RETRY + } + } + // if we don't see any messages, then it's just broken. + if seen == 0 { + t.Errorf("seen %d messages, should have seen more than one", seen) + } + // ensure that load shedding happens: there can be at most 3 + // messages that we get out of this, one that was buffered + // plus 2 that were under the cap, everything else gets + // dropped. + if seen > 3 { + t.Errorf("saw %d messages, should have seen 5 or fewer", seen) + } + +} From 451e6973312693c019c76c5b0814a34a94d72f7f Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Mon, 11 Jul 2022 06:18:36 -0700 Subject: [PATCH 31/58] Update generated mocks after upgrade of Mockery v2. (#8973) --- abci/client/mocks/client.go | 4 ++-- internal/consensus/mocks/cons_sync_reactor.go | 4 ++-- internal/evidence/mocks/block_store.go | 4 ++-- internal/p2p/mocks/connection.go | 4 ++-- internal/p2p/mocks/peer.go | 4 ++-- internal/p2p/mocks/transport.go | 4 ++-- internal/proxy/mocks/app_conn_consensus.go | 4 ++-- internal/proxy/mocks/app_conn_mempool.go | 4 ++-- internal/proxy/mocks/app_conn_query.go | 4 ++-- internal/proxy/mocks/app_conn_snapshot.go | 4 ++-- internal/state/indexer/mocks/event_sink.go | 4 ++-- internal/state/mocks/block_store.go | 4 ++-- internal/state/mocks/evidence_pool.go | 4 ++-- internal/state/mocks/store.go | 4 ++-- internal/statesync/mocks/state_provider.go | 4 ++-- light/provider/mocks/provider.go | 4 ++-- light/rpc/mocks/light_client.go | 4 ++-- rpc/client/mocks/client.go | 4 ++-- 18 files changed, 36 insertions(+), 36 deletions(-) diff --git a/abci/client/mocks/client.go b/abci/client/mocks/client.go index cfe5ea7af..efd0960d6 100644 --- a/abci/client/mocks/client.go +++ b/abci/client/mocks/client.go @@ -802,13 +802,13 @@ func (_m *Client) Wait() { _m.Called() } -type NewClientT interface { +type mockConstructorTestingTNewClient interface { mock.TestingT Cleanup(func()) } // NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewClient(t NewClientT) *Client { +func NewClient(t mockConstructorTestingTNewClient) *Client { mock := &Client{} mock.Mock.Test(t) diff --git a/internal/consensus/mocks/cons_sync_reactor.go b/internal/consensus/mocks/cons_sync_reactor.go index 2c694742b..25caddfb9 100644 --- a/internal/consensus/mocks/cons_sync_reactor.go +++ b/internal/consensus/mocks/cons_sync_reactor.go @@ -27,13 +27,13 @@ func (_m *ConsSyncReactor) SwitchToConsensus(_a0 state.State, _a1 bool) { _m.Called(_a0, _a1) } -type NewConsSyncReactorT interface { +type mockConstructorTestingTNewConsSyncReactor interface { mock.TestingT Cleanup(func()) } // NewConsSyncReactor creates a new instance of ConsSyncReactor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewConsSyncReactor(t NewConsSyncReactorT) *ConsSyncReactor { +func NewConsSyncReactor(t mockConstructorTestingTNewConsSyncReactor) *ConsSyncReactor { mock := &ConsSyncReactor{} mock.Mock.Test(t) diff --git a/internal/evidence/mocks/block_store.go b/internal/evidence/mocks/block_store.go index b0c67ff87..e61c4e0ae 100644 --- a/internal/evidence/mocks/block_store.go +++ b/internal/evidence/mocks/block_store.go @@ -58,13 +58,13 @@ func (_m *BlockStore) LoadBlockMeta(height int64) *types.BlockMeta { return r0 } -type NewBlockStoreT interface { +type mockConstructorTestingTNewBlockStore interface { mock.TestingT Cleanup(func()) } // NewBlockStore creates a new instance of BlockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewBlockStore(t NewBlockStoreT) *BlockStore { +func NewBlockStore(t mockConstructorTestingTNewBlockStore) *BlockStore { mock := &BlockStore{} mock.Mock.Test(t) diff --git a/internal/p2p/mocks/connection.go b/internal/p2p/mocks/connection.go index e5ba9584a..db86225b9 100644 --- a/internal/p2p/mocks/connection.go +++ b/internal/p2p/mocks/connection.go @@ -209,13 +209,13 @@ func (_m *Connection) TrySendMessage(_a0 p2p.ChannelID, _a1 []byte) (bool, error return r0, r1 } -type NewConnectionT interface { +type mockConstructorTestingTNewConnection interface { mock.TestingT Cleanup(func()) } // NewConnection creates a new instance of Connection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewConnection(t NewConnectionT) *Connection { +func NewConnection(t mockConstructorTestingTNewConnection) *Connection { mock := &Connection{} mock.Mock.Test(t) diff --git a/internal/p2p/mocks/peer.go b/internal/p2p/mocks/peer.go index 021c905f2..67721be47 100644 --- a/internal/p2p/mocks/peer.go +++ b/internal/p2p/mocks/peer.go @@ -333,13 +333,13 @@ func (_m *Peer) Wait() { _m.Called() } -type NewPeerT interface { +type mockConstructorTestingTNewPeer interface { mock.TestingT Cleanup(func()) } // NewPeer creates a new instance of Peer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewPeer(t NewPeerT) *Peer { +func NewPeer(t mockConstructorTestingTNewPeer) *Peer { mock := &Peer{} mock.Mock.Test(t) diff --git a/internal/p2p/mocks/transport.go b/internal/p2p/mocks/transport.go index 46d825501..2cf93e078 100644 --- a/internal/p2p/mocks/transport.go +++ b/internal/p2p/mocks/transport.go @@ -120,13 +120,13 @@ func (_m *Transport) String() string { return r0 } -type NewTransportT interface { +type mockConstructorTestingTNewTransport interface { mock.TestingT Cleanup(func()) } // NewTransport creates a new instance of Transport. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewTransport(t NewTransportT) *Transport { +func NewTransport(t mockConstructorTestingTNewTransport) *Transport { mock := &Transport{} mock.Mock.Test(t) diff --git a/internal/proxy/mocks/app_conn_consensus.go b/internal/proxy/mocks/app_conn_consensus.go index bff6bf6a4..b9f37301c 100644 --- a/internal/proxy/mocks/app_conn_consensus.go +++ b/internal/proxy/mocks/app_conn_consensus.go @@ -151,13 +151,13 @@ func (_m *AppConnConsensus) SetResponseCallback(_a0 abciclient.Callback) { _m.Called(_a0) } -type NewAppConnConsensusT interface { +type mockConstructorTestingTNewAppConnConsensus interface { mock.TestingT Cleanup(func()) } // NewAppConnConsensus creates a new instance of AppConnConsensus. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewAppConnConsensus(t NewAppConnConsensusT) *AppConnConsensus { +func NewAppConnConsensus(t mockConstructorTestingTNewAppConnConsensus) *AppConnConsensus { mock := &AppConnConsensus{} mock.Mock.Test(t) diff --git a/internal/proxy/mocks/app_conn_mempool.go b/internal/proxy/mocks/app_conn_mempool.go index a17deb7ca..6d62194fc 100644 --- a/internal/proxy/mocks/app_conn_mempool.go +++ b/internal/proxy/mocks/app_conn_mempool.go @@ -119,13 +119,13 @@ func (_m *AppConnMempool) SetResponseCallback(_a0 abciclient.Callback) { _m.Called(_a0) } -type NewAppConnMempoolT interface { +type mockConstructorTestingTNewAppConnMempool interface { mock.TestingT Cleanup(func()) } // NewAppConnMempool creates a new instance of AppConnMempool. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewAppConnMempool(t NewAppConnMempoolT) *AppConnMempool { +func NewAppConnMempool(t mockConstructorTestingTNewAppConnMempool) *AppConnMempool { mock := &AppConnMempool{} mock.Mock.Test(t) diff --git a/internal/proxy/mocks/app_conn_query.go b/internal/proxy/mocks/app_conn_query.go index 9bde883a0..b73be9c0b 100644 --- a/internal/proxy/mocks/app_conn_query.go +++ b/internal/proxy/mocks/app_conn_query.go @@ -98,13 +98,13 @@ func (_m *AppConnQuery) QuerySync(_a0 context.Context, _a1 types.RequestQuery) ( return r0, r1 } -type NewAppConnQueryT interface { +type mockConstructorTestingTNewAppConnQuery interface { mock.TestingT Cleanup(func()) } // NewAppConnQuery creates a new instance of AppConnQuery. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewAppConnQuery(t NewAppConnQueryT) *AppConnQuery { +func NewAppConnQuery(t mockConstructorTestingTNewAppConnQuery) *AppConnQuery { mock := &AppConnQuery{} mock.Mock.Test(t) diff --git a/internal/proxy/mocks/app_conn_snapshot.go b/internal/proxy/mocks/app_conn_snapshot.go index 36df29781..d7469c44d 100644 --- a/internal/proxy/mocks/app_conn_snapshot.go +++ b/internal/proxy/mocks/app_conn_snapshot.go @@ -121,13 +121,13 @@ func (_m *AppConnSnapshot) OfferSnapshotSync(_a0 context.Context, _a1 types.Requ return r0, r1 } -type NewAppConnSnapshotT interface { +type mockConstructorTestingTNewAppConnSnapshot interface { mock.TestingT Cleanup(func()) } // NewAppConnSnapshot creates a new instance of AppConnSnapshot. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewAppConnSnapshot(t NewAppConnSnapshotT) *AppConnSnapshot { +func NewAppConnSnapshot(t mockConstructorTestingTNewAppConnSnapshot) *AppConnSnapshot { mock := &AppConnSnapshot{} mock.Mock.Test(t) diff --git a/internal/state/indexer/mocks/event_sink.go b/internal/state/indexer/mocks/event_sink.go index ec5e279a8..984d8414d 100644 --- a/internal/state/indexer/mocks/event_sink.go +++ b/internal/state/indexer/mocks/event_sink.go @@ -166,13 +166,13 @@ func (_m *EventSink) Type() indexer.EventSinkType { return r0 } -type NewEventSinkT interface { +type mockConstructorTestingTNewEventSink interface { mock.TestingT Cleanup(func()) } // NewEventSink creates a new instance of EventSink. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewEventSink(t NewEventSinkT) *EventSink { +func NewEventSink(t mockConstructorTestingTNewEventSink) *EventSink { mock := &EventSink{} mock.Mock.Test(t) diff --git a/internal/state/mocks/block_store.go b/internal/state/mocks/block_store.go index b4d0c7f99..23532da75 100644 --- a/internal/state/mocks/block_store.go +++ b/internal/state/mocks/block_store.go @@ -209,13 +209,13 @@ func (_m *BlockStore) Size() int64 { return r0 } -type NewBlockStoreT interface { +type mockConstructorTestingTNewBlockStore interface { mock.TestingT Cleanup(func()) } // NewBlockStore creates a new instance of BlockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewBlockStore(t NewBlockStoreT) *BlockStore { +func NewBlockStore(t mockConstructorTestingTNewBlockStore) *BlockStore { mock := &BlockStore{} mock.Mock.Test(t) diff --git a/internal/state/mocks/evidence_pool.go b/internal/state/mocks/evidence_pool.go index 8bc208289..9a21c73ce 100644 --- a/internal/state/mocks/evidence_pool.go +++ b/internal/state/mocks/evidence_pool.go @@ -69,13 +69,13 @@ func (_m *EvidencePool) Update(_a0 state.State, _a1 types.EvidenceList) { _m.Called(_a0, _a1) } -type NewEvidencePoolT interface { +type mockConstructorTestingTNewEvidencePool interface { mock.TestingT Cleanup(func()) } // NewEvidencePool creates a new instance of EvidencePool. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewEvidencePool(t NewEvidencePoolT) *EvidencePool { +func NewEvidencePool(t mockConstructorTestingTNewEvidencePool) *EvidencePool { mock := &EvidencePool{} mock.Mock.Test(t) diff --git a/internal/state/mocks/store.go b/internal/state/mocks/store.go index d08ba4c9e..91dad3127 100644 --- a/internal/state/mocks/store.go +++ b/internal/state/mocks/store.go @@ -187,13 +187,13 @@ func (_m *Store) SaveValidatorSets(_a0 int64, _a1 int64, _a2 *types.ValidatorSet return r0 } -type NewStoreT interface { +type mockConstructorTestingTNewStore interface { mock.TestingT Cleanup(func()) } // NewStore creates a new instance of Store. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewStore(t NewStoreT) *Store { +func NewStore(t mockConstructorTestingTNewStore) *Store { mock := &Store{} mock.Mock.Test(t) diff --git a/internal/statesync/mocks/state_provider.go b/internal/statesync/mocks/state_provider.go index 17ddb54ac..099588ed1 100644 --- a/internal/statesync/mocks/state_provider.go +++ b/internal/statesync/mocks/state_provider.go @@ -83,13 +83,13 @@ func (_m *StateProvider) State(ctx context.Context, height uint64) (state.State, return r0, r1 } -type NewStateProviderT interface { +type mockConstructorTestingTNewStateProvider interface { mock.TestingT Cleanup(func()) } // NewStateProvider creates a new instance of StateProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewStateProvider(t NewStateProviderT) *StateProvider { +func NewStateProvider(t mockConstructorTestingTNewStateProvider) *StateProvider { mock := &StateProvider{} mock.Mock.Test(t) diff --git a/light/provider/mocks/provider.go b/light/provider/mocks/provider.go index 9a5789ce9..a17bc4cb5 100644 --- a/light/provider/mocks/provider.go +++ b/light/provider/mocks/provider.go @@ -52,13 +52,13 @@ func (_m *Provider) ReportEvidence(_a0 context.Context, _a1 types.Evidence) erro return r0 } -type NewProviderT interface { +type mockConstructorTestingTNewProvider interface { mock.TestingT Cleanup(func()) } // NewProvider creates a new instance of Provider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewProvider(t NewProviderT) *Provider { +func NewProvider(t mockConstructorTestingTNewProvider) *Provider { mock := &Provider{} mock.Mock.Test(t) diff --git a/light/rpc/mocks/light_client.go b/light/rpc/mocks/light_client.go index 25e101c86..fabf73b01 100644 --- a/light/rpc/mocks/light_client.go +++ b/light/rpc/mocks/light_client.go @@ -100,13 +100,13 @@ func (_m *LightClient) VerifyLightBlockAtHeight(ctx context.Context, height int6 return r0, r1 } -type NewLightClientT interface { +type mockConstructorTestingTNewLightClient interface { mock.TestingT Cleanup(func()) } // NewLightClient creates a new instance of LightClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewLightClient(t NewLightClientT) *LightClient { +func NewLightClient(t mockConstructorTestingTNewLightClient) *LightClient { mock := &LightClient{} mock.Mock.Test(t) diff --git a/rpc/client/mocks/client.go b/rpc/client/mocks/client.go index 0f1581f70..2b5ff2670 100644 --- a/rpc/client/mocks/client.go +++ b/rpc/client/mocks/client.go @@ -801,13 +801,13 @@ func (_m *Client) Validators(ctx context.Context, height *int64, page *int, perP return r0, r1 } -type NewClientT interface { +type mockConstructorTestingTNewClient interface { mock.TestingT Cleanup(func()) } // NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewClient(t NewClientT) *Client { +func NewClient(t mockConstructorTestingTNewClient) *Client { mock := &Client{} mock.Mock.Test(t) From f98de20f7e196650654c11ce8201e83fe225edfb Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Mon, 11 Jul 2022 16:34:05 -0700 Subject: [PATCH 32/58] p2p: ensure closed channels stop receiving service (#8979) Once these channels are closed, we should not continue to service them, as they will never again deliver nonzero values. --- internal/p2p/router.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/p2p/router.go b/internal/p2p/router.go index 56558a80f..55377169c 100644 --- a/internal/p2p/router.go +++ b/internal/p2p/router.go @@ -427,8 +427,10 @@ func (r *Router) routeChannel( ) { for { select { - case envelope := <-outCh: - if envelope.IsZero() { + case envelope, ok := <-outCh: + if !ok { + return + } else if envelope.IsZero() { continue } // Mark the envelope with the channel ID to allow sendPeer() to pass @@ -507,7 +509,10 @@ func (r *Router) routeChannel( } } - case peerError := <-errCh: + case peerError, ok := <-errCh: + if !ok { + return + } maxPeerCapacity := r.peerManager.HasMaxPeerCapacity() r.logger.Error("peer error", "peer", peerError.NodeID, From cb93d3b587cd85f4c918f30bbd90311f827760c0 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Mon, 11 Jul 2022 18:06:49 -0700 Subject: [PATCH 33/58] mempool: don't log message type mismatch in the default callback (#8969) --- internal/mempool/v1/mempool.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/mempool/v1/mempool.go b/internal/mempool/v1/mempool.go index 9c4122d8e..aa9ef69b0 100644 --- a/internal/mempool/v1/mempool.go +++ b/internal/mempool/v1/mempool.go @@ -462,6 +462,10 @@ func (txmp *TxMempool) Update( func (txmp *TxMempool) initialTxCallback(wtx *WrappedTx, res *abci.Response) { checkTxRes, ok := res.Value.(*abci.Response_CheckTx) if !ok { + txmp.logger.Error("mempool: received incorrect result type in CheckTx callback", + "expected", reflect.TypeOf(&abci.Response_CheckTx{}).Name(), + "got", reflect.TypeOf(res.Value).Name(), + ) return } @@ -630,10 +634,8 @@ func (txmp *TxMempool) insertTx(wtx *WrappedTx) { func (txmp *TxMempool) recheckTxCallback(req *abci.Request, res *abci.Response) { checkTxRes, ok := res.Value.(*abci.Response_CheckTx) if !ok { - txmp.logger.Error("mempool: received incorrect result type in CheckTx callback", - "expected", reflect.TypeOf(&abci.Response_CheckTx{}).Name(), - "got", reflect.TypeOf(res.Value).Name(), - ) + // Don't log this; this is the default callback and other response types + // can safely be ignored. return } From 9e64c95e56dad03c566cc6b9966e205d0d66b299 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Tue, 12 Jul 2022 08:00:29 -0700 Subject: [PATCH 34/58] mempool: reduce lock contention during CheckTx (cleanup) (#8983) The way this was originally structured, we reacquired the lock after issuing the initial ABCI CheckTx call, only to immediately release it. Restructure the code so that this redundant acquire is no longer necessary. --- internal/mempool/v1/mempool.go | 109 +++++++++++++++++---------------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/internal/mempool/v1/mempool.go b/internal/mempool/v1/mempool.go index aa9ef69b0..f797568c9 100644 --- a/internal/mempool/v1/mempool.go +++ b/internal/mempool/v1/mempool.go @@ -177,69 +177,70 @@ func (txmp *TxMempool) CheckTx( // During the initial phase of CheckTx, we do not need to modify any state. // A transaction will not actually be added to the mempool until it survives // a call to the ABCI CheckTx method and size constraint checks. - txmp.mtx.RLock() - defer txmp.mtx.RUnlock() + height, err := func() (int64, error) { + txmp.mtx.RLock() + defer txmp.mtx.RUnlock() - // Reject transactions in excess of the configured maximum transaction size. - if len(tx) > txmp.config.MaxTxBytes { - return types.ErrTxTooLarge{Max: txmp.config.MaxTxBytes, Actual: len(tx)} - } - - // If a precheck hook is defined, call it before invoking the application. - if txmp.preCheck != nil { - if err := txmp.preCheck(tx); err != nil { - return types.ErrPreCheck{Reason: err} + // Reject transactions in excess of the configured maximum transaction size. + if len(tx) > txmp.config.MaxTxBytes { + return 0, types.ErrTxTooLarge{Max: txmp.config.MaxTxBytes, Actual: len(tx)} } - } - // Early exit if the proxy connection has an error. - if err := txmp.proxyAppConn.Error(); err != nil { + // If a precheck hook is defined, call it before invoking the application. + if txmp.preCheck != nil { + if err := txmp.preCheck(tx); err != nil { + return 0, types.ErrPreCheck{Reason: err} + } + } + + // Early exit if the proxy connection has an error. + if err := txmp.proxyAppConn.Error(); err != nil { + return 0, err + } + + txKey := tx.Key() + + // Check for the transaction in the cache. + if !txmp.cache.Push(tx) { + // If the cached transaction is also in the pool, record its sender. + if elt, ok := txmp.txByKey[txKey]; ok { + w := elt.Value.(*WrappedTx) + w.SetPeer(txInfo.SenderID) + } + return 0, types.ErrTxInCache + } + return txmp.height, nil + }() + if err != nil { return err } - txKey := tx.Key() - - // Check for the transaction in the cache. - if !txmp.cache.Push(tx) { - // If the cached transaction is also in the pool, record its sender. - if elt, ok := txmp.txByKey[txKey]; ok { - w := elt.Value.(*WrappedTx) - w.SetPeer(txInfo.SenderID) - } - return types.ErrTxInCache - } - // Initiate an ABCI CheckTx for this transaction. The callback is // responsible for adding the transaction to the pool if it survives. - return func() error { - // N.B.: We have to issue the call outside the lock. In a local client, - // even an "async" call invokes its callback immediately which will make - // the callback deadlock trying to acquire the same lock. This isn't a - // problem with out-of-process calls, but this has to work for both. - height := txmp.height - txmp.mtx.RUnlock() - defer txmp.mtx.RLock() - - reqRes, err := txmp.proxyAppConn.CheckTxAsync(ctx, abci.RequestCheckTx{Tx: tx}) - if err != nil { - txmp.cache.Remove(tx) - return err + // + // N.B.: We have to issue the call outside the lock. In a local client, + // even an "async" call invokes its callback immediately which will make + // the callback deadlock trying to acquire the same lock. This isn't a + // problem with out-of-process calls, but this has to work for both. + reqRes, err := txmp.proxyAppConn.CheckTxAsync(ctx, abci.RequestCheckTx{Tx: tx}) + if err != nil { + txmp.cache.Remove(tx) + return err + } + reqRes.SetCallback(func(res *abci.Response) { + wtx := &WrappedTx{ + tx: tx, + hash: tx.Key(), + timestamp: time.Now().UTC(), + height: height, } - reqRes.SetCallback(func(res *abci.Response) { - wtx := &WrappedTx{ - tx: tx, - hash: txKey, - timestamp: time.Now().UTC(), - height: height, - } - wtx.SetPeer(txInfo.SenderID) - txmp.initialTxCallback(wtx, res) - if cb != nil { - cb(res) - } - }) - return nil - }() + wtx.SetPeer(txInfo.SenderID) + txmp.initialTxCallback(wtx, res) + if cb != nil { + cb(res) + } + }) + return nil } // RemoveTxByKey removes the transaction with the specified key from the From 379096815648dfc9d007a33530016b9e58cef492 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Tue, 12 Jul 2022 10:28:51 -0700 Subject: [PATCH 35/58] mempool: release lock during app connection flush (#8984) This case is symmetric to what we did for CheckTx calls, where we release the mempool mutex to ensure callbacks can fire during call setup. We also need this behaviour for application flush, for the same reason: The caller holds the lock by contract from the Mempool interface. --- internal/mempool/v1/mempool.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/mempool/v1/mempool.go b/internal/mempool/v1/mempool.go index f797568c9..67c4c2858 100644 --- a/internal/mempool/v1/mempool.go +++ b/internal/mempool/v1/mempool.go @@ -131,6 +131,15 @@ func (txmp *TxMempool) SizeBytes() int64 { return atomic.LoadInt64(&txmp.txsByte // The caller must hold an exclusive mempool lock (by calling txmp.Lock) before // calling FlushAppConn. func (txmp *TxMempool) FlushAppConn() error { + // N.B.: We have to issue the call outside the lock so that its callback can + // fire. It's safe to do this, the flush will block until complete. + // + // We could just not require the caller to hold the lock at all, but the + // semantics of the Mempool interface require the caller to hold it, and we + // can't change that without disrupting existing use. + txmp.mtx.Unlock() + defer txmp.mtx.Lock() + return txmp.proxyAppConn.FlushSync(context.Background()) } From a1c8f8df0b0e81e2c9a86de9e4bd1e3804029e63 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 11:44:08 +0200 Subject: [PATCH 36/58] doc: fix typos in quick-start.md. (#8990) (#8997) --- docs/introduction/quick-start.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/introduction/quick-start.md b/docs/introduction/quick-start.md index 040da8eb2..74baf7201 100644 --- a/docs/introduction/quick-start.md +++ b/docs/introduction/quick-start.md @@ -106,10 +106,10 @@ Next, use the `tendermint testnet` command to create four directories of config Before you can start the network, you'll need peers identifiers (IPs are not enough and can change). We'll refer to them as ID1, ID2, ID3, ID4. ```sh -tendermint show_node_id --home ./mytestnet/node0 -tendermint show_node_id --home ./mytestnet/node1 -tendermint show_node_id --home ./mytestnet/node2 -tendermint show_node_id --home ./mytestnet/node3 +tendermint show-node-id --home ./mytestnet/node0 +tendermint show-node-id --home ./mytestnet/node1 +tendermint show-node-id --home ./mytestnet/node2 +tendermint show-node-id --home ./mytestnet/node3 ``` Finally, from each machine, run: From b94470a6a42e8ffe7e7467521de5f51eb937c454 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Thu, 14 Jul 2022 06:51:54 -0700 Subject: [PATCH 37/58] mempool: ensure evicted transactions are removed from the cache (#9000) In the original implementation transactions evicted for priority were also removed from the cache. In addition, remove expired transactions from the cache. Related: - Add Has method to cache implementations. - Update tests to exercise this condition. --- internal/mempool/cache.go | 13 +++++++++++++ internal/mempool/v1/mempool.go | 3 +++ internal/mempool/v1/mempool_test.go | 12 ++++++++---- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/internal/mempool/cache.go b/internal/mempool/cache.go index 3cd45d2bc..deaae09e7 100644 --- a/internal/mempool/cache.go +++ b/internal/mempool/cache.go @@ -22,6 +22,10 @@ type TxCache interface { // Remove removes the given raw transaction from the cache. Remove(tx types.Tx) + + // Has reports whether tx is present in the cache. Checking for presence is + // not treated as an access of the value. + Has(tx types.Tx) bool } var _ TxCache = (*LRUTxCache)(nil) @@ -97,6 +101,14 @@ func (c *LRUTxCache) Remove(tx types.Tx) { } } +func (c *LRUTxCache) Has(tx types.Tx) bool { + c.mtx.Lock() + defer c.mtx.Unlock() + + _, ok := c.cacheMap[tx.Key()] + return ok +} + // NopTxCache defines a no-op raw transaction cache. type NopTxCache struct{} @@ -105,3 +117,4 @@ var _ TxCache = (*NopTxCache)(nil) func (NopTxCache) Reset() {} func (NopTxCache) Push(types.Tx) bool { return true } func (NopTxCache) Remove(types.Tx) {} +func (NopTxCache) Has(types.Tx) bool { return false } diff --git a/internal/mempool/v1/mempool.go b/internal/mempool/v1/mempool.go index 67c4c2858..622ee2c86 100644 --- a/internal/mempool/v1/mempool.go +++ b/internal/mempool/v1/mempool.go @@ -597,6 +597,7 @@ func (txmp *TxMempool) initialTxCallback(wtx *WrappedTx, res *abci.Response) { "old_priority", w.priority, ) txmp.removeTxByElement(vic) + txmp.cache.Remove(w.tx) txmp.metrics.EvictedTxs.Add(1) // We may not need to evict all the eligible transactions. Bail out @@ -783,9 +784,11 @@ func (txmp *TxMempool) purgeExpiredTxs(blockHeight int64) { w := cur.Value.(*WrappedTx) if txmp.config.TTLNumBlocks > 0 && (blockHeight-w.height) > txmp.config.TTLNumBlocks { txmp.removeTxByElement(cur) + txmp.cache.Remove(w.tx) txmp.metrics.EvictedTxs.Add(1) } else if txmp.config.TTLDuration > 0 && now.Sub(w.timestamp) > txmp.config.TTLDuration { txmp.removeTxByElement(cur) + txmp.cache.Remove(w.tx) txmp.metrics.EvictedTxs.Add(1) } cur = next diff --git a/internal/mempool/v1/mempool_test.go b/internal/mempool/v1/mempool_test.go index b338301b8..616264c70 100644 --- a/internal/mempool/v1/mempool_test.go +++ b/internal/mempool/v1/mempool_test.go @@ -209,7 +209,7 @@ func TestTxMempool_Size(t *testing.T) { } func TestTxMempool_Eviction(t *testing.T) { - txmp := setup(t, 0) + txmp := setup(t, 1000) txmp.config.Size = 5 txmp.config.MaxTxsBytes = 60 txExists := func(spec string) bool { @@ -238,6 +238,7 @@ func TestTxMempool_Eviction(t *testing.T) { mustCheckTx(t, txmp, "key1=0000=25") require.True(t, txExists("key1=0000=25")) require.False(t, txExists(bigTx)) + require.False(t, txmp.cache.Has([]byte(bigTx))) require.Equal(t, int64(len("key1=0000=25")), txmp.SizeBytes()) // Now fill up the rest of the slots with other transactions. @@ -521,10 +522,10 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) { } func TestTxMempool_ExpiredTxs_Timestamp(t *testing.T) { - txmp := setup(t, 50) + txmp := setup(t, 5000) txmp.config.TTLDuration = 5 * time.Millisecond - added1 := checkTxs(t, txmp, 25, 0) + added1 := checkTxs(t, txmp, 10, 0) require.Equal(t, len(added1), txmp.Size()) // Wait a while, then add some more transactions that should not be expired @@ -540,7 +541,7 @@ func TestTxMempool_ExpiredTxs_Timestamp(t *testing.T) { // The exact intervals are not important except that the delta should be // large relative to the cost of CheckTx (ms vs. ns is fine here). time.Sleep(3 * time.Millisecond) - added2 := checkTxs(t, txmp, 25, 1) + added2 := checkTxs(t, txmp, 10, 1) // Wait a while longer, so that the first batch will expire. time.Sleep(3 * time.Millisecond) @@ -555,6 +556,9 @@ func TestTxMempool_ExpiredTxs_Timestamp(t *testing.T) { if _, ok := txmp.txByKey[tx.tx.Key()]; ok { t.Errorf("Transaction %X should have been purged for TTL", tx.tx.Key()) } + if txmp.cache.Has(tx.tx) { + t.Errorf("Transaction %X should have been removed from the cache", tx.tx.Key()) + } } // All the transactions added later should still be around. From 7971514b556b34b6f7718b6e7296955d0b25d042 Mon Sep 17 00:00:00 2001 From: William Banfield <4561443+williambanfield@users.noreply.github.com> Date: Thu, 14 Jul 2022 10:11:34 -0400 Subject: [PATCH 38/58] p2p: configure max accepted for non-legacy as well (#8999) * p2p: configure max connected for non-legacy as well * remove explicit 0 --- node/setup.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/node/setup.go b/node/setup.go index e94e85c78..f4bd0f13c 100644 --- a/node/setup.go +++ b/node/setup.go @@ -441,12 +441,21 @@ func createConsensusReactor( } func createTransport(logger log.Logger, cfg *config.Config) *p2p.MConnTransport { + var maxAccepted uint32 + switch { + case cfg.P2P.MaxConnections > 0 && !cfg.P2P.UseLegacy: + maxAccepted = uint32(cfg.P2P.MaxConnections) + + uint32(len(tmstrings.SplitAndTrimEmpty(cfg.P2P.UnconditionalPeerIDs, ",", " "))) + + case cfg.P2P.MaxNumInboundPeers > 0: + maxAccepted = uint32(cfg.P2P.MaxNumInboundPeers) + + uint32(len(tmstrings.SplitAndTrimEmpty(cfg.P2P.UnconditionalPeerIDs, ",", " "))) + } + return p2p.NewMConnTransport( logger, p2p.MConnConfig(cfg.P2P), []*p2p.ChannelDescriptor{}, p2p.MConnTransportOptions{ - MaxAcceptedConnections: uint32(cfg.P2P.MaxNumInboundPeers + - len(tmstrings.SplitAndTrimEmpty(cfg.P2P.UnconditionalPeerIDs, ",", " ")), - ), + MaxAcceptedConnections: maxAccepted, }, ) } From f8d15fc6828f45694870d23da5f54d2bcd037e2e Mon Sep 17 00:00:00 2001 From: Sam Kleinman Date: Thu, 14 Jul 2022 13:19:12 -0400 Subject: [PATCH 39/58] blocksync: drop support for enabled=false (#8912) --- cmd/tendermint/commands/run_node.go | 3 --- node/node.go | 37 +++++++++++++---------------- node/setup.go | 4 ++++ test/e2e/generator/generate.go | 4 +--- test/e2e/networks/ci.toml | 5 ---- test/e2e/pkg/testnet.go | 2 +- 6 files changed, 22 insertions(+), 33 deletions(-) diff --git a/cmd/tendermint/commands/run_node.go b/cmd/tendermint/commands/run_node.go index 435ce9ea4..dcd62c491 100644 --- a/cmd/tendermint/commands/run_node.go +++ b/cmd/tendermint/commands/run_node.go @@ -34,9 +34,6 @@ func AddNodeFlags(cmd *cobra.Command) { config.PrivValidator.ListenAddr, "socket address to listen on for connections from external priv-validator process") - // node flags - cmd.Flags().Bool("blocksync.enable", config.BlockSync.Enable, "enable fast blockchain syncing") - // TODO (https://github.com/tendermint/tendermint/issues/6908): remove this check after the v0.35 release cycle // This check was added to give users an upgrade prompt to use the new flag for syncing. // diff --git a/node/node.go b/node/node.go index bea42b338..fcdc547a5 100644 --- a/node/node.go +++ b/node/node.go @@ -250,7 +250,7 @@ func makeNode(cfg *config.Config, // Determine whether we should do block sync. This must happen after the handshake, since the // app may modify the validator set, specifying ourself as the only validator. - blockSync := cfg.BlockSync.Enable && !onlyValidatorIsUs(state, pubKey) + blockSync := !onlyValidatorIsUs(state, pubKey) logNodeStartupInfo(state, pubKey, logger, consensusLogger, cfg.Mode) @@ -777,30 +777,25 @@ func (n *nodeImpl) OnStart() error { n.consensusReactor.SetStateSyncingMetrics(0) - d := types.EventDataStateSyncStatus{Complete: true, Height: state.LastBlockHeight} - if err := n.eventBus.PublishEventStateSyncStatus(d); err != nil { + if err := n.eventBus.PublishEventStateSyncStatus( + types.EventDataStateSyncStatus{Complete: true, Height: state.LastBlockHeight}, + ); err != nil { n.eventBus.Logger.Error("failed to emit the statesync start event", "err", err) } - // TODO: Some form of orchestrator is needed here between the state - // advancing reactors to be able to control which one of the three - // is running - if n.config.BlockSync.Enable { - // FIXME Very ugly to have these metrics bleed through here. - n.consensusReactor.SetBlockSyncingMetrics(1) - if err := bcR.SwitchToBlockSync(state); err != nil { - n.Logger.Error("failed to switch to block sync", "err", err) - return - } - - d := types.EventDataBlockSyncStatus{Complete: false, Height: state.LastBlockHeight} - if err := n.eventBus.PublishEventBlockSyncStatus(d); err != nil { - n.eventBus.Logger.Error("failed to emit the block sync starting event", "err", err) - } - - } else { - n.consensusReactor.SwitchToConsensus(state, true) + // FIXME Very ugly to have these metrics bleed through here. + n.consensusReactor.SetBlockSyncingMetrics(1) + if err := bcR.SwitchToBlockSync(state); err != nil { + n.Logger.Error("failed to switch to block sync", "err", err) + return } + + if err := n.eventBus.PublishEventBlockSyncStatus( + types.EventDataBlockSyncStatus{Complete: false, Height: state.LastBlockHeight}, + ); err != nil { + n.eventBus.Logger.Error("failed to emit the block sync starting event", "err", err) + } + }() } diff --git a/node/setup.go b/node/setup.go index f4bd0f13c..3dad14991 100644 --- a/node/setup.go +++ b/node/setup.go @@ -338,6 +338,10 @@ func createBlockchainReactor( metrics *consensus.Metrics, ) (*p2p.ReactorShim, service.Service, error) { + if !cfg.BlockSync.Enable { + logger.Error("blocksync.enable = false, but Tendermint no longer allows blocksync to be disabled. This setting is now ignored and will be removed in the next version.") + } + logger = logger.With("module", "blockchain") switch cfg.BlockSync.Version { diff --git a/test/e2e/generator/generate.go b/test/e2e/generator/generate.go index 4a17ddf3a..c66c32e03 100644 --- a/test/e2e/generator/generate.go +++ b/test/e2e/generator/generate.go @@ -47,8 +47,6 @@ var ( "tcp": 20, "unix": 10, } - // FIXME: v2 disabled due to flake - nodeBlockSyncs = uniformChoice{"v0"} // "v2" nodeMempools = uniformChoice{"v0", "v1"} nodeStateSyncs = weightedChoice{ e2e.StateSyncDisabled: 10, @@ -397,7 +395,7 @@ func generateNode( StartAt: startAt, Database: nodeDatabases.Choose(r), PrivvalProtocol: nodePrivvalProtocols.Choose(r), - BlockSync: nodeBlockSyncs.Choose(r).(string), + BlockSync: "v0", Mempool: nodeMempools.Choose(r).(string), StateSync: e2e.StateSyncDisabled, PersistInterval: ptrUint64(uint64(nodePersistIntervals.Choose(r).(int))), diff --git a/test/e2e/networks/ci.toml b/test/e2e/networks/ci.toml index 7e07febd5..bf835df35 100644 --- a/test/e2e/networks/ci.toml +++ b/test/e2e/networks/ci.toml @@ -43,7 +43,6 @@ persist_interval = 0 perturb = ["restart"] privval_protocol = "tcp" seeds = ["seed01"] -block_sync = "v0" [node.validator03] database = "badgerdb" @@ -52,7 +51,6 @@ abci_protocol = "grpc" persist_interval = 3 perturb = ["kill"] privval_protocol = "grpc" -block_sync = "v0" retain_blocks = 10 [node.validator04] @@ -61,11 +59,9 @@ snapshot_interval = 5 database = "rocksdb" persistent_peers = ["validator01"] perturb = ["pause"] -block_sync = "v0" [node.validator05] database = "cleveldb" -block_sync = "v0" state_sync = "p2p" seeds = ["seed01"] start_at = 1005 # Becomes part of the validator set at 1010 @@ -76,7 +72,6 @@ privval_protocol = "tcp" [node.full01] mode = "full" start_at = 1010 -block_sync = "v0" persistent_peers = ["validator01", "validator02", "validator03", "validator04"] perturb = ["restart"] retain_blocks = 10 diff --git a/test/e2e/pkg/testnet.go b/test/e2e/pkg/testnet.go index fe085dd1d..d1bbc7cb1 100644 --- a/test/e2e/pkg/testnet.go +++ b/test/e2e/pkg/testnet.go @@ -178,7 +178,7 @@ func LoadTestnet(file string) (*Testnet, error) { ABCIProtocol: Protocol(testnet.ABCIProtocol), PrivvalProtocol: ProtocolFile, StartAt: nodeManifest.StartAt, - BlockSync: nodeManifest.BlockSync, + BlockSync: "v0", Mempool: nodeManifest.Mempool, StateSync: nodeManifest.StateSync, PersistInterval: 1, From 0c6efd8c51056fb52da1356aa193929b938f6dd2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 17:13:41 -0400 Subject: [PATCH 40/58] config: update config to reflect simple-priority queue (#9007) (#9008) Update the queue documentation to reflect the types of queues and current default queue. (cherry picked from commit c1c501ecd43dec53e56a197f7013bface2a962fb) Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com> --- config/config.go | 2 +- config/toml.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index c4182fe48..e7628b7f1 100644 --- a/config/config.go +++ b/config/config.go @@ -762,7 +762,7 @@ type P2PConfig struct { //nolint: maligned UseLegacy bool `mapstructure:"use-legacy"` // Makes it possible to configure which queue backend the p2p - // layer uses. Options are: "fifo", "priority" and "wdrr", + // layer uses. Options are: "fifo", "simple-priority", "priority", and "wdrr", // with the default being "priority". QueueType string `mapstructure:"queue-type"` } diff --git a/config/toml.go b/config/toml.go index 284e1919a..4792f9d63 100644 --- a/config/toml.go +++ b/config/toml.go @@ -298,10 +298,11 @@ pprof-laddr = "{{ .RPC.PprofListenAddress }}" ####################################################### [p2p] -# Enable the legacy p2p layer. +# Select the p2p internal queue. +# Options are: "fifo", "simple-priority", "priority", and "wdrr" +# with the default being "priority". use-legacy = {{ .P2P.UseLegacy }} -# Select the p2p internal queue queue-type = "{{ .P2P.QueueType }}" # Address to listen for incoming connections From 917720675009c28fc912ec36b8dba145779cefcf Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Thu, 14 Jul 2022 14:49:37 -0700 Subject: [PATCH 41/58] Prepare changelog for Release v0.35.8 (#8988) --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ CHANGELOG_PENDING.md | 6 +----- version/version.go | 2 +- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d467f29e..1ef64569b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/cosmos). +## v0.35.8 + +July 12, 2022 + +Special thanks to external contributors on this release: @joeabbey + +This release fixes an unbounded heap growth issue in the implementation of the +priority mempool, as well as some configuration, logging, and peer dialing +improvements in the non-legacy p2p stack. It also adds a new opt-in +"simple-priority" value for the `p2p.queue-type` setting, that should improve +gossip performance for non-legacy peer networks. + +### BREAKING CHANGES + +- CLI/RPC/Config + + - [node] [\#8902](https://github.com/tendermint/tendermint/pull/8902) Always start blocksync and avoid misconfiguration (@tychoish) + +### FEATURES + +- [cli] [\#8675](https://github.com/tendermint/tendermint/pull/8675) Add command to force compact goleveldb databases (@cmwaters) + +### IMPROVEMENTS + +- [p2p] [\#8914](https://github.com/tendermint/tendermint/pull/8914) [\#8875](https://github.com/tendermint/tendermint/pull/8875) Improvements to peer dialing (backported). (@tychoish) +- [p2p] [\#8820](https://github.com/tendermint/tendermint/pull/8820) add eviction metrics and cleanup dialing error handling (backport #8819) (@tychoish) +- [logging] [\#8896](https://github.com/tendermint/tendermint/pull/8896) Do not pre-process log results (backport #8895). (@tychoish) +- [p2p] [\#8956](https://github.com/tendermint/tendermint/pull/8956) Simpler priority queue (backport #8929). (@tychoish) + +### BUG FIXES + +- [mempool] [\#8944](https://github.com/tendermint/tendermint/pull/8944) Fix unbounded heap growth in the priority mempool. (@creachadair) +- [p2p] [\#8869](https://github.com/tendermint/tendermint/pull/8869) Set empty timeouts to configed values. (backport #8847). (@williambanfield) + + ## v0.35.7 June 16, 2022 diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 265833deb..183026e46 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -2,7 +2,7 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/cosmos). -## v0.35.8 +## v0.35.9 Month DD, YYYY @@ -22,10 +22,6 @@ Special thanks to external contributors on this release: ### FEATURES -- [cli] [\#8675] Add command to force compact goleveldb databases (@cmwaters) - ### IMPROVEMENTS ### BUG FIXES - -- [mempool] \#8944 Fix unbounded heap growth in the priority mempool. (@creachadair) diff --git a/version/version.go b/version/version.go index b0096fdca..1f69337c1 100644 --- a/version/version.go +++ b/version/version.go @@ -10,7 +10,7 @@ const ( // TMVersionDefault is the used as the fallback version of Tendermint Core // when not using git describe. It is formatted with semantic versioning. - TMVersionDefault = "0.35.7" + TMVersionDefault = "0.35.8" // ABCISemVer is the semantic version of the ABCI library ABCISemVer = "0.17.0" From 819e7f4bdd1b2ed73509b257da54897bce081421 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 16:42:27 -0700 Subject: [PATCH 42/58] build(deps): Bump google.golang.org/grpc from 1.47.0 to 1.48.0 (#8992) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.47.0 to 1.48.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.47.0...v1.48.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 66ce029a0..169099001 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 - google.golang.org/grpc v1.47.0 + google.golang.org/grpc v1.48.0 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gotest.tools v2.2.0+incompatible // indirect pgregory.net/rapid v0.4.7 diff --git a/go.sum b/go.sum index 7afca0a73..7f23153ad 100644 --- a/go.sum +++ b/go.sum @@ -1786,8 +1786,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From 9f2522148bd32f3bf830e974d01d19a897f27431 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Fri, 15 Jul 2022 07:11:16 -0700 Subject: [PATCH 43/58] config: fix the comments on p2p.queue-type (#9021) These got disarranged during a previous cleanup. --- config/toml.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/toml.go b/config/toml.go index 4792f9d63..7755a908b 100644 --- a/config/toml.go +++ b/config/toml.go @@ -298,11 +298,12 @@ pprof-laddr = "{{ .RPC.PprofListenAddress }}" ####################################################### [p2p] +# Enable the legacy p2p layer. +use-legacy = {{ .P2P.UseLegacy }} + # Select the p2p internal queue. # Options are: "fifo", "simple-priority", "priority", and "wdrr" # with the default being "priority". -use-legacy = {{ .P2P.UseLegacy }} - queue-type = "{{ .P2P.QueueType }}" # Address to listen for incoming connections From 6b18dfcea145007bd7403d3f466a4a1741956751 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 15 Jul 2022 08:46:28 -0700 Subject: [PATCH 44/58] Extract a library from the confix command-line tool. (backport #9012) (#9025) (cherry picked from commit 18b5a500da06723823da0424a99e13a7bcad7d68) Pull out the library functionality from scripts/confix and move it to internal/libs/confix. Replace scripts/confix with a simple stub that has the same command-line API, but uses the library instead. Related: - Move and update unit tests. - Move scripts/confix/condiff to scripts/condiff. - Update test data for v34, v35, and v36. - Update reference diffs. - Update testdata README. Co-authored-by: M. J. Fromberger --- internal/libs/confix/confix.go | 155 ++++++++++++++++++ .../libs}/confix/confix_test.go | 4 +- {scripts => internal/libs}/confix/plan.go | 2 +- .../libs}/confix/testdata/README.md | 4 +- .../libs}/confix/testdata/baseline.txt | 0 .../libs}/confix/testdata/diff-26-27.txt | 0 .../libs}/confix/testdata/diff-27-28.txt | 0 .../libs}/confix/testdata/diff-28-29.txt | 0 .../libs}/confix/testdata/diff-29-30.txt | 0 .../libs}/confix/testdata/diff-30-31.txt | 0 internal/libs/confix/testdata/diff-31-32.txt | 5 + internal/libs/confix/testdata/diff-32-33.txt | 6 + internal/libs/confix/testdata/diff-33-34.txt | 20 +++ internal/libs/confix/testdata/diff-34-35.txt | 28 ++++ internal/libs/confix/testdata/diff-35-36.txt | 29 ++++ .../libs}/confix/testdata/non-config.toml | 0 .../libs}/confix/testdata/v26-config.toml | 0 .../libs}/confix/testdata/v27-config.toml | 0 .../libs}/confix/testdata/v28-config.toml | 0 .../libs}/confix/testdata/v29-config.toml | 0 .../libs}/confix/testdata/v30-config.toml | 0 .../libs}/confix/testdata/v31-config.toml | 0 .../libs}/confix/testdata/v32-config.toml | 0 .../libs}/confix/testdata/v33-config.toml | 0 .../libs}/confix/testdata/v34-config.toml | 27 +++ .../libs}/confix/testdata/v35-config.toml | 8 +- .../libs}/confix/testdata/v36-config.toml | 10 +- scripts/condiff/condiff.go | 152 +++++++++++++++++ scripts/confix/confix.go | 132 ++------------- 29 files changed, 452 insertions(+), 130 deletions(-) create mode 100644 internal/libs/confix/confix.go rename {scripts => internal/libs}/confix/confix_test.go (97%) rename {scripts => internal/libs}/confix/plan.go (99%) rename {scripts => internal/libs}/confix/testdata/README.md (90%) rename {scripts => internal/libs}/confix/testdata/baseline.txt (100%) rename {scripts => internal/libs}/confix/testdata/diff-26-27.txt (100%) rename {scripts => internal/libs}/confix/testdata/diff-27-28.txt (100%) rename {scripts => internal/libs}/confix/testdata/diff-28-29.txt (100%) rename {scripts => internal/libs}/confix/testdata/diff-29-30.txt (100%) rename {scripts => internal/libs}/confix/testdata/diff-30-31.txt (100%) create mode 100644 internal/libs/confix/testdata/diff-31-32.txt create mode 100644 internal/libs/confix/testdata/diff-32-33.txt create mode 100644 internal/libs/confix/testdata/diff-33-34.txt create mode 100644 internal/libs/confix/testdata/diff-34-35.txt create mode 100644 internal/libs/confix/testdata/diff-35-36.txt rename {scripts => internal/libs}/confix/testdata/non-config.toml (100%) rename {scripts => internal/libs}/confix/testdata/v26-config.toml (100%) rename {scripts => internal/libs}/confix/testdata/v27-config.toml (100%) rename {scripts => internal/libs}/confix/testdata/v28-config.toml (100%) rename {scripts => internal/libs}/confix/testdata/v29-config.toml (100%) rename {scripts => internal/libs}/confix/testdata/v30-config.toml (100%) rename {scripts => internal/libs}/confix/testdata/v31-config.toml (100%) rename {scripts => internal/libs}/confix/testdata/v32-config.toml (100%) rename {scripts => internal/libs}/confix/testdata/v33-config.toml (100%) rename {scripts => internal/libs}/confix/testdata/v34-config.toml (93%) rename {scripts => internal/libs}/confix/testdata/v35-config.toml (98%) rename {scripts => internal/libs}/confix/testdata/v36-config.toml (98%) create mode 100644 scripts/condiff/condiff.go diff --git a/internal/libs/confix/confix.go b/internal/libs/confix/confix.go new file mode 100644 index 000000000..a9449fa22 --- /dev/null +++ b/internal/libs/confix/confix.go @@ -0,0 +1,155 @@ +// Package confix applies changes to a Tendermint TOML configuration file, to +// update configurations created with an older version of Tendermint to a +// compatible format for a newer version. +package confix + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/creachadair/atomicfile" + "github.com/creachadair/tomledit" + "github.com/creachadair/tomledit/transform" + "github.com/spf13/viper" + + "github.com/tendermint/tendermint/config" +) + +// Upgrade reads the configuration file at configPath and applies any +// transformations necessary to upgrade it to the current version. If this +// succeeds, the transformed output is written to outputPath. As a special +// case, if outputPath == "" the output is written to stdout. +// +// It is safe if outputPath == inputPath. If a regular file outputPath already +// exists, it is overwritten. In case of error, the output is not written. +// +// Upgrade is a convenience wrapper for calls to LoadConfig, ApplyFixes, and +// CheckValid. If the caller requires more control over the behavior of the +// upgrade, call those functions directly. +func Upgrade(ctx context.Context, configPath, outputPath string) error { + if configPath == "" { + return errors.New("empty input configuration path") + } + + doc, err := LoadConfig(configPath) + if err != nil { + return fmt.Errorf("loading config: %v", err) + } + + if err := ApplyFixes(ctx, doc); err != nil { + return fmt.Errorf("updating %q: %v", configPath, err) + } + + var buf bytes.Buffer + if err := tomledit.Format(&buf, doc); err != nil { + return fmt.Errorf("formatting config: %v", err) + } + + // Verify that Tendermint can parse the results after our edits. + if err := CheckValid(buf.Bytes()); err != nil { + return fmt.Errorf("updated config is invalid: %v", err) + } + + if outputPath == "" { + _, err = os.Stdout.Write(buf.Bytes()) + } else { + err = atomicfile.WriteData(outputPath, buf.Bytes(), 0600) + } + return err +} + +// ApplyFixes transforms doc and reports whether it succeeded. +func ApplyFixes(ctx context.Context, doc *tomledit.Document) error { + // Check what version of Tendermint might have created this config file, as + // a safety check for the updates we are about to make. + tmVersion := GuessConfigVersion(doc) + if tmVersion == vUnknown { + return errors.New("cannot tell what Tendermint version created this config") + } else if tmVersion < v34 || tmVersion > v36 { + // TODO(creachadair): Add in rewrites for older versions. This will + // require some digging to discover what the changes were. The upgrade + // instructions do not give specifics. + return fmt.Errorf("unable to update version %s config", tmVersion) + } + return plan.Apply(ctx, doc) +} + +// LoadConfig loads and parses the TOML document from path. +func LoadConfig(path string) (*tomledit.Document, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + return tomledit.Parse(f) +} + +const ( + vUnknown = "" + v32 = "v0.32" + v33 = "v0.33" + v34 = "v0.34" + v35 = "v0.35" + v36 = "v0.36" +) + +// GuessConfigVersion attempts to figure out which version of Tendermint +// created the specified config document. It returns "" if the creating version +// cannot be determined, otherwise a string of the form "vX.YY". +func GuessConfigVersion(doc *tomledit.Document) string { + hasDisableWS := doc.First("rpc", "experimental-disable-websocket") != nil + hasUseLegacy := doc.First("p2p", "use-legacy") != nil // v0.35 only + if hasDisableWS && !hasUseLegacy { + return v36 + } + + hasBlockSync := transform.FindTable(doc, "blocksync") != nil // add: v0.35 + hasStateSync := transform.FindTable(doc, "statesync") != nil // add: v0.34 + if hasBlockSync && hasStateSync { + return v35 + } else if hasStateSync { + return v34 + } + + hasIndexKeys := doc.First("tx_index", "index_keys") != nil // add: v0.33 + hasIndexTags := doc.First("tx_index", "index_tags") != nil // rem: v0.33 + if hasIndexKeys && !hasIndexTags { + return v33 + } + + hasFastSync := transform.FindTable(doc, "fastsync") != nil // add: v0.32 + if hasIndexTags && hasFastSync { + return v32 + } + + // Something older, probably. + return vUnknown +} + +// CheckValid checks whether the specified config appears to be a valid +// Tendermint config file. This emulates how the node loads the config. +func CheckValid(data []byte) error { + v := viper.New() + v.SetConfigType("toml") + + if err := v.ReadConfig(bytes.NewReader(data)); err != nil { + return fmt.Errorf("reading config: %w", err) + } + + var cfg config.Config + if err := v.Unmarshal(&cfg); err != nil { + return fmt.Errorf("decoding config: %w", err) + } + + return cfg.ValidateBasic() +} + +// WithLogWriter returns a child of ctx with a logger attached that sends +// output to w. This is a convenience wrapper for transform.WithLogWriter. +func WithLogWriter(ctx context.Context, w io.Writer) context.Context { + return transform.WithLogWriter(ctx, w) +} diff --git a/scripts/confix/confix_test.go b/internal/libs/confix/confix_test.go similarity index 97% rename from scripts/confix/confix_test.go rename to internal/libs/confix/confix_test.go index ec258f4ca..dc0042fe5 100644 --- a/scripts/confix/confix_test.go +++ b/internal/libs/confix/confix_test.go @@ -1,4 +1,4 @@ -package main_test +package confix_test import ( "bytes" @@ -9,7 +9,7 @@ import ( "github.com/creachadair/tomledit" "github.com/google/go-cmp/cmp" - confix "github.com/tendermint/tendermint/scripts/confix" + "github.com/tendermint/tendermint/internal/libs/confix" ) func mustParseConfig(t *testing.T, path string) *tomledit.Document { diff --git a/scripts/confix/plan.go b/internal/libs/confix/plan.go similarity index 99% rename from scripts/confix/plan.go rename to internal/libs/confix/plan.go index 7e08c9baf..47c04a582 100644 --- a/scripts/confix/plan.go +++ b/internal/libs/confix/plan.go @@ -1,4 +1,4 @@ -package main +package confix import ( "context" diff --git a/scripts/confix/testdata/README.md b/internal/libs/confix/testdata/README.md similarity index 90% rename from scripts/confix/testdata/README.md rename to internal/libs/confix/testdata/README.md index 5bbfa795f..04f2af205 100644 --- a/scripts/confix/testdata/README.md +++ b/internal/libs/confix/testdata/README.md @@ -41,12 +41,12 @@ The files named `diff-XX-YY.txt` were generated by using the `condiff` tool on the config samples for versions v0.XX and v0.YY: ```shell -go run ./scripts/confix/condiff -desnake vXX-config vYY-config.toml > diff-XX-YY.txt +go run ./scripts/condiff -desnake vXX-config vYY-config.toml > diff-XX-YY.txt ``` The `baseline.txt` was computed in the same way, but using an empty starting file so that we capture all the settings in the target: ```shell -go run ./scripts/confix/condiff -desnake /dev/null v26-config.toml > baseline.txt +go run ./scripts/condiff -desnake /dev/null v26-config.toml > baseline.txt ``` diff --git a/scripts/confix/testdata/baseline.txt b/internal/libs/confix/testdata/baseline.txt similarity index 100% rename from scripts/confix/testdata/baseline.txt rename to internal/libs/confix/testdata/baseline.txt diff --git a/scripts/confix/testdata/diff-26-27.txt b/internal/libs/confix/testdata/diff-26-27.txt similarity index 100% rename from scripts/confix/testdata/diff-26-27.txt rename to internal/libs/confix/testdata/diff-26-27.txt diff --git a/scripts/confix/testdata/diff-27-28.txt b/internal/libs/confix/testdata/diff-27-28.txt similarity index 100% rename from scripts/confix/testdata/diff-27-28.txt rename to internal/libs/confix/testdata/diff-27-28.txt diff --git a/scripts/confix/testdata/diff-28-29.txt b/internal/libs/confix/testdata/diff-28-29.txt similarity index 100% rename from scripts/confix/testdata/diff-28-29.txt rename to internal/libs/confix/testdata/diff-28-29.txt diff --git a/scripts/confix/testdata/diff-29-30.txt b/internal/libs/confix/testdata/diff-29-30.txt similarity index 100% rename from scripts/confix/testdata/diff-29-30.txt rename to internal/libs/confix/testdata/diff-29-30.txt diff --git a/scripts/confix/testdata/diff-30-31.txt b/internal/libs/confix/testdata/diff-30-31.txt similarity index 100% rename from scripts/confix/testdata/diff-30-31.txt rename to internal/libs/confix/testdata/diff-30-31.txt diff --git a/internal/libs/confix/testdata/diff-31-32.txt b/internal/libs/confix/testdata/diff-31-32.txt new file mode 100644 index 000000000..98855bade --- /dev/null +++ b/internal/libs/confix/testdata/diff-31-32.txt @@ -0,0 +1,5 @@ ++S fastsync ++M fastsync.version ++M mempool.max-tx-bytes ++M rpc.max-body-bytes ++M rpc.max-header-bytes diff --git a/internal/libs/confix/testdata/diff-32-33.txt b/internal/libs/confix/testdata/diff-32-33.txt new file mode 100644 index 000000000..7aa61856a --- /dev/null +++ b/internal/libs/confix/testdata/diff-32-33.txt @@ -0,0 +1,6 @@ ++M p2p.persistent-peers-max-dial-period ++M p2p.unconditional-peer-ids ++M tx-index.index-all-keys +-M tx-index.index-all-tags ++M tx-index.index-keys +-M tx-index.index-tags diff --git a/internal/libs/confix/testdata/diff-33-34.txt b/internal/libs/confix/testdata/diff-33-34.txt new file mode 100644 index 000000000..a0ac7a98d --- /dev/null +++ b/internal/libs/confix/testdata/diff-33-34.txt @@ -0,0 +1,20 @@ +-M prof-laddr ++M consensus.double-sign-check-height ++M mempool.keep-invalid-txs-in-cache ++M mempool.max-batch-bytes ++M rpc.experimental-close-on-slow-client ++M rpc.experimental-subscription-buffer-size ++M rpc.experimental-websocket-write-buffer-size ++M rpc.pprof-laddr ++S statesync ++M statesync.enable ++M statesync.rpc-servers ++M statesync.trust-height ++M statesync.trust-hash ++M statesync.trust-period ++M statesync.discovery-time ++M statesync.temp-dir ++M statesync.chunk-request-timeout ++M statesync.chunk-fetchers +-M tx-index.index-all-keys +-M tx-index.index-keys diff --git a/internal/libs/confix/testdata/diff-34-35.txt b/internal/libs/confix/testdata/diff-34-35.txt new file mode 100644 index 000000000..de08f2965 --- /dev/null +++ b/internal/libs/confix/testdata/diff-34-35.txt @@ -0,0 +1,28 @@ +-M fast-sync ++M mode +-M priv-validator-key-file +-M priv-validator-laddr +-M priv-validator-state-file ++S blocksync ++M blocksync.enable ++M blocksync.version +-S fastsync +-M fastsync.version +-M mempool.wal-dir ++M p2p.bootstrap-peers ++M p2p.max-connections ++M p2p.max-incoming-connection-attempts ++M p2p.max-outgoing-connections ++M p2p.queue-type +-M p2p.seed-mode ++M p2p.use-legacy ++S priv-validator ++M priv-validator.key-file ++M priv-validator.state-file ++M priv-validator.laddr ++M priv-validator.client-certificate-file ++M priv-validator.client-key-file ++M priv-validator.root-ca-file +-M statesync.chunk-fetchers ++M statesync.fetchers ++M statesync.use-p2p diff --git a/internal/libs/confix/testdata/diff-35-36.txt b/internal/libs/confix/testdata/diff-35-36.txt new file mode 100644 index 000000000..298c53056 --- /dev/null +++ b/internal/libs/confix/testdata/diff-35-36.txt @@ -0,0 +1,29 @@ +-S blocksync +-M blocksync.enable +-M blocksync.version +-M consensus.skip-timeout-commit +-M consensus.timeout-commit +-M consensus.timeout-precommit +-M consensus.timeout-precommit-delta +-M consensus.timeout-prevote +-M consensus.timeout-prevote-delta +-M consensus.timeout-propose +-M consensus.timeout-propose-delta +-M mempool.recheck +-M mempool.version +-M p2p.addr-book-file +-M p2p.addr-book-strict +-M p2p.max-num-inbound-peers +-M p2p.max-num-outbound-peers +-M p2p.persistent-peers-max-dial-period +-M p2p.seeds +-M p2p.unconditional-peer-ids +-M p2p.use-legacy ++M rpc.event-log-max-items ++M rpc.event-log-window-size +-M rpc.experimental-close-on-slow-client ++M rpc.experimental-disable-websocket +-M rpc.experimental-subscription-buffer-size +-M rpc.experimental-websocket-write-buffer-size +-M rpc.grpc-laddr +-M rpc.grpc-max-open-connections diff --git a/scripts/confix/testdata/non-config.toml b/internal/libs/confix/testdata/non-config.toml similarity index 100% rename from scripts/confix/testdata/non-config.toml rename to internal/libs/confix/testdata/non-config.toml diff --git a/scripts/confix/testdata/v26-config.toml b/internal/libs/confix/testdata/v26-config.toml similarity index 100% rename from scripts/confix/testdata/v26-config.toml rename to internal/libs/confix/testdata/v26-config.toml diff --git a/scripts/confix/testdata/v27-config.toml b/internal/libs/confix/testdata/v27-config.toml similarity index 100% rename from scripts/confix/testdata/v27-config.toml rename to internal/libs/confix/testdata/v27-config.toml diff --git a/scripts/confix/testdata/v28-config.toml b/internal/libs/confix/testdata/v28-config.toml similarity index 100% rename from scripts/confix/testdata/v28-config.toml rename to internal/libs/confix/testdata/v28-config.toml diff --git a/scripts/confix/testdata/v29-config.toml b/internal/libs/confix/testdata/v29-config.toml similarity index 100% rename from scripts/confix/testdata/v29-config.toml rename to internal/libs/confix/testdata/v29-config.toml diff --git a/scripts/confix/testdata/v30-config.toml b/internal/libs/confix/testdata/v30-config.toml similarity index 100% rename from scripts/confix/testdata/v30-config.toml rename to internal/libs/confix/testdata/v30-config.toml diff --git a/scripts/confix/testdata/v31-config.toml b/internal/libs/confix/testdata/v31-config.toml similarity index 100% rename from scripts/confix/testdata/v31-config.toml rename to internal/libs/confix/testdata/v31-config.toml diff --git a/scripts/confix/testdata/v32-config.toml b/internal/libs/confix/testdata/v32-config.toml similarity index 100% rename from scripts/confix/testdata/v32-config.toml rename to internal/libs/confix/testdata/v32-config.toml diff --git a/scripts/confix/testdata/v33-config.toml b/internal/libs/confix/testdata/v33-config.toml similarity index 100% rename from scripts/confix/testdata/v33-config.toml rename to internal/libs/confix/testdata/v33-config.toml diff --git a/scripts/confix/testdata/v34-config.toml b/internal/libs/confix/testdata/v34-config.toml similarity index 93% rename from scripts/confix/testdata/v34-config.toml rename to internal/libs/confix/testdata/v34-config.toml index 0ef8b25eb..f9d61f493 100644 --- a/scripts/confix/testdata/v34-config.toml +++ b/internal/libs/confix/testdata/v34-config.toml @@ -272,6 +272,11 @@ dial_timeout = "3s" ####################################################### [mempool] +# Mempool version to use: +# 1) "v0" - (default) FIFO mempool. +# 2) "v1" - prioritized mempool. +version = "v0" + recheck = true broadcast = true wal_dir = "" @@ -301,6 +306,22 @@ max_tx_bytes = 1048576 # XXX: Unused due to https://github.com/tendermint/tendermint/issues/5796 max_batch_bytes = 0 +# ttl-duration, if non-zero, defines the maximum amount of time a transaction +# can exist for in the mempool. +# +# Note, if ttl-num-blocks is also defined, a transaction will be removed if it +# has existed in the mempool at least ttl-num-blocks number of blocks or if it's +# insertion time into the mempool is beyond ttl-duration. +ttl-duration = "0s" + +# ttl-num-blocks, if non-zero, defines the maximum number of blocks a transaction +# can exist for in the mempool. +# +# Note, if ttl-duration is also defined, a transaction will be removed if it +# has existed in the mempool at least ttl-num-blocks number of blocks or if +# it's insertion time into the mempool is beyond ttl-duration. +ttl-num-blocks = 0 + ####################################################### ### State Sync Configuration Options ### ####################################################### @@ -403,8 +424,14 @@ peer_query_maj23_sleep_duration = "2s" # 1) "null" # 2) "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). # - When "kv" is chosen "tx.height" and "tx.hash" will always be indexed. +# 3) "psql" - the indexer services backed by PostgreSQL. +# When "kv" or "psql" is chosen "tx.height" and "tx.hash" will always be indexed. indexer = "kv" +# The PostgreSQL connection configuration, the connection format: +# postgresql://:@:/? +psql-conn = "" + ####################################################### ### Instrumentation Configuration Options ### ####################################################### diff --git a/scripts/confix/testdata/v35-config.toml b/internal/libs/confix/testdata/v35-config.toml similarity index 98% rename from scripts/confix/testdata/v35-config.toml rename to internal/libs/confix/testdata/v35-config.toml index 79616d6cd..a59161f07 100644 --- a/scripts/confix/testdata/v35-config.toml +++ b/internal/libs/confix/testdata/v35-config.toml @@ -227,7 +227,9 @@ pprof-laddr = "" # Enable the legacy p2p layer. use-legacy = false -# Select the p2p internal queue +# Select the p2p internal queue. +# Options are: "fifo", "simple-priority", "priority", and "wdrr" +# with the default being "priority". queue-type = "priority" # Address to listen for incoming connections @@ -281,6 +283,10 @@ max-num-outbound-peers = 10 # Maximum number of connections (inbound and outbound). max-connections = 64 +# Maximum number of connections reserved for outgoing +# connections. Must be less than max-connections +max-outgoing-connections = 12 + # Rate limits the number of incoming connection attempts per IP address. max-incoming-connection-attempts = 100 diff --git a/scripts/confix/testdata/v36-config.toml b/internal/libs/confix/testdata/v36-config.toml similarity index 98% rename from scripts/confix/testdata/v36-config.toml rename to internal/libs/confix/testdata/v36-config.toml index e49b97d89..d74c6a349 100644 --- a/scripts/confix/testdata/v36-config.toml +++ b/internal/libs/confix/testdata/v36-config.toml @@ -208,8 +208,10 @@ pprof-laddr = "" ####################################################### [p2p] -# Select the p2p internal queue -queue-type = "priority" +# Select the p2p internal queue. +# Options are: "fifo", "simple-priority", and "priority", +# with the default being "priority". +queue-type = "simple-priority" # Address to listen for incoming connections laddr = "tcp://0.0.0.0:26656" @@ -242,6 +244,10 @@ upnp = false # Maximum number of connections (inbound and outbound). max-connections = 64 +# Maximum number of connections reserved for outgoing +# connections. Must be less than max-connections +max-outgoing-connections = 12 + # Rate limits the number of incoming connection attempts per IP address. max-incoming-connection-attempts = 100 diff --git a/scripts/condiff/condiff.go b/scripts/condiff/condiff.go new file mode 100644 index 000000000..6b11e4e2c --- /dev/null +++ b/scripts/condiff/condiff.go @@ -0,0 +1,152 @@ +// Program condiff performs a keyspace diff on two TOML documents. +package main + +import ( + "context" + "flag" + "fmt" + "io" + "log" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/creachadair/tomledit" + "github.com/creachadair/tomledit/parser" + "github.com/creachadair/tomledit/transform" +) + +var ( + doDesnake = flag.Bool("desnake", false, "Convert snake_case to kebab-case before comparing") +) + +func init() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, `Usage: %[1]s [options] f1 f2 + +Diff the keyspaces of the TOML documents in files f1 and f2. +The output prints one line per key that differs: + + -S name -- section exists in f1 but not f2 + +S name -- section exists in f2 but not f1 + -M name -- mapping exists in f1 but not f2 + +M name -- mapping exists in f2 but not f1 + +Comments, order, and values are ignored for comparison purposes. + +Options: +`, filepath.Base(os.Args[0])) + flag.PrintDefaults() + } +} + +func main() { + flag.Parse() + + if flag.NArg() != 2 { + log.Fatalf("Usage: %[1]s ", filepath.Base(os.Args[0])) + } + lhs := mustParse(flag.Arg(0)) + rhs := mustParse(flag.Arg(1)) + if *doDesnake { + log.Printf("Converting all names from snake_case to kebab-case") + fix := transform.SnakeToKebab() + _ = fix(context.Background(), lhs) + _ = fix(context.Background(), rhs) + } + diffDocs(os.Stdout, lhs, rhs) +} + +func mustParse(path string) *tomledit.Document { + f, err := os.Open(path) + if err != nil { + log.Fatalf("Opening TOML input: %v", err) + } + defer f.Close() + doc, err := tomledit.Parse(f) + if err != nil { + log.Fatalf("Parsing %q: %v", path, err) + } + return doc +} + +func allKeys(s *tomledit.Section) []string { + var keys []string + s.Scan(func(key parser.Key, _ *tomledit.Entry) bool { + keys = append(keys, key.String()) + return true + }) + return keys +} + +const ( + delSection = "-S" + delMapping = "-M" + addSection = "+S" + addMapping = "+M" + + delMapSep = "\n" + delMapping + " " + addMapSep = "\n" + addMapping + " " +) + +func diffDocs(w io.Writer, lhs, rhs *tomledit.Document) { + diffSections(w, lhs.Global, rhs.Global) + lsec, rsec := lhs.Sections, rhs.Sections + transform.SortSectionsByName(lsec) + transform.SortSectionsByName(rsec) + + i, j := 0, 0 + for i < len(lsec) && j < len(rsec) { + if lsec[i].Name.Before(rsec[j].Name) { + fmt.Fprintln(w, delSection, lsec[i].Name) + fmt.Fprintln(w, delMapping, strings.Join(allKeys(lsec[i]), delMapSep)) + i++ + } else if rsec[j].Name.Before(lsec[i].Name) { + fmt.Fprintln(w, addSection, rsec[j].Name) + fmt.Fprintln(w, addMapping, strings.Join(allKeys(rsec[j]), addMapSep)) + j++ + } else { + diffSections(w, lsec[i], rsec[j]) + i++ + j++ + } + } + for ; i < len(lsec); i++ { + fmt.Fprintln(w, delSection, lsec[i].Name) + fmt.Fprintln(w, delMapping, strings.Join(allKeys(lsec[i]), delMapSep)) + } + for ; j < len(rsec); j++ { + fmt.Fprintln(w, addSection, rsec[j].Name) + fmt.Fprintln(w, addMapping, strings.Join(allKeys(rsec[j]), addMapSep)) + } +} + +func diffSections(w io.Writer, lhs, rhs *tomledit.Section) { + diffKeys(w, allKeys(lhs), allKeys(rhs)) +} + +func diffKeys(w io.Writer, lhs, rhs []string) { + sort.Strings(lhs) + sort.Strings(rhs) + + i, j := 0, 0 + for i < len(lhs) && j < len(rhs) { + if lhs[i] < rhs[j] { + fmt.Fprintln(w, delMapping, lhs[i]) + i++ + } else if lhs[i] > rhs[j] { + fmt.Fprintln(w, addMapping, rhs[j]) + j++ + } else { + i++ + j++ + } + } + for ; i < len(lhs); i++ { + fmt.Fprintln(w, delMapping, lhs[i]) + } + for ; j < len(rhs); j++ { + fmt.Fprintln(w, addMapping, rhs[j]) + } +} diff --git a/scripts/confix/confix.go b/scripts/confix/confix.go index c19d09957..29c5bb753 100644 --- a/scripts/confix/confix.go +++ b/scripts/confix/confix.go @@ -1,23 +1,17 @@ -// Program confix applies fixes to a Tendermint TOML configuration file to -// update a file created with an older version of Tendermint to a compatible -// format for a newer version. +// Program confix applies changes to a Tendermint TOML configuration file, to +// update configurations created with an older version of Tendermint to a +// compatible format for a newer version. package main import ( - "bytes" "context" - "errors" "flag" "fmt" "log" "os" "path/filepath" - "github.com/creachadair/atomicfile" - "github.com/creachadair/tomledit" - "github.com/creachadair/tomledit/transform" - "github.com/spf13/viper" - "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/internal/libs/confix" ) func init() { @@ -41,6 +35,7 @@ Options: var ( configPath = flag.String("config", "", "Config file path (required)") outPath = flag.String("out", "", "Output file path (default stdout)") + doVerbose = flag.Bool("v", false, "Log changes to stderr") ) func main() { @@ -49,118 +44,11 @@ func main() { log.Fatal("You must specify a non-empty -config path") } - doc, err := LoadConfig(*configPath) - if err != nil { - log.Fatalf("Loading config: %v", err) + ctx := context.Background() + if *doVerbose { + ctx = confix.WithLogWriter(ctx, os.Stderr) } - - ctx := transform.WithLogWriter(context.Background(), os.Stderr) - if err := ApplyFixes(ctx, doc); err != nil { - log.Fatalf("Updating %q: %v", *configPath, err) - } - - var buf bytes.Buffer - if err := tomledit.Format(&buf, doc); err != nil { - log.Fatalf("Formatting config: %v", err) - } - - // Verify that Tendermint can parse the results after our edits. - if err := CheckValid(buf.Bytes()); err != nil { - log.Fatalf("Updated config is invalid: %v", err) - } - - if *outPath == "" { - os.Stdout.Write(buf.Bytes()) - } else if err := atomicfile.WriteData(*outPath, buf.Bytes(), 0600); err != nil { - log.Fatalf("Writing output: %v", err) + if err := confix.Upgrade(ctx, *configPath, *outPath); err != nil { + log.Fatalf("Upgrading config: %v", err) } } - -// ApplyFixes transforms doc and reports whether it succeeded. -func ApplyFixes(ctx context.Context, doc *tomledit.Document) error { - // Check what version of Tendermint might have created this config file, as - // a safety check for the updates we are about to make. - tmVersion := GuessConfigVersion(doc) - if tmVersion == vUnknown { - return errors.New("cannot tell what Tendermint version created this config") - } else if tmVersion < v34 || tmVersion > v35 { - // TODO(creachadair): Add in rewrites for older versions. This will - // require some digging to discover what the changes were. The upgrade - // instructions do not give specifics. - return fmt.Errorf("unable to update version %s config", tmVersion) - } - return plan.Apply(ctx, doc) -} - -// LoadConfig loads and parses the TOML document from path. -func LoadConfig(path string) (*tomledit.Document, error) { - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - return tomledit.Parse(f) -} - -const ( - vUnknown = "" - v32 = "v0.32" - v33 = "v0.33" - v34 = "v0.34" - v35 = "v0.35" - v36 = "v0.36" -) - -// GuessConfigVersion attempts to figure out which version of Tendermint -// created the specified config document. It returns "" if the creating version -// cannot be determined, otherwise a string of the form "vX.YY". -func GuessConfigVersion(doc *tomledit.Document) string { - hasDisableWS := doc.First("rpc", "experimental-disable-websocket") != nil - hasUseLegacy := doc.First("p2p", "use-legacy") != nil // v0.35 only - if hasDisableWS && !hasUseLegacy { - return v36 - } - - hasBlockSync := transform.FindTable(doc, "blocksync") != nil // add: v0.35 - hasStateSync := transform.FindTable(doc, "statesync") != nil // add: v0.34 - if hasBlockSync && hasStateSync { - return v35 - } else if hasStateSync { - return v34 - } - - hasIndexKeys := doc.First("tx_index", "index_keys") != nil // add: v0.33 - hasIndexTags := doc.First("tx_index", "index_tags") != nil // rem: v0.33 - if hasIndexKeys && !hasIndexTags { - return v33 - } - - hasFastSync := transform.FindTable(doc, "fastsync") != nil // add: v0.32 - if hasIndexTags && hasFastSync { - return v32 - } - - // Something older, probably. - return vUnknown -} - -// CheckValid checks whether the specified config appears to be a valid -// Tendermint config file. This emulates how the node loads the config. -func CheckValid(data []byte) error { - v := viper.New() - v.SetConfigType("toml") - - if err := v.ReadConfig(bytes.NewReader(data)); err != nil { - return fmt.Errorf("reading config: %w", err) - } - - var cfg config.Config - if err := v.Unmarshal(&cfg); err != nil { - return fmt.Errorf("decoding config: %w", err) - } - - // Stub in required value not stored in the config file, so that validation - // will not fail spuriously. - cfg.Mode = config.ModeValidator - return cfg.ValidateBasic() -} From 5edc9e3a152a1e7dc89c72d4d353bca1941df729 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Jul 2022 09:33:52 -0700 Subject: [PATCH 45/58] build(deps): Bump pgregory.net/rapid from 0.4.7 to 0.4.8 (#9015) Bumps [pgregory.net/rapid](https://github.com/flyingmutant/rapid) from 0.4.7 to 0.4.8. - [Release notes](https://github.com/flyingmutant/rapid/releases) - [Commits](https://github.com/flyingmutant/rapid/compare/v0.4.7...v0.4.8) --- updated-dependencies: - dependency-name: pgregory.net/rapid dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sam Kleinman Co-authored-by: M. J. Fromberger --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 169099001..8a33d0239 100644 --- a/go.mod +++ b/go.mod @@ -51,5 +51,5 @@ require ( google.golang.org/grpc v1.48.0 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gotest.tools v2.2.0+incompatible // indirect - pgregory.net/rapid v0.4.7 + pgregory.net/rapid v0.4.8 ) diff --git a/go.sum b/go.sum index 7f23153ad..7b686f536 100644 --- a/go.sum +++ b/go.sum @@ -1861,8 +1861,8 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphD mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 h1:Jh3LAeMt1eGpxomyu3jVkmVZWW2MxZ1qIIV2TZ/nRio= mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5/go.mod h1:b8RRCBm0eeiWR8cfN88xeq2G5SG3VKGO+5UPWi5FSOY= -pgregory.net/rapid v0.4.7 h1:MTNRktPuv5FNqOO151TM9mDTa+XHcX6ypYeISDVD14g= -pgregory.net/rapid v0.4.7/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU= +pgregory.net/rapid v0.4.8 h1:d+5SGZWUbJPbl3ss6tmPFqnNeQR6VDOFly+eTjwPiEw= +pgregory.net/rapid v0.4.8/go.mod h1:Z5PbWqjvWR1I3UGjvboUuan4fe4ZYEYNLNQLExzCoUs= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= From 32761ec729fa1a82849424a7eb66fdf01be3c02a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Jul 2022 08:17:34 -0700 Subject: [PATCH 46/58] build(deps): Bump github.com/golangci/golangci-lint (#9037) Bumps [github.com/golangci/golangci-lint](https://github.com/golangci/golangci-lint) from 1.46.0 to 1.47.0. - [Release notes](https://github.com/golangci/golangci-lint/releases) - [Changelog](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md) - [Commits](https://github.com/golangci/golangci-lint/compare/v1.46.0...v1.47.0) --- updated-dependencies: - dependency-name: github.com/golangci/golangci-lint dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 124 +++++++++++++++++++++++++++++---------------------------- 2 files changed, 65 insertions(+), 61 deletions(-) diff --git a/go.mod b/go.mod index 8a33d0239..0579f11eb 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/go-kit/kit v0.12.0 github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.2 - github.com/golangci/golangci-lint v1.46.0 + github.com/golangci/golangci-lint v1.47.0 github.com/google/go-cmp v0.5.8 github.com/google/orderedcode v0.0.1 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index 7b686f536..98f054355 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Antonboom/errname v0.1.6 h1:LzIJZlyLOCSu51o3/t2n9Ck7PcoP9wdbrdaW6J8fX24= -github.com/Antonboom/errname v0.1.6/go.mod h1:7lz79JAnuoMNDAWE9MeeIr1/c/VpSUWatBv2FH9NYpI= +github.com/Antonboom/errname v0.1.7 h1:mBBDKvEYwPl4WFFNwec1CZO096G6vzK9vvDQzAwkako= +github.com/Antonboom/errname v0.1.7/go.mod h1:g0ONh16msHIPgJSGsecu1G/dcF2hlYR/0SddnIAGavU= github.com/Antonboom/nilnil v0.1.1 h1:PHhrh5ANKFWRBh7TdYmyyq2gyT2lotnvFvvFbylF81Q= github.com/Antonboom/nilnil v0.1.1/go.mod h1:L1jBqoWM7AOeTD+tSquifKSesRHs4ZdaxvZR+xdJEaI= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= @@ -84,8 +84,8 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/GaijinEntertainment/go-exhaustruct/v2 v2.1.0 h1:LAPPhJ4KR5Z8aKVZF5S48csJkxL5RMKmE/98fMs1u5M= -github.com/GaijinEntertainment/go-exhaustruct/v2 v2.1.0/go.mod h1:LGOGuvEgCfCQsy3JF2tRmpGDpzA53iZfyGEWSPwQ6/4= +github.com/GaijinEntertainment/go-exhaustruct/v2 v2.2.0 h1:V9xVvhKbLt7unNEGAruK1xXglyc668Pq3Xx0MNTNqpo= +github.com/GaijinEntertainment/go-exhaustruct/v2 v2.2.0/go.mod h1:n/vLeA7V+QY84iYAGwMkkUUp9ooeuftMEvaDrSVch+Q= github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= @@ -121,6 +121,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alingse/asasalint v0.0.10 h1:qqGPDTV0ff0tWHN/nnIlSdjlU/EwRPaUY4SfpE1rnms= +github.com/alingse/asasalint v0.0.10/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= @@ -144,6 +146,7 @@ github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -244,8 +247,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/daixiang0/gci v0.3.3 h1:55xJKH7Gl9Vk6oQ1cMkwrDWjAkT1D+D1G9kNmRcAIY4= -github.com/daixiang0/gci v0.3.3/go.mod h1:1Xr2bxnQbDxCqqulUOv8qpGqkgRw9RSCGGjEC2LjF8o= +github.com/daixiang0/gci v0.4.2 h1:PyT/Y4a265wDhPCZo2ip/YH33M4zEuFA3nDMdAvcKSA= +github.com/daixiang0/gci v0.4.2/go.mod h1:d0f+IJhr9loBtIq+ebwhRoTt1LGbPH96ih8bKlsRT9E= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -312,8 +315,8 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/firefart/nonamedreturns v1.0.1 h1:fSvcq6ZpK/uBAgJEGMvzErlzyM4NELLqqdTofVjVNag= -github.com/firefart/nonamedreturns v1.0.1/go.mod h1:D3dpIBojGGNh5UfElmwPu73SwDCm+VKhHYqwlNOk2uQ= +github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= +github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= @@ -329,8 +332,8 @@ github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5 github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= -github.com/fzipp/gocyclo v0.5.1 h1:L66amyuYogbxl0j2U+vGqJXusPF2IkduvXLnYD5TFgw= -github.com/fzipp/gocyclo v0.5.1/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-critic/go-critic v0.6.3 h1:abibh5XYBTASawfTQ0rA7dVtQT+6KzpGqb/J+DxRDaw= github.com/go-critic/go-critic v0.6.3/go.mod h1:c6b3ZP1MQ7o6lPR7Rv3lEf7pYQUmAcx8ABHgdZCQt/k= @@ -439,8 +442,8 @@ github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6 github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks= github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.46.0 h1:uz9AtEcIP63FH+FIyuAXcQGVQO4vCUavEsMTJpPeD4s= -github.com/golangci/golangci-lint v1.46.0/go.mod h1:IJpcNOUfx/XLRwE95FHQ6QtbhYwwqcm0H5QkwUfF4ZE= +github.com/golangci/golangci-lint v1.47.0 h1:h2s+ZGGF63fdzUtac+VYUHPsEO0ADTqHouI7Vase+FY= +github.com/golangci/golangci-lint v1.47.0/go.mod h1:3TZhfF5KolbIkXYjUFvER6G9CoxzLEaafr/u/QI1S5A= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= @@ -513,7 +516,7 @@ github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/Oth github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 h1:PVRE9d4AQKmbelZ7emNig1+NT27DUmKZn5qXxfio54U= @@ -582,8 +585,8 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= -github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= @@ -653,8 +656,8 @@ github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/errcheck v1.6.0 h1:YTDO4pNy7AUN/021p+JGHycQyYNIyMoenM1YDVK6RlY= -github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/errcheck v1.6.1 h1:cErYo+J4SmEjdXZrVXGwLJCE2sB06s23LpkcyWNrT+s= +github.com/kisielk/errcheck v1.6.1/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= @@ -675,10 +678,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kulti/thelper v0.6.2 h1:K4xulKkwOCnT1CDms6Ex3uG1dvSMUUQe9zxgYQgbRXs= -github.com/kulti/thelper v0.6.2/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= -github.com/kunwardeep/paralleltest v1.0.3 h1:UdKIkImEAXjR1chUWLn+PNXqWUGs//7tzMeWuP7NhmI= -github.com/kunwardeep/paralleltest v1.0.3/go.mod h1:vLydzomDFpk7yu5UX02RmP0H8QfRPOV/oFhWN85Mjb4= +github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= +github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= +github.com/kunwardeep/paralleltest v1.0.6 h1:FCKYMF1OF2+RveWlABsdnmsvJrei5aoyZoaGS+Ugg8g= +github.com/kunwardeep/paralleltest v1.0.6/go.mod h1:Y0Y0XISdZM5IKm3TREQMZ6iteqn1YuwCsJO/0kL9Zes= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyoh86/exportloopref v0.1.8 h1:5Ry/at+eFdkX9Vsdw3qU4YkvGtzuVfzT4X7S77LoN/M= github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= @@ -698,16 +701,16 @@ github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= -github.com/lufeee/execinquery v1.0.0 h1:1XUTuLIVPDlFvUU3LXmmZwHDsolsxXnY67lzhpeqe0I= -github.com/lufeee/execinquery v1.0.0/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= +github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= +github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/maratori/testpackage v1.0.1 h1:QtJ5ZjqapShm0w5DosRjg0PRlSdAdlx+W6cCKoALdbQ= -github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= +github.com/maratori/testpackage v1.1.0 h1:GJY4wlzQhuBusMF1oahQCBtUV/AQ/k69IZ68vxaac2Q= +github.com/maratori/testpackage v1.1.0/go.mod h1:PeAhzU8qkCwdGEMTEupsHJNlQu2gZopMC6RjbhmHeDc= github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 h1:pWxk9e//NbPwfxat7RXkts09K+dEBJWakUWwICVqYbA= github.com/matoous/godox v0.0.0-20210227103229-6504466cf951/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= @@ -802,8 +805,8 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6Fx github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nishanths/exhaustive v0.7.11 h1:xV/WU3Vdwh5BUH4N06JNUznb6d5zhRPOnlgCrpNYNKA= -github.com/nishanths/exhaustive v0.7.11/go.mod h1:gX+MP7DWMKJmNa1HfMozK+u04hQd3na9i0hyqf3/dOI= +github.com/nishanths/exhaustive v0.8.1 h1:0QKNascWv9qIHY7zRoZSxeRr6kuk5aAT3YXLTiDmjTo= +github.com/nishanths/exhaustive v0.8.1/go.mod h1:qj+zJJUgJ76tR92+25+03oYUhzF4R7/2Wk7fGTfCHmg= github.com/nishanths/predeclared v0.0.0-20190419143655-18a43bb90ffc/go.mod h1:62PewwiQTlm/7Rj+cxVYqZvDIUc+JjZq6GHAC1fsObQ= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= @@ -825,17 +828,17 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -865,8 +868,6 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.0.0/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= @@ -890,8 +891,8 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/polyfloyd/go-errorlint v0.0.0-20211125173453-6d6d39c5bb8b h1:/BDyEJWLnDUYKGWdlNx/82qSaVu2bUok/EvPUtIGuvw= -github.com/polyfloyd/go-errorlint v0.0.0-20211125173453-6d6d39c5bb8b/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= +github.com/polyfloyd/go-errorlint v1.0.0 h1:pDrQG0lrh68e602Wfp68BlUTRFoHn8PZYAjLgt2LFsM= +github.com/polyfloyd/go-errorlint v1.0.0/go.mod h1:KZy4xxPJyy88/gldCe5OdW6OQRtNO3EZE7hXzmnebgA= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= @@ -930,7 +931,7 @@ github.com/quasilyte/go-ruleguard v0.3.16-0.20220213074421-6aa060fab41a h1:sWFav github.com/quasilyte/go-ruleguard v0.3.16-0.20220213074421-6aa060fab41a/go.mod h1:VMX+OnnSw4LicdiEGtRSD/1X8kW7GuEscjYNr4cOIT4= github.com/quasilyte/go-ruleguard/dsl v0.3.0/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/go-ruleguard/dsl v0.3.16/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/go-ruleguard/dsl v0.3.19/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/go-ruleguard/dsl v0.3.21/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc= github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50= github.com/quasilyte/gogrep v0.0.0-20220120141003-628d8b3623b5 h1:PDWGei+Rf2bBiuZIbZmM20J2ftEy9IeUCHA8HbQqed8= @@ -966,7 +967,6 @@ github.com/ryanrolds/sqlclosecheck v0.3.0 h1:AZx+Bixh8zdUBxUA1NxbxVAS78vTPq4rCb8 github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= -github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/sanposhiho/wastedassign/v2 v2.0.6 h1:+6/hQIHKNJAUixEj6EmOngGIisyeI+T3335lYTyxRoA= github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= @@ -975,12 +975,12 @@ github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa/go.mod h1:F7 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/securego/gosec/v2 v2.11.0 h1:+PDkpzR41OI2jrw1q6AdXZCbsNGNGT7pQjal0H0cArI= -github.com/securego/gosec/v2 v2.11.0/go.mod h1:SX8bptShuG8reGC0XS09+a4H2BoWSJi+fscA+Pulbpo= +github.com/securego/gosec/v2 v2.12.0 h1:CQWdW7ATFpvLSohMVsajscfyHJ5rsGmEXmsNcsDNmAg= +github.com/securego/gosec/v2 v2.12.0/go.mod h1:iTpT+eKTw59bSgklBHlSnH5O2tNygHMDxfvMubA4i7I= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= -github.com/shirou/gopsutil/v3 v3.22.4/go.mod h1:D01hZJ4pVHPpCTZ3m3T2+wDF2YAGfd+H4ifUguaQzHM= +github.com/shirou/gopsutil/v3 v3.22.6/go.mod h1:EdIubSnZhbAvBS1yJ7Xi+AShB/hxwLHOMz4MCYz7yMs= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -992,8 +992,10 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sivchari/containedctx v1.0.2 h1:0hLQKpgC53OVF1VT7CeoFHk9YKstur1XOgfYIc1yrHI= github.com/sivchari/containedctx v1.0.2/go.mod h1:PwZOeqm4/DLoJOqMSIJs3aKqXRX4YO+uXww087KZ7Bw= -github.com/sivchari/tenv v1.5.0 h1:wxW0mFpKI6DIb3s6m1jCDYvkWXCskrimXMuGd0K/kSQ= -github.com/sivchari/tenv v1.5.0/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= +github.com/sivchari/nosnakecase v1.5.0 h1:ZBvAu1H3uteN0KQ0IsLpIFOwYgPEhKLyv2ahrVkub6M= +github.com/sivchari/nosnakecase v1.5.0/go.mod h1:CwDzrzPea40/GB6uynrNLiorAlgFRvRbFSgJx2Gs+QY= +github.com/sivchari/tenv v1.6.0 h1:FyE4WysxLwYljKqWhTfOMjgKjBSnmzzg7lWOmpDiAcc= +github.com/sivchari/tenv v1.6.0/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa h1:YJfZp12Z3AFhSBeXOlv4BO55RMwPn2NoQeDsrdWnBtY= @@ -1031,7 +1033,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= -github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= @@ -1055,6 +1056,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -1081,13 +1083,15 @@ github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaE github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 h1:kl4KhGNsJIbDHS9/4U9yQo1UcPQM0kOMJHn29EoH/Ro= github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= +github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= +github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tomarrell/wrapcheck/v2 v2.6.1 h1:Cf4a/iwuMp9s7kKrh74GTgijRVim0wEpKjgAsT7Wctw= -github.com/tomarrell/wrapcheck/v2 v2.6.1/go.mod h1:Eo+Opt6pyMW1b6cNllOcDSSoHO0aTJ+iF6BfCUbHltA= +github.com/tomarrell/wrapcheck/v2 v2.6.2 h1:3dI6YNcrJTQ/CJQ6M/DUkc0gnqYSIk6o0rChn9E/D0M= +github.com/tomarrell/wrapcheck/v2 v2.6.2/go.mod h1:ao7l5p0aOlUNJKI0qVwB4Yjlqutd0IvAB9Rdwyilxvg= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/tommy-muehle/go-mnd/v2 v2.5.0 h1:iAj0a8e6+dXSL7Liq0aXPox36FiN1dBbjA6lt9fl65s= github.com/tommy-muehle/go-mnd/v2 v2.5.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= @@ -1100,8 +1104,8 @@ github.com/ultraware/whitespace v0.0.5 h1:hh+/cpIcopyMYbZNVov9iSxvJU3OYQg78Sfaqz github.com/ultraware/whitespace v0.0.5/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/uudashr/gocognit v1.0.5 h1:rrSex7oHr3/pPLQ0xoWq108XMU8s678FJcQ+aSfOHa4= -github.com/uudashr/gocognit v1.0.5/go.mod h1:wgYz0mitoKOTysqxTDMOUXg+Jb5SvtihkfmugIZYpEA= +github.com/uudashr/gocognit v1.0.6 h1:2Cgi6MweCsdB6kpcVQp7EW4U23iBFQWfTXiWlyp842Y= +github.com/uudashr/gocognit v1.0.6/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8= @@ -1136,8 +1140,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -gitlab.com/bosi/decorder v0.2.1 h1:ehqZe8hI4w7O4b1vgsDZw1YU1PE7iJXrQWFMsocbQ1w= -gitlab.com/bosi/decorder v0.2.1/go.mod h1:6C/nhLSbF6qZbYD8bRmISBwc6vcWdNsiIBkRvjJFrH0= +gitlab.com/bosi/decorder v0.2.2 h1:LRfb3lP6mZWjUzpMOCLTVjcnl/SqZWBWmKNqQvMocQs= +gitlab.com/bosi/decorder v0.2.2/go.mod h1:9K1RB5+VPNQYtXtTDAzd2OEftsZb1oV0IrJrzChSdGE= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= @@ -1145,15 +1149,12 @@ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= -go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= @@ -1170,17 +1171,21 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1207,7 +1212,6 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1467,16 +1471,16 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220403020550-483a9cbc67c0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220702020025-31831981b65f h1:xdsejrW/0Wf2diT5CPp3XmKUNbr7Xvw8kYilQ+6qjRY= +golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1554,7 +1558,6 @@ golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWc golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -1601,7 +1604,7 @@ golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.9-0.20211228192929-ee1ca4ffc4da/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.11-0.20220316014157-77aa08bb151a/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1836,6 +1839,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -1851,8 +1855,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.3.1 h1:1kJlrWJLkaGXgcaeosRXViwviqjI7nkBvU2+sZW0AYc= -honnef.co/go/tools v0.3.1/go.mod h1:vlRD9XErLMGT+mDuofSr0mMMquscM/1nQqtRSsh6m70= +honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34= +honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= mvdan.cc/gofumpt v0.3.1 h1:avhhrOmv0IuvQVK7fvwV91oFSGAk5/6Po8GXTzICeu8= mvdan.cc/gofumpt v0.3.1/go.mod h1:w3ymliuxvzVx8DAutBnVyDqYb1Niy/yCJt/lk821YCE= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= From 22ed610083cb8275a954406296832149c4cc1dcd Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Tue, 19 Jul 2022 13:28:46 -0700 Subject: [PATCH 47/58] mempool: rework lock discipline to mitigate callback deadlocks (#9030) The priority mempool has a stricter synchronization requirement than the legacy mempool. Under sufficiently-heavy load, exclusive access can lead to deadlocks when processing a large batch of transaction rechecks through an out-of-process application using the socket client. By design, a socket client stalls when its send buffer fills, during which time it holds a lock shared with the receive thread. While blocked in this state, a response read by the receive thread waits for the shared lock so the callback can be invoked. If we're lucky, the server will then read the next request and make enough room in the buffer for the sender to proceed. If not however (e.g., if the next request is bigger than the one just consumed), the receive thread is blocked: It is waiting on the lock and cannot read a response. Once the server's output buffer fills, the system deadlocks. This can happen with any sufficiently-busy workload, but is more likely during a large recheck in the v1 mempool, where the callbacks need exclusive access to mempool state. As a workaround, process rechecks for the priority mempool in their own goroutines outside the mempool mutex. Responses still head-of-line block, but will no longer get pushback due to contention on the mempool itself. --- internal/mempool/v1/mempool.go | 181 +++++++++++++-------------------- 1 file changed, 72 insertions(+), 109 deletions(-) diff --git a/internal/mempool/v1/mempool.go b/internal/mempool/v1/mempool.go index 622ee2c86..9bb5f2b0a 100644 --- a/internal/mempool/v1/mempool.go +++ b/internal/mempool/v1/mempool.go @@ -3,12 +3,13 @@ package v1 import ( "context" "fmt" - "reflect" + "runtime" "sort" "sync" "sync/atomic" "time" + "github.com/creachadair/taskgroup" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/internal/libs/clist" @@ -41,8 +42,7 @@ type TxMempool struct { cache mempool.TxCache // seen transactions // Atomically-updated fields - txsBytes int64 // atomic: the total size of all transactions in the mempool, in bytes - txRecheck int64 // atomic: the number of pending recheck calls + txsBytes int64 // atomic: the total size of all transactions in the mempool, in bytes // Synchronized fields, protected by mtx. mtx *sync.RWMutex @@ -83,8 +83,6 @@ func NewTxMempool( txmp.cache = mempool.NewLRUTxCache(cfg.CacheSize) } - proxyAppConn.SetResponseCallback(txmp.recheckTxCallback) - for _, opt := range options { opt(txmp) } @@ -182,7 +180,6 @@ func (txmp *TxMempool) CheckTx( cb func(*abci.Response), txInfo mempool.TxInfo, ) error { - // During the initial phase of CheckTx, we do not need to modify any state. // A transaction will not actually be added to the mempool until it survives // a call to the ABCI CheckTx method and size constraint checks. @@ -224,31 +221,23 @@ func (txmp *TxMempool) CheckTx( return err } - // Initiate an ABCI CheckTx for this transaction. The callback is - // responsible for adding the transaction to the pool if it survives. - // - // N.B.: We have to issue the call outside the lock. In a local client, - // even an "async" call invokes its callback immediately which will make - // the callback deadlock trying to acquire the same lock. This isn't a - // problem with out-of-process calls, but this has to work for both. - reqRes, err := txmp.proxyAppConn.CheckTxAsync(ctx, abci.RequestCheckTx{Tx: tx}) + // Invoke an ABCI CheckTx for this transaction. + rsp, err := txmp.proxyAppConn.CheckTxSync(ctx, abci.RequestCheckTx{Tx: tx}) if err != nil { txmp.cache.Remove(tx) return err } - reqRes.SetCallback(func(res *abci.Response) { - wtx := &WrappedTx{ - tx: tx, - hash: tx.Key(), - timestamp: time.Now().UTC(), - height: height, - } - wtx.SetPeer(txInfo.SenderID) - txmp.initialTxCallback(wtx, res) - if cb != nil { - cb(res) - } - }) + wtx := &WrappedTx{ + tx: tx, + hash: tx.Key(), + timestamp: time.Now().UTC(), + height: height, + } + wtx.SetPeer(txInfo.SenderID) + txmp.addNewTransaction(wtx, rsp) + if cb != nil { + cb(&abci.Response{Value: &abci.Response_CheckTx{CheckTx: rsp}}) + } return nil } @@ -304,10 +293,6 @@ func (txmp *TxMempool) Flush() { cur = next } txmp.cache.Reset() - - // Discard any pending recheck calls that may be in flight. The calls will - // still complete, but will have no effect on the mempool. - atomic.StoreInt64(&txmp.txRecheck, 0) } // allEntriesSorted returns a slice of all the transactions currently in the @@ -403,12 +388,6 @@ func (txmp *TxMempool) Update( newPreFn mempool.PreCheckFunc, newPostFn mempool.PostCheckFunc, ) error { - // TODO(creachadair): This would be a nice safety check but requires Go 1.18. - // // Safety check: The caller is required to hold the lock. - // if txmp.mtx.TryLock() { - // txmp.mtx.Unlock() - // panic("mempool: Update caller does not hold the lock") - // } // Safety check: Transactions and responses must match in number. if len(blockTxs) != len(deliverTxResponses) { panic(fmt.Sprintf("mempool: got %d transactions but %d DeliverTx responses", @@ -456,9 +435,9 @@ func (txmp *TxMempool) Update( return nil } -// initialTxCallback handles the ABCI CheckTx response for the first time a +// addNewTransaction handles the ABCI CheckTx response for the first time a // transaction is added to the mempool. A recheck after a block is committed -// goes to the default callback (see recheckTxCallback). +// goes to handleRecheckResult. // // If either the application rejected the transaction or a post-check hook is // defined and rejects the transaction, it is discarded. @@ -469,31 +448,22 @@ func (txmp *TxMempool) Update( // transactions are evicted. // // Finally, the new transaction is added and size stats updated. -func (txmp *TxMempool) initialTxCallback(wtx *WrappedTx, res *abci.Response) { - checkTxRes, ok := res.Value.(*abci.Response_CheckTx) - if !ok { - txmp.logger.Error("mempool: received incorrect result type in CheckTx callback", - "expected", reflect.TypeOf(&abci.Response_CheckTx{}).Name(), - "got", reflect.TypeOf(res.Value).Name(), - ) - return - } - +func (txmp *TxMempool) addNewTransaction(wtx *WrappedTx, checkTxRes *abci.ResponseCheckTx) { txmp.mtx.Lock() defer txmp.mtx.Unlock() var err error if txmp.postCheck != nil { - err = txmp.postCheck(wtx.tx, checkTxRes.CheckTx) + err = txmp.postCheck(wtx.tx, checkTxRes) } - if err != nil || checkTxRes.CheckTx.Code != abci.CodeTypeOK { + if err != nil || checkTxRes.Code != abci.CodeTypeOK { txmp.logger.Info( "rejected bad transaction", "priority", wtx.Priority(), "tx", fmt.Sprintf("%X", wtx.tx.Hash()), "peer_id", wtx.peers, - "code", checkTxRes.CheckTx.Code, + "code", checkTxRes.Code, "post_check_err", err, ) @@ -508,13 +478,13 @@ func (txmp *TxMempool) initialTxCallback(wtx *WrappedTx, res *abci.Response) { // If there was a post-check error, record its text in the result for // debugging purposes. if err != nil { - checkTxRes.CheckTx.MempoolError = err.Error() + checkTxRes.MempoolError = err.Error() } return } - priority := checkTxRes.CheckTx.Priority - sender := checkTxRes.CheckTx.Sender + priority := checkTxRes.Priority + sender := checkTxRes.Sender // Disallow multiple concurrent transactions from the same sender assigned // by the ABCI application. As a special case, an empty sender is not @@ -528,7 +498,7 @@ func (txmp *TxMempool) initialTxCallback(wtx *WrappedTx, res *abci.Response) { "tx", fmt.Sprintf("%X", w.tx.Hash()), "sender", sender, ) - checkTxRes.CheckTx.MempoolError = + checkTxRes.MempoolError = fmt.Sprintf("rejected valid incoming transaction; tx already exists for sender %q (%X)", sender, w.tx.Hash()) txmp.metrics.RejectedTxs.Add(1) @@ -563,7 +533,7 @@ func (txmp *TxMempool) initialTxCallback(wtx *WrappedTx, res *abci.Response) { "tx", fmt.Sprintf("%X", wtx.tx.Hash()), "err", err.Error(), ) - checkTxRes.CheckTx.MempoolError = + checkTxRes.MempoolError = fmt.Sprintf("rejected valid incoming transaction; mempool is full (%X)", wtx.tx.Hash()) txmp.metrics.RejectedTxs.Add(1) @@ -609,7 +579,7 @@ func (txmp *TxMempool) initialTxCallback(wtx *WrappedTx, res *abci.Response) { } } - wtx.SetGasWanted(checkTxRes.CheckTx.GasWanted) + wtx.SetGasWanted(checkTxRes.GasWanted) wtx.SetPriority(priority) wtx.SetSender(sender) txmp.insertTx(wtx) @@ -636,33 +606,14 @@ func (txmp *TxMempool) insertTx(wtx *WrappedTx) { atomic.AddInt64(&txmp.txsBytes, wtx.Size()) } -// recheckTxCallback handles the responses from ABCI CheckTx calls issued -// during the recheck phase of a block Update. It updates the recheck counter -// and removes any transactions invalidated by the application. +// handleRecheckResult handles the responses from ABCI CheckTx calls issued +// during the recheck phase of a block Update. It removes any transactions +// invalidated by the application. // -// This callback is NOT executed for the initial CheckTx on a new transaction; -// that case is handled by initialTxCallback instead. -func (txmp *TxMempool) recheckTxCallback(req *abci.Request, res *abci.Response) { - checkTxRes, ok := res.Value.(*abci.Response_CheckTx) - if !ok { - // Don't log this; this is the default callback and other response types - // can safely be ignored. - return - } - - // Check whether we are expecting recheck responses at this point. - // If not, we will ignore the response, this usually means the mempool was Flushed. - // If this is the "last" pending recheck, trigger a notification when it's been processed. - numLeft := atomic.AddInt64(&txmp.txRecheck, -1) - if numLeft == 0 { - defer txmp.notifyTxsAvailable() // notify waiters on return, if mempool is non-empty - } else if numLeft < 0 { - return - } - +// This method is NOT executed for the initial CheckTx on a new transaction; +// that case is handled by addNewTransaction instead. +func (txmp *TxMempool) handleRecheckResult(tx types.Tx, checkTxRes *abci.ResponseCheckTx) { txmp.metrics.RecheckTimes.Add(1) - tx := types.Tx(req.GetCheckTx().Tx) - txmp.mtx.Lock() defer txmp.mtx.Unlock() @@ -678,11 +629,11 @@ func (txmp *TxMempool) recheckTxCallback(req *abci.Request, res *abci.Response) // If a postcheck hook is defined, call it before checking the result. var err error if txmp.postCheck != nil { - err = txmp.postCheck(tx, checkTxRes.CheckTx) + err = txmp.postCheck(tx, checkTxRes) } - if checkTxRes.CheckTx.Code == abci.CodeTypeOK && err == nil { - wtx.SetPriority(checkTxRes.CheckTx.Priority) + if checkTxRes.Code == abci.CodeTypeOK && err == nil { + wtx.SetPriority(checkTxRes.Priority) return // N.B. Size of mempool did not change } @@ -691,7 +642,7 @@ func (txmp *TxMempool) recheckTxCallback(req *abci.Request, res *abci.Response) "priority", wtx.Priority(), "tx", fmt.Sprintf("%X", wtx.tx.Hash()), "err", err, - "code", checkTxRes.CheckTx.Code, + "code", checkTxRes.Code, ) txmp.removeTxByElement(elt) txmp.metrics.FailedTxs.Add(1) @@ -716,33 +667,45 @@ func (txmp *TxMempool) recheckTransactions() { "num_txs", txmp.Size(), "height", txmp.height, ) - // N.B.: We have to issue the calls outside the lock. In a local client, - // even an "async" call invokes its callback immediately which will make the - // callback deadlock trying to acquire the same lock. This isn't a problem - // with out-of-process calls, but this has to work for both. - txmp.mtx.Unlock() - defer txmp.mtx.Lock() - ctx := context.TODO() - atomic.StoreInt64(&txmp.txRecheck, int64(txmp.txs.Len())) + // Collect transactions currently in the mempool requiring recheck. + wtxs := make([]*WrappedTx, 0, txmp.txs.Len()) for e := txmp.txs.Front(); e != nil; e = e.Next() { - wtx := e.Value.(*WrappedTx) + wtxs = append(wtxs, e.Value.(*WrappedTx)) + } - // The response for this CheckTx is handled by the default recheckTxCallback. - _, err := txmp.proxyAppConn.CheckTxAsync(ctx, abci.RequestCheckTx{ - Tx: wtx.tx, - Type: abci.CheckTxType_Recheck, - }) - if err != nil { - txmp.logger.Error("failed to execute CheckTx during recheck", - "err", err, "hash", fmt.Sprintf("%x", wtx.tx.Hash())) - atomic.AddInt64(&txmp.txRecheck, -1) + // Issue CheckTx calls for each remaining transaction, and when all the + // rechecks are complete signal watchers that transactions may be available. + go func() { + ctx := context.TODO() + g, start := taskgroup.New(nil).Limit(2 * runtime.NumCPU()) + + for _, wtx := range wtxs { + wtx := wtx + start(func() error { + rsp, err := txmp.proxyAppConn.CheckTxSync(ctx, abci.RequestCheckTx{ + Tx: wtx.tx, + Type: abci.CheckTxType_Recheck, + }) + if err != nil { + txmp.logger.Error("failed to execute CheckTx during recheck", + "err", err, "hash", fmt.Sprintf("%x", wtx.tx.Hash())) + } else { + txmp.handleRecheckResult(wtx.tx, rsp) + } + return nil + }) + } + if _, err := txmp.proxyAppConn.FlushAsync(ctx); err != nil { + txmp.logger.Error("failed to flush transactions during recheck", "err", err) } - } - if _, err := txmp.proxyAppConn.FlushAsync(ctx); err != nil { - txmp.logger.Error("failed to flush transactions during recheck", "err", err) - } + // When recheck is complete, trigger a notification for more transactions. + _ = g.Wait() + txmp.mtx.Lock() + defer txmp.mtx.Unlock() + txmp.notifyTxsAvailable() + }() } // canAddTx returns an error if we cannot insert the provided *WrappedTx into From 183e24970996d17d34a8c6732c7d1b21815f8de3 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Tue, 19 Jul 2022 14:02:14 -0700 Subject: [PATCH 48/58] Prepare changelog for candidate v0.35.9-rc0 (#9040) --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef64569b..d3172f85e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/cosmos). +## v0.35.9-rc0 + +This release fixes a deadlock that could occur in some cases when using the +priority mempool with the ABCI socket client. + +### BUG FIXES + +- [mempool] \#9030 rework lock discipline to mitigate callback deadlocks (@creachadair) + ## v0.35.8 July 12, 2022 From 3e96a376b02b3462beb0d4195a72fce239a98f31 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Wed, 20 Jul 2022 12:37:46 +0200 Subject: [PATCH 49/58] spec: merge v0.35 spec into tendermint (#9018) --- .github/CODEOWNERS | 4 +- .github/workflows/markdown-links.yml | 23 + .github/workflows/proto-check.yml | 29 + .github/workflows/proto.yml | 23 - .gitignore | 9 +- .markdownlint.yml | 11 + CHANGELOG.md | 2 +- CONTRIBUTING.md | 2 +- Makefile | 41 +- README.md | 4 +- UPGRADING.md | 6 +- abci/README.md | 2 +- abci/types/types.pb.go | 200 +- buf.work.yaml | 3 + docs/app-dev/indexing-transactions.md | 2 +- docs/nodes/logging.md | 2 +- docs/roadmap/roadmap.md | 4 +- docs/tendermint-core/consensus/README.md | 2 +- docs/tendermint-core/subscription.md | 2 +- docs/tendermint-core/using-tendermint.md | 2 +- go.mod | 6 +- go.sum | 43 +- internal/evidence/doc.go | 2 +- ivy-proofs/Dockerfile | 37 + ivy-proofs/README.md | 33 + ivy-proofs/abstract_tendermint.ivy | 178 + ivy-proofs/accountable_safety_1.ivy | 143 + ivy-proofs/accountable_safety_2.ivy | 52 + ivy-proofs/check_proofs.sh | 39 + ivy-proofs/classic_safety.ivy | 85 + ivy-proofs/count_lines.sh | 13 + ivy-proofs/docker-compose.yml | 8 + ivy-proofs/domain_model.ivy | 143 + ivy-proofs/network_shim.ivy | 133 + ivy-proofs/output/.gitignore | 4 + ivy-proofs/tendermint.ivy | 420 ++ ivy-proofs/tendermint_test.ivy | 127 + light/client.go | 2 +- light/doc.go | 2 +- proto/Dockerfile | 20 + proto/README.md | 21 + proto/buf.lock | 7 + buf.yaml => proto/buf.yaml | 17 +- proto/tendermint/blocksync/types.pb.go | 30 +- proto/tendermint/consensus/types.pb.go | 50 +- proto/tendermint/consensus/wal.pb.go | 25 +- proto/tendermint/crypto/keys.pb.go | 5 +- proto/tendermint/crypto/proof.pb.go | 25 +- proto/tendermint/libs/bits/types.pb.go | 5 +- proto/tendermint/mempool/types.pb.go | 10 +- proto/tendermint/p2p/conn.pb.go | 25 +- proto/tendermint/p2p/pex.pb.go | 35 +- proto/tendermint/p2p/types.pb.go | 25 +- proto/tendermint/privval/types.pb.go | 55 +- proto/tendermint/rpc/grpc/types.pb.go | 936 ++++ proto/tendermint/state/types.pb.go | 25 +- proto/tendermint/statesync/types.pb.go | 45 +- proto/tendermint/statesync/types.proto | 2 +- proto/tendermint/types/block.pb.go | 5 +- proto/tendermint/types/canonical.pb.go | 20 +- proto/tendermint/types/events.pb.go | 5 +- proto/tendermint/types/evidence.pb.go | 20 +- proto/tendermint/types/params.pb.go | 30 +- proto/tendermint/types/types.pb.go | 65 +- proto/tendermint/types/validator.pb.go | 15 +- proto/tendermint/version/types.pb.go | 5 +- scripts/protocgen.sh | 9 - spec/README.md | 93 + spec/abci/README.md | 27 + spec/abci/abci.md | 775 +++ spec/abci/apps.md | 671 +++ spec/abci/client-server.md | 113 + spec/blockchain/blockchain.md | 3 + spec/blockchain/encoding.md | 3 + spec/blockchain/readme.md | 14 + spec/blockchain/state.md | 3 + spec/consensus/bft-time.md | 55 + spec/consensus/consensus-paper/IEEEtran.bst | 2417 +++++++++ spec/consensus/consensus-paper/IEEEtran.cls | 4733 +++++++++++++++++ spec/consensus/consensus-paper/README.md | 24 + .../consensus-paper/algorithmicplus.sty | 195 + spec/consensus/consensus-paper/conclusion.tex | 16 + spec/consensus/consensus-paper/consensus.tex | 397 ++ .../consensus/consensus-paper/definitions.tex | 126 + spec/consensus/consensus-paper/homodel.sty | 32 + spec/consensus/consensus-paper/intro.tex | 138 + spec/consensus/consensus-paper/latex8.bst | 1124 ++++ spec/consensus/consensus-paper/latex8.sty | 168 + spec/consensus/consensus-paper/lit.bib | 1659 ++++++ spec/consensus/consensus-paper/paper.tex | 153 + spec/consensus/consensus-paper/proof.tex | 280 + spec/consensus/consensus-paper/rounddiag.sty | 62 + spec/consensus/consensus-paper/technote.sty | 118 + spec/consensus/consensus.md | 352 ++ spec/consensus/creating-proposal.md | 43 + spec/consensus/evidence.md | 199 + spec/consensus/light-client/README.md | 9 + spec/consensus/light-client/accountability.md | 3 + .../light-client/assets/light-node-image.png | Bin 0 -> 122270 bytes spec/consensus/light-client/detection.md | 3 + spec/consensus/light-client/verification.md | 3 + spec/consensus/proposer-selection.md | 323 ++ spec/consensus/readme.md | 32 + spec/consensus/signing.md | 229 + spec/consensus/wal.md | 32 + spec/core/data_structures.md | 456 ++ spec/core/encoding.md | 300 ++ spec/core/genesis.md | 34 + spec/core/readme.md | 13 + spec/core/state.md | 121 + spec/ivy-proofs/Dockerfile | 37 + spec/ivy-proofs/README.md | 33 + spec/ivy-proofs/abstract_tendermint.ivy | 178 + spec/ivy-proofs/accountable_safety_1.ivy | 143 + spec/ivy-proofs/accountable_safety_2.ivy | 52 + spec/ivy-proofs/check_proofs.sh | 39 + spec/ivy-proofs/classic_safety.ivy | 85 + spec/ivy-proofs/count_lines.sh | 13 + spec/ivy-proofs/docker-compose.yml | 8 + spec/ivy-proofs/domain_model.ivy | 143 + spec/ivy-proofs/network_shim.ivy | 133 + spec/ivy-proofs/output/.gitignore | 4 + spec/ivy-proofs/tendermint.ivy | 420 ++ spec/ivy-proofs/tendermint_test.ivy | 127 + spec/light-client/README.md | 205 + .../accountability/001indinv-apalache.csv | 13 + spec/light-client/accountability/MC_n4_f1.tla | 22 + spec/light-client/accountability/MC_n4_f2.tla | 22 + .../accountability/MC_n4_f2_amnesia.tla | 40 + spec/light-client/accountability/MC_n4_f3.tla | 22 + spec/light-client/accountability/MC_n5_f1.tla | 22 + spec/light-client/accountability/MC_n5_f2.tla | 22 + spec/light-client/accountability/MC_n6_f1.tla | 22 + spec/light-client/accountability/README.md | 308 ++ spec/light-client/accountability/Synopsis.md | 105 + .../TendermintAccDebug_004_draft.tla | 100 + .../TendermintAccInv_004_draft.tla | 370 ++ .../TendermintAccTrace_004_draft.tla | 33 + .../TendermintAcc_004_draft.tla | 474 ++ .../results/001indinv-apalache-mem-log.svg | 1063 ++++ .../results/001indinv-apalache-mem.svg | 1141 ++++ .../results/001indinv-apalache-ncells.svg | 1015 ++++ .../results/001indinv-apalache-nclauses.svg | 1133 ++++ .../results/001indinv-apalache-report.md | 61 + .../results/001indinv-apalache-time-log.svg | 1134 ++++ .../results/001indinv-apalache-time.svg | 957 ++++ .../results/001indinv-apalache-unstable.csv | 13 + spec/light-client/accountability/run.sh | 9 + spec/light-client/assets/light-node-image.png | Bin 0 -> 122270 bytes .../attacks/Blockchain_003_draft.tla | 166 + .../attacks/Isolation_001_draft.tla | 159 + .../attacks/LCVerificationApi_003_draft.tla | 192 + spec/light-client/attacks/MC_5_3.tla | 18 + .../attacks/isolate-attackers_001_draft.md | 221 + .../attacks/isolate-attackers_002_reviewed.md | 223 + .../attacks/notes-on-evidence-handling.md | 219 + .../detection/004bmc-apalache-ok.csv | 10 + .../detection/005bmc-apalache-error.csv | 4 + .../detection/Blockchain_003_draft.tla | 164 + .../detection/LCD_MC3_3_faulty.tla | 27 + .../detection/LCD_MC3_4_faulty.tla | 27 + .../detection/LCD_MC4_4_faulty.tla | 27 + .../detection/LCD_MC5_5_faulty.tla | 27 + .../detection/LCDetector_003_draft.tla | 373 ++ .../detection/LCVerificationApi_003_draft.tla | 192 + spec/light-client/detection/README.md | 75 + .../detection/detection_001_reviewed.md | 788 +++ .../detection/detection_003_reviewed.md | 839 +++ spec/light-client/detection/discussions.md | 178 + .../light-client/detection/draft-functions.md | 289 + .../detection/req-ibc-detection.md | 345 ++ spec/light-client/experiments.png | Bin 0 -> 83681 bytes .../supervisor/supervisor_001_draft.md | 637 +++ .../supervisor/supervisor_001_draft.tla | 71 + .../supervisor/supervisor_002_draft.md | 131 + .../verification/001bmc-apalache.csv | 49 + .../verification/002bmc-apalache-ok.csv | 55 + .../verification/003bmc-apalache-error.csv | 45 + .../verification/004bmc-apalache-ok.csv | 10 + .../verification/005bmc-apalache-error.csv | 4 + .../verification/Blockchain_002_draft.tla | 171 + .../verification/Blockchain_003_draft.tla | 164 + .../verification/Blockchain_A_1.tla | 171 + .../LCVerificationApi_003_draft.tla | 192 + .../verification/Lightclient_002_draft.tla | 465 ++ .../verification/Lightclient_003_draft.tla | 493 ++ .../verification/Lightclient_A_1.tla | 440 ++ .../verification/MC4_3_correct.tla | 26 + .../verification/MC4_3_faulty.tla | 26 + .../verification/MC4_4_correct.tla | 26 + .../verification/MC4_4_correct_drifted.tla | 26 + .../verification/MC4_4_faulty.tla | 26 + .../verification/MC4_4_faulty_drifted.tla | 26 + .../verification/MC4_5_correct.tla | 26 + .../verification/MC4_5_faulty.tla | 26 + .../verification/MC4_6_faulty.tla | 26 + .../verification/MC4_7_faulty.tla | 26 + .../verification/MC5_5_correct.tla | 26 + .../MC5_5_correct_peer_two_thirds_faulty.tla | 26 + .../verification/MC5_5_faulty.tla | 26 + .../MC5_5_faulty_peer_two_thirds_faulty.tla | 26 + .../verification/MC5_7_faulty.tla | 26 + .../verification/MC7_5_faulty.tla | 26 + .../verification/MC7_7_faulty.tla | 26 + spec/light-client/verification/README.md | 577 ++ .../verification_001_published.md | 1178 ++++ .../verification/verification_002_draft.md | 1061 ++++ .../verification/verification_003_draft.md | 76 + spec/p2p/config.md | 49 + spec/p2p/connection.md | 111 + spec/p2p/messages/README.md | 19 + spec/p2p/messages/block-sync.md | 68 + spec/p2p/messages/consensus.md | 149 + spec/p2p/messages/evidence.md | 23 + spec/p2p/messages/mempool.md | 33 + spec/p2p/messages/pex.md | 76 + spec/p2p/messages/state-sync.md | 133 + spec/p2p/node.md | 67 + spec/p2p/peer.md | 130 + spec/p2p/readme.md | 6 + spec/rpc/README.md | 1264 +++++ third_party/proto/gogoproto/gogo.proto | 2 +- tools/tools.go | 1 + types/block.go | 2 +- 224 files changed, 41549 insertions(+), 231 deletions(-) create mode 100644 .github/workflows/markdown-links.yml create mode 100644 .github/workflows/proto-check.yml delete mode 100644 .github/workflows/proto.yml create mode 100644 .markdownlint.yml create mode 100644 buf.work.yaml create mode 100644 ivy-proofs/Dockerfile create mode 100644 ivy-proofs/README.md create mode 100644 ivy-proofs/abstract_tendermint.ivy create mode 100644 ivy-proofs/accountable_safety_1.ivy create mode 100644 ivy-proofs/accountable_safety_2.ivy create mode 100755 ivy-proofs/check_proofs.sh create mode 100644 ivy-proofs/classic_safety.ivy create mode 100755 ivy-proofs/count_lines.sh create mode 100644 ivy-proofs/docker-compose.yml create mode 100644 ivy-proofs/domain_model.ivy create mode 100644 ivy-proofs/network_shim.ivy create mode 100644 ivy-proofs/output/.gitignore create mode 100644 ivy-proofs/tendermint.ivy create mode 100644 ivy-proofs/tendermint_test.ivy create mode 100644 proto/Dockerfile create mode 100644 proto/README.md create mode 100644 proto/buf.lock rename buf.yaml => proto/buf.yaml (50%) create mode 100644 proto/tendermint/rpc/grpc/types.pb.go delete mode 100755 scripts/protocgen.sh create mode 100644 spec/README.md create mode 100644 spec/abci/README.md create mode 100644 spec/abci/abci.md create mode 100644 spec/abci/apps.md create mode 100644 spec/abci/client-server.md create mode 100644 spec/blockchain/blockchain.md create mode 100644 spec/blockchain/encoding.md create mode 100644 spec/blockchain/readme.md create mode 100644 spec/blockchain/state.md create mode 100644 spec/consensus/bft-time.md create mode 100644 spec/consensus/consensus-paper/IEEEtran.bst create mode 100644 spec/consensus/consensus-paper/IEEEtran.cls create mode 100644 spec/consensus/consensus-paper/README.md create mode 100644 spec/consensus/consensus-paper/algorithmicplus.sty create mode 100644 spec/consensus/consensus-paper/conclusion.tex create mode 100644 spec/consensus/consensus-paper/consensus.tex create mode 100644 spec/consensus/consensus-paper/definitions.tex create mode 100644 spec/consensus/consensus-paper/homodel.sty create mode 100644 spec/consensus/consensus-paper/intro.tex create mode 100644 spec/consensus/consensus-paper/latex8.bst create mode 100644 spec/consensus/consensus-paper/latex8.sty create mode 100644 spec/consensus/consensus-paper/lit.bib create mode 100644 spec/consensus/consensus-paper/paper.tex create mode 100644 spec/consensus/consensus-paper/proof.tex create mode 100644 spec/consensus/consensus-paper/rounddiag.sty create mode 100644 spec/consensus/consensus-paper/technote.sty create mode 100644 spec/consensus/consensus.md create mode 100644 spec/consensus/creating-proposal.md create mode 100644 spec/consensus/evidence.md create mode 100644 spec/consensus/light-client/README.md create mode 100644 spec/consensus/light-client/accountability.md create mode 100644 spec/consensus/light-client/assets/light-node-image.png create mode 100644 spec/consensus/light-client/detection.md create mode 100644 spec/consensus/light-client/verification.md create mode 100644 spec/consensus/proposer-selection.md create mode 100644 spec/consensus/readme.md create mode 100644 spec/consensus/signing.md create mode 100644 spec/consensus/wal.md create mode 100644 spec/core/data_structures.md create mode 100644 spec/core/encoding.md create mode 100644 spec/core/genesis.md create mode 100644 spec/core/readme.md create mode 100644 spec/core/state.md create mode 100644 spec/ivy-proofs/Dockerfile create mode 100644 spec/ivy-proofs/README.md create mode 100644 spec/ivy-proofs/abstract_tendermint.ivy create mode 100644 spec/ivy-proofs/accountable_safety_1.ivy create mode 100644 spec/ivy-proofs/accountable_safety_2.ivy create mode 100755 spec/ivy-proofs/check_proofs.sh create mode 100644 spec/ivy-proofs/classic_safety.ivy create mode 100755 spec/ivy-proofs/count_lines.sh create mode 100644 spec/ivy-proofs/docker-compose.yml create mode 100644 spec/ivy-proofs/domain_model.ivy create mode 100644 spec/ivy-proofs/network_shim.ivy create mode 100644 spec/ivy-proofs/output/.gitignore create mode 100644 spec/ivy-proofs/tendermint.ivy create mode 100644 spec/ivy-proofs/tendermint_test.ivy create mode 100644 spec/light-client/README.md create mode 100644 spec/light-client/accountability/001indinv-apalache.csv create mode 100644 spec/light-client/accountability/MC_n4_f1.tla create mode 100644 spec/light-client/accountability/MC_n4_f2.tla create mode 100644 spec/light-client/accountability/MC_n4_f2_amnesia.tla create mode 100644 spec/light-client/accountability/MC_n4_f3.tla create mode 100644 spec/light-client/accountability/MC_n5_f1.tla create mode 100644 spec/light-client/accountability/MC_n5_f2.tla create mode 100644 spec/light-client/accountability/MC_n6_f1.tla create mode 100644 spec/light-client/accountability/README.md create mode 100644 spec/light-client/accountability/Synopsis.md create mode 100644 spec/light-client/accountability/TendermintAccDebug_004_draft.tla create mode 100644 spec/light-client/accountability/TendermintAccInv_004_draft.tla create mode 100644 spec/light-client/accountability/TendermintAccTrace_004_draft.tla create mode 100644 spec/light-client/accountability/TendermintAcc_004_draft.tla create mode 100644 spec/light-client/accountability/results/001indinv-apalache-mem-log.svg create mode 100644 spec/light-client/accountability/results/001indinv-apalache-mem.svg create mode 100644 spec/light-client/accountability/results/001indinv-apalache-ncells.svg create mode 100644 spec/light-client/accountability/results/001indinv-apalache-nclauses.svg create mode 100644 spec/light-client/accountability/results/001indinv-apalache-report.md create mode 100644 spec/light-client/accountability/results/001indinv-apalache-time-log.svg create mode 100644 spec/light-client/accountability/results/001indinv-apalache-time.svg create mode 100644 spec/light-client/accountability/results/001indinv-apalache-unstable.csv create mode 100755 spec/light-client/accountability/run.sh create mode 100644 spec/light-client/assets/light-node-image.png create mode 100644 spec/light-client/attacks/Blockchain_003_draft.tla create mode 100644 spec/light-client/attacks/Isolation_001_draft.tla create mode 100644 spec/light-client/attacks/LCVerificationApi_003_draft.tla create mode 100644 spec/light-client/attacks/MC_5_3.tla create mode 100644 spec/light-client/attacks/isolate-attackers_001_draft.md create mode 100644 spec/light-client/attacks/isolate-attackers_002_reviewed.md create mode 100644 spec/light-client/attacks/notes-on-evidence-handling.md create mode 100644 spec/light-client/detection/004bmc-apalache-ok.csv create mode 100644 spec/light-client/detection/005bmc-apalache-error.csv create mode 100644 spec/light-client/detection/Blockchain_003_draft.tla create mode 100644 spec/light-client/detection/LCD_MC3_3_faulty.tla create mode 100644 spec/light-client/detection/LCD_MC3_4_faulty.tla create mode 100644 spec/light-client/detection/LCD_MC4_4_faulty.tla create mode 100644 spec/light-client/detection/LCD_MC5_5_faulty.tla create mode 100644 spec/light-client/detection/LCDetector_003_draft.tla create mode 100644 spec/light-client/detection/LCVerificationApi_003_draft.tla create mode 100644 spec/light-client/detection/README.md create mode 100644 spec/light-client/detection/detection_001_reviewed.md create mode 100644 spec/light-client/detection/detection_003_reviewed.md create mode 100644 spec/light-client/detection/discussions.md create mode 100644 spec/light-client/detection/draft-functions.md create mode 100644 spec/light-client/detection/req-ibc-detection.md create mode 100644 spec/light-client/experiments.png create mode 100644 spec/light-client/supervisor/supervisor_001_draft.md create mode 100644 spec/light-client/supervisor/supervisor_001_draft.tla create mode 100644 spec/light-client/supervisor/supervisor_002_draft.md create mode 100644 spec/light-client/verification/001bmc-apalache.csv create mode 100644 spec/light-client/verification/002bmc-apalache-ok.csv create mode 100644 spec/light-client/verification/003bmc-apalache-error.csv create mode 100644 spec/light-client/verification/004bmc-apalache-ok.csv create mode 100644 spec/light-client/verification/005bmc-apalache-error.csv create mode 100644 spec/light-client/verification/Blockchain_002_draft.tla create mode 100644 spec/light-client/verification/Blockchain_003_draft.tla create mode 100644 spec/light-client/verification/Blockchain_A_1.tla create mode 100644 spec/light-client/verification/LCVerificationApi_003_draft.tla create mode 100644 spec/light-client/verification/Lightclient_002_draft.tla create mode 100644 spec/light-client/verification/Lightclient_003_draft.tla create mode 100644 spec/light-client/verification/Lightclient_A_1.tla create mode 100644 spec/light-client/verification/MC4_3_correct.tla create mode 100644 spec/light-client/verification/MC4_3_faulty.tla create mode 100644 spec/light-client/verification/MC4_4_correct.tla create mode 100644 spec/light-client/verification/MC4_4_correct_drifted.tla create mode 100644 spec/light-client/verification/MC4_4_faulty.tla create mode 100644 spec/light-client/verification/MC4_4_faulty_drifted.tla create mode 100644 spec/light-client/verification/MC4_5_correct.tla create mode 100644 spec/light-client/verification/MC4_5_faulty.tla create mode 100644 spec/light-client/verification/MC4_6_faulty.tla create mode 100644 spec/light-client/verification/MC4_7_faulty.tla create mode 100644 spec/light-client/verification/MC5_5_correct.tla create mode 100644 spec/light-client/verification/MC5_5_correct_peer_two_thirds_faulty.tla create mode 100644 spec/light-client/verification/MC5_5_faulty.tla create mode 100644 spec/light-client/verification/MC5_5_faulty_peer_two_thirds_faulty.tla create mode 100644 spec/light-client/verification/MC5_7_faulty.tla create mode 100644 spec/light-client/verification/MC7_5_faulty.tla create mode 100644 spec/light-client/verification/MC7_7_faulty.tla create mode 100644 spec/light-client/verification/README.md create mode 100644 spec/light-client/verification/verification_001_published.md create mode 100644 spec/light-client/verification/verification_002_draft.md create mode 100644 spec/light-client/verification/verification_003_draft.md create mode 100644 spec/p2p/config.md create mode 100644 spec/p2p/connection.md create mode 100644 spec/p2p/messages/README.md create mode 100644 spec/p2p/messages/block-sync.md create mode 100644 spec/p2p/messages/consensus.md create mode 100644 spec/p2p/messages/evidence.md create mode 100644 spec/p2p/messages/mempool.md create mode 100644 spec/p2p/messages/pex.md create mode 100644 spec/p2p/messages/state-sync.md create mode 100644 spec/p2p/node.md create mode 100644 spec/p2p/peer.md create mode 100644 spec/p2p/readme.md create mode 100644 spec/rpc/README.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b48354f01..27f9d2704 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,4 +7,6 @@ # global owners are only requested if there isn't a more specific # codeowner specified below. For this reason, the global codeowners # are often repeated in package-level definitions. -* @ebuchman @cmwaters @tychoish @williambanfield @creachadair +* @ebuchman @cmwaters @tychoish @williambanfield @creachadair @sergio-mena @jmalicevic @thanethomson @samricotta + +/spec @josef-widder @milosevic @cason @sergio-mena @jmalicevic diff --git a/.github/workflows/markdown-links.yml b/.github/workflows/markdown-links.yml new file mode 100644 index 000000000..7af7e3ce9 --- /dev/null +++ b/.github/workflows/markdown-links.yml @@ -0,0 +1,23 @@ +name: Check Markdown links + +on: + push: + branches: + - master + pull_request: + branches: [master] + +jobs: + markdown-link-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: technote-space/get-diff-action@v6 + with: + PATTERNS: | + **/**.md + - uses: creachadair/github-action-markdown-link-check@master + with: + check-modified-files-only: 'yes' + config-file: '.md-link-check.json' + if: env.GIT_DIFF diff --git a/.github/workflows/proto-check.yml b/.github/workflows/proto-check.yml new file mode 100644 index 000000000..e245d52ef --- /dev/null +++ b/.github/workflows/proto-check.yml @@ -0,0 +1,29 @@ +name: Proto Check +# Protobuf runs buf (https://buf.build/) lint and check-breakage +# This workflow is only run when a file in the proto directory +# has been modified. +on: + workflow_dispatch: # allow running workflow manually + pull_request: + paths: + - "proto/**" +jobs: + proto-lint: + runs-on: ubuntu-latest + timeout-minutes: 4 + steps: + - uses: actions/checkout@v2.4.0 + - uses: bufbuild/buf-setup-action@v1.6.0 + - uses: bufbuild/buf-lint-action@v1 + with: + input: 'proto' + + proto-breakage: + runs-on: ubuntu-latest + timeout-minutes: 4 + steps: + - uses: actions/checkout@v2.4.0 + - uses: bufbuild/buf-setup-action@v1.6.0 + - uses: bufbuild/buf-breaking-action@v1 + with: + against: 'https://github.com/tendermint/tendermint.git#branch=v0.35.x' diff --git a/.github/workflows/proto.yml b/.github/workflows/proto.yml deleted file mode 100644 index 92902433d..000000000 --- a/.github/workflows/proto.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Protobuf -# Protobuf runs buf (https://buf.build/) lint and check-breakage -# This workflow is only run when a .proto file has been modified -on: - workflow_dispatch: # allow running workflow manually - pull_request: - paths: - - "**.proto" -jobs: - proto-lint: - runs-on: ubuntu-latest - timeout-minutes: 4 - steps: - - uses: actions/checkout@v3 - - name: lint - run: make proto-lint - proto-breakage: - runs-on: ubuntu-latest - timeout-minutes: 4 - steps: - - uses: actions/checkout@v3 - - name: check-breakage - run: make proto-check-breaking-ci diff --git a/.gitignore b/.gitignore index 3994eee92..a38c383d2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ .idea/ .revision .tendermint -.tendermint-lite +.tendermint-light .terraform .vagrant .vendor-new/ @@ -47,3 +47,10 @@ test/fuzz/**/corpus test/fuzz/**/crashers test/fuzz/**/suppressions test/fuzz/**/*.zip +*.aux +*.bbl +*.blg +*.pdf +*.gz +*.dvi +.idea diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 000000000..baa78a116 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,11 @@ +default: true +MD001: false +MD007: { indent: 4 } +MD013: false +MD024: { siblings_only: true } +MD025: false +MD033: false +MD036: false +MD010: false +MD012: false +MD028: false diff --git a/CHANGELOG.md b/CHANGELOG.md index d3172f85e..906321aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1091,7 +1091,7 @@ and a validator address plus a timestamp. Note we may remove the validator address & timestamp fields in the future (see ADR-25). `lite2` package has been added to solve `lite` issues and introduce weak -subjectivity interface. Refer to the [spec](https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md) for complete details. +subjectivity interface. Refer to the [spec](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/consensus/light-client.md) for complete details. `lite` package is now deprecated and will be removed in v0.34 release. ### BREAKING CHANGES: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33b8cf6a7..f9f0bacd2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ will indicate their support with a heartfelt emoji. If the issue would benefit from thorough discussion, maintainers may request that you create a [Request For -Comment](https://github.com/tendermint/spec/tree/master/rfc) +Comment](https://github.com/tendermint/tendermint/tree/master/docs/rfc) in the Tendermint spec repo. Discussion at the RFC stage will build collective understanding of the dimensions of the problems and help structure conversations around trade-offs. diff --git a/Makefile b/Makefile index c9836956b..03d9cb5b8 100644 --- a/Makefile +++ b/Makefile @@ -83,14 +83,29 @@ $(BUILDDIR)/: proto-all: proto-gen proto-lint proto-check-breaking .PHONY: proto-all -proto-gen: - @echo "Generating Go packages for .proto files" - @$(DOCKER_PROTO) sh ./scripts/protocgen.sh +check-proto-deps: +ifeq (,$(shell which protoc-gen-gogofaster)) + $(error "gogofaster plugin for protoc is required. Run 'go install github.com/gogo/protobuf/protoc-gen-gogofaster@latest' to install") +endif +.PHONY: check-proto-deps + +check-proto-format-deps: +ifeq (,$(shell which clang-format)) + $(error "clang-format is required for Protobuf formatting. See instructions for your platform on how to install it.") +endif +.PHONY: check-proto-format-deps + +proto-gen: check-proto-deps + @echo "Generating Protobuf files" + @go run github.com/bufbuild/buf/cmd/buf generate + @mv ./proto/tendermint/abci/types.pb.go ./abci/types/ .PHONY: proto-gen -proto-lint: - @echo "Running lint checks for .proto files" - @$(DOCKER_PROTO) buf lint --error-format=json +# These targets are provided for convenience and are intended for local +# execution only. +proto-lint: check-proto-deps + @echo "Linting Protobuf files" + @go run github.com/bufbuild/buf/cmd/buf lint .PHONY: proto-lint proto-format: @@ -98,16 +113,14 @@ proto-format: @$(DOCKER_PROTO) find ./ -not -path "./third_party/*" -name '*.proto' -exec clang-format -i {} \; .PHONY: proto-format -proto-check-breaking: - @echo "Checking for breaking changes in .proto files" - @$(DOCKER_PROTO) buf breaking --against .git#branch=$(BASE_BRANCH) +proto-check-breaking: check-proto-deps + @echo "Checking for breaking changes in Protobuf files against local branch" + @echo "Note: This is only useful if your changes have not yet been committed." + @echo " Otherwise read up on buf's \"breaking\" command usage:" + @echo " https://docs.buf.build/breaking/usage" + @go run github.com/bufbuild/buf/cmd/buf breaking --against ".git" .PHONY: proto-check-breaking -proto-check-breaking-ci: - @echo "Checking for breaking changes in .proto files" - @$(DOCKER_PROTO) buf breaking --against $(HTTPS_GIT)#branch=$(BASE_BRANCH) -.PHONY: proto-check-breaking-ci - ############################################################################### ### Build ABCI ### ############################################################################### diff --git a/README.md b/README.md index 0fc06a7ad..1179aed68 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Or [Blockchain](), for shor Tendermint Core is a Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine - written in any programming language - and securely replicates it on many machines. -For protocol details, see [the specification](https://github.com/tendermint/spec). +For protocol details, see [the specification](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/README.md). For detailed analysis of the consensus protocol, including safety and liveness proofs, see our recent paper, "[The latest gossip on BFT consensus](https://arxiv.org/abs/1807.04938)". @@ -70,7 +70,7 @@ Please abide by the [Code of Conduct](CODE_OF_CONDUCT.md) in all interactions. Before contributing to the project, please take a look at the [contributing guidelines](CONTRIBUTING.md) and the [style guide](STYLE_GUIDE.md). You may also find it helpful to read the -[specifications](https://github.com/tendermint/spec), watch the [Developer Sessions](/docs/DEV_SESSIONS.md), +[specifications](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/README.md), watch the [Developer Sessions](/docs/DEV_SESSIONS.md), and familiarize yourself with our [Architectural Decision Records](https://github.com/tendermint/tendermint/tree/master/docs/architecture). diff --git a/UPGRADING.md b/UPGRADING.md index 89aa67e7e..29eeb138b 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -713,14 +713,14 @@ due to changes in how various data structures are hashed. Any implementations of Tendermint blockchain verification, including lite clients, will need to be updated. For specific details: -* [Merkle tree](https://github.com/tendermint/spec/blob/master/spec/blockchain/encoding.md#merkle-trees) -* [ConsensusParams](https://github.com/tendermint/spec/blob/master/spec/blockchain/state.md#consensusparams) +* [Merkle tree](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/encoding.md#merkle-trees) +* [ConsensusParams](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/state.md#consensusparams) There was also a small change to field ordering in the vote struct. Any implementations of an out-of-process validator (like a Key-Management Server) will need to be updated. For specific details: -* [Vote](https://github.com/tendermint/spec/blob/master/spec/consensus/signing.md#votes) +* [Vote](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/consensus/signing.md#votes) Finally, the proposer selection algorithm continues to evolve. See the [work-in-progress diff --git a/abci/README.md b/abci/README.md index 4a953dab3..59deda24d 100644 --- a/abci/README.md +++ b/abci/README.md @@ -19,7 +19,7 @@ To get up and running quickly, see the [getting started guide](../docs/app-dev/g A detailed description of the ABCI methods and message types is contained in: -- [The main spec](https://github.com/tendermint/spec/blob/master/spec/abci/abci.md) +- [The main spec](https://github.com/tendermint/tendermint/blob/master/spec/abci/abci.md) - [A protobuf file](../proto/tendermint/abci/types.proto) - [A Go interface](./types/application.go) diff --git a/abci/types/types.pb.go b/abci/types/types.pb.go index 6b00c587a..b7b1311c6 100644 --- a/abci/types/types.pb.go +++ b/abci/types/types.pb.go @@ -7715,7 +7715,10 @@ func (m *Request) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -7797,7 +7800,10 @@ func (m *RequestEcho) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -7847,7 +7853,10 @@ func (m *RequestFlush) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -7999,7 +8008,10 @@ func (m *RequestInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -8237,7 +8249,10 @@ func (m *RequestInitChain) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -8392,7 +8407,10 @@ func (m *RequestQuery) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -8576,7 +8594,10 @@ func (m *RequestBeginBlock) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -8679,7 +8700,10 @@ func (m *RequestCheckTx) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -8763,7 +8787,10 @@ func (m *RequestDeliverTx) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -8832,7 +8859,10 @@ func (m *RequestEndBlock) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -8882,7 +8912,10 @@ func (m *RequestCommit) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -8932,7 +8965,10 @@ func (m *RequestListSnapshots) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -9052,7 +9088,10 @@ func (m *RequestOfferSnapshot) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -9159,7 +9198,10 @@ func (m *RequestLoadSnapshotChunk) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -9294,7 +9336,10 @@ func (m *RequestApplySnapshotChunk) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -9869,7 +9914,10 @@ func (m *Response) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -9951,7 +9999,10 @@ func (m *ResponseException) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -10033,7 +10084,10 @@ func (m *ResponseEcho) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -10083,7 +10137,10 @@ func (m *ResponseFlush) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -10269,7 +10326,10 @@ func (m *ResponseInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -10423,7 +10483,10 @@ func (m *ResponseInitChain) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -10730,7 +10793,10 @@ func (m *ResponseQuery) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -10814,7 +10880,10 @@ func (m *ResponseBeginBlock) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -11168,7 +11237,10 @@ func (m *ResponseCheckTx) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -11439,7 +11511,10 @@ func (m *ResponseDeliverTx) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -11593,7 +11668,10 @@ func (m *ResponseEndBlock) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -11696,7 +11774,10 @@ func (m *ResponseCommit) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -11780,7 +11861,10 @@ func (m *ResponseListSnapshots) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -11849,7 +11933,10 @@ func (m *ResponseOfferSnapshot) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -11933,7 +12020,10 @@ func (m *ResponseLoadSnapshotChunk) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -12110,7 +12200,10 @@ func (m *ResponseApplySnapshotChunk) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -12213,7 +12306,10 @@ func (m *LastCommitInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -12329,7 +12425,10 @@ func (m *Event) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -12463,7 +12562,10 @@ func (m *EventAttribute) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -12618,7 +12720,10 @@ func (m *TxResult) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -12721,7 +12826,10 @@ func (m *Validator) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -12823,7 +12931,10 @@ func (m *ValidatorUpdate) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -12926,7 +13037,10 @@ func (m *VoteInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -13099,7 +13213,10 @@ func (m *Evidence) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -13274,7 +13391,10 @@ func (m *Snapshot) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/buf.work.yaml b/buf.work.yaml new file mode 100644 index 000000000..1878b341b --- /dev/null +++ b/buf.work.yaml @@ -0,0 +1,3 @@ +version: v1 +directories: + - proto diff --git a/docs/app-dev/indexing-transactions.md b/docs/app-dev/indexing-transactions.md index b8b06d01b..af96b741b 100644 --- a/docs/app-dev/indexing-transactions.md +++ b/docs/app-dev/indexing-transactions.md @@ -15,7 +15,7 @@ the block itself is never stored. Each event contains a type and a list of attributes, which are key-value pairs denoting something about what happened during the method's execution. For more details on `Events`, see the -[ABCI](https://github.com/tendermint/spec/blob/master/spec/abci/abci.md#events) +[ABCI](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/abci/abci.md#events) documentation. An `Event` has a composite key associated with it. A `compositeKey` is diff --git a/docs/nodes/logging.md b/docs/nodes/logging.md index 31a9d08d2..5a0a8f1a8 100644 --- a/docs/nodes/logging.md +++ b/docs/nodes/logging.md @@ -120,7 +120,7 @@ Next follows a standard block creation cycle, where we enter a new round, propose a block, receive more than 2/3 of prevotes, then precommits and finally have a chance to commit a block. For details, please refer to [Byzantine Consensus -Algorithm](https://github.com/tendermint/spec/blob/master/spec/consensus/consensus.md). +Algorithm](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/consensus/consensus.md). ```sh I[10-04|13:54:30.393] enterNewRound(91/0). Current: 91/0/RoundStepNewHeight module=consensus diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 19d9d89fb..a199a97a2 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -8,7 +8,7 @@ order: 1 This document endeavours to inform the wider Tendermint community about development plans and priorities for Tendermint Core, and when we expect features to be delivered. It is intended to broadly inform all users of Tendermint, including application developers, node operators, integrators, and the engineering and research teams. -Anyone wishing to propose work to be a part of this roadmap should do so by opening an [issue](https://github.com/tendermint/spec/issues/new/choose) in the spec. Bug reports and other implementation concerns should be brought up in the [core repository](https://github.com/tendermint/tendermint). +Anyone wishing to propose work to be a part of this roadmap should do so by opening an [issue](https://github.com/tendermint/tendermint/issues/new/choose) in the spec. Bug reports and other implementation concerns should be brought up in the [core repository](https://github.com/tendermint/tendermint). This roadmap should be read as a high-level guide to plans and priorities, rather than a commitment to schedules and deliverables. Features earlier on the roadmap will generally be more specific and detailed than those later on. We will update this document periodically to reflect the current status. @@ -43,7 +43,7 @@ Added a new `EventSink` interface to allow alternatives to Tendermint's propriet ### ABCI++ -An overhaul of the existing interface between the application and consensus, to give the application more control over block construction. ABCI++ adds new hooks allowing modification of transactions before they get into a block, verification of a block before voting, injection of signed information into votes, and more compact delivery of blocks after agreement (to allow for concurrent execution). [More](https://github.com/tendermint/spec/blob/master/rfc/004-abci%2B%2B.md) +An overhaul of the existing interface between the application and consensus, to give the application more control over block construction. ABCI++ adds new hooks allowing modification of transactions before they get into a block, verification of a block before voting, injection of signed information into votes, and more compact delivery of blocks after agreement (to allow for concurrent execution). [More](https://github.com/tendermint/tendermint/blob/v0.35.x/rfc/004-abci%2B%2B.md) ### Proposer-Based Timestamps diff --git a/docs/tendermint-core/consensus/README.md b/docs/tendermint-core/consensus/README.md index bd7def551..35567745f 100644 --- a/docs/tendermint-core/consensus/README.md +++ b/docs/tendermint-core/consensus/README.md @@ -23,7 +23,7 @@ explained in a forthcoming document. For efficiency reasons, validators in Tendermint consensus protocol do not agree directly on the block as the block size is big, i.e., they don't embed the block inside `Proposal` and `VoteMessage`. Instead, they reach agreement on the `BlockID` (see `BlockID` definition in -[Blockchain](https://github.com/tendermint/spec/blob/master/spec/core/data_structures.md#blockid) section) +[Blockchain](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/core/data_structures.md#blockid) section) that uniquely identifies each block. The block itself is disseminated to validator processes using peer-to-peer gossiping protocol. It starts by having a proposer first splitting a block into a number of block parts, that are then gossiped between diff --git a/docs/tendermint-core/subscription.md b/docs/tendermint-core/subscription.md index 1ab6828b6..8b5303e87 100644 --- a/docs/tendermint-core/subscription.md +++ b/docs/tendermint-core/subscription.md @@ -43,7 +43,7 @@ transactions](../app-dev/indexing-transactions.md) for details. When validator set changes, ValidatorSetUpdates event is published. The event carries a list of pubkey/power pairs. The list is the same Tendermint receives from ABCI application (see [EndBlock -section](https://github.com/tendermint/spec/blob/master/spec/abci/abci.md#endblock) in +section](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/abci/abci.md#endblock) in the ABCI spec). Response: diff --git a/docs/tendermint-core/using-tendermint.md b/docs/tendermint-core/using-tendermint.md index 3910c3845..9d0eac250 100644 --- a/docs/tendermint-core/using-tendermint.md +++ b/docs/tendermint-core/using-tendermint.md @@ -49,7 +49,7 @@ definition](https://github.com/tendermint/tendermint/blob/master/types/genesis.g chain IDs, you will have a bad time. The ChainID must be less than 50 symbols. - `initial_height`: Height at which Tendermint should begin at. If a blockchain is conducting a network upgrade, starting from the stopped height brings uniqueness to previous heights. -- `consensus_params` [spec](https://github.com/tendermint/spec/blob/master/spec/core/state.md#consensusparams) +- `consensus_params` [spec](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/core/state.md#consensusparams) - `block` - `max_bytes`: Max block size, in bytes. - `max_gas`: Max gas per block. diff --git a/go.mod b/go.mod index 0579f11eb..23f180216 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/adlio/schema v1.3.3 github.com/btcsuite/btcd v0.22.1 github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce + github.com/bufbuild/buf v1.3.1 github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/creachadair/atomicfile v0.2.6 github.com/creachadair/taskgroup v0.3.2 @@ -47,9 +48,8 @@ require ( github.com/vektra/mockery/v2 v2.14.0 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 - golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 - google.golang.org/grpc v1.48.0 - gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f + google.golang.org/grpc v1.47.0 gotest.tools v2.2.0+incompatible // indirect pgregory.net/rapid v0.4.8 ) diff --git a/go.sum b/go.sum index 98f054355..affbe939c 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,8 @@ github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnO github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -179,6 +181,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/bufbuild/buf v1.3.1 h1:AelWcENnbNEjwxmQXIZaU51GHgnWQ8Mc94kZdDUKgRs= +github.com/bufbuild/buf v1.3.1/go.mod h1:CTRUb23N+zlm1U8ZIBKz0Sqluk++qQloB2i/MZNZHIs= github.com/butuzov/ireturn v0.1.1 h1:QvrO2QF2+/Cx1WA/vETCIYBKtRjc30vesdoPUNo1EbY= github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= @@ -231,10 +235,12 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/atomicfile v0.2.6 h1:FgYxYvGcqREApTY8Nxg8msM6P/KVKK3ob5h9FaRUTNg= github.com/creachadair/atomicfile v0.2.6/go.mod h1:BRq8Une6ckFneYXZQ+kO7p1ZZP3I2fzVzf28JxrIkBc= @@ -384,6 +390,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= @@ -401,6 +409,7 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -617,11 +626,17 @@ github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a h1:d4+I1YEKVmWZrgkt6jpXBnLgV2ZjO0YxEtLDdfIZfH4= +github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jgautheron/goconst v1.5.1 h1:HxVbL1MhydKs8R8n/HE5NPvzfaYmQJA3o879lE4+WcM= github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jhump/protocompile v0.0.0-20220216033700-d705409f108f h1:BNuUg9k2EiJmlMwjoef3e8vZLHplbVw6DrjGFjLL+Yo= +github.com/jhump/protocompile v0.0.0-20220216033700-d705409f108f/go.mod h1:qr2b5kx4HbFS7/g4uYO5qv9ei8303JMsC7ESbYiqr2Q= github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= +github.com/jhump/protoreflect v1.11.1-0.20220213155251-0c2aedc66cf4 h1:E2CdxLXYSn6Zrj2+u8DWrwMJW3YZLSWtM/7kIL8OL18= +github.com/jhump/protoreflect v1.11.1-0.20220213155251-0c2aedc66cf4/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= @@ -664,6 +679,10 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= +github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -815,6 +834,7 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oasisprotocol/curve25519-voi v0.0.0-20210609091139-0a56a4bca00b h1:MKwruh+HeCSKWphkxuzvRzU4QzDkg7yiPkDVV0cDFgI= github.com/oasisprotocol/curve25519-voi v0.0.0-20210609091139-0a56a4bca00b/go.mod h1:TLJifjWF6eotcfzDjKZsDqWJ+73Uvj/N85MvVyrvynM= +github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= @@ -864,6 +884,7 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9 github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= @@ -880,12 +901,16 @@ github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7 github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -958,8 +983,10 @@ github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.2.3 h1:ww2fsjqocGCAFamzvv/b8IsRduuHHeK2MHTcTxZTQX8= github.com/ryancurrah/gomodguard v1.2.3/go.mod h1:rYbA/4Tg5c54mV1sv4sQTP5WOPBcoLtnBZ7/TEhXAbg= @@ -1165,6 +1192,7 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1175,18 +1203,24 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1362,8 +1396,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1445,6 +1480,7 @@ golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1789,8 +1825,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1804,6 +1840,7 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/internal/evidence/doc.go b/internal/evidence/doc.go index d521debd3..294c354bf 100644 --- a/internal/evidence/doc.go +++ b/internal/evidence/doc.go @@ -1,7 +1,7 @@ /* Package evidence handles all evidence storage and gossiping from detection to block proposal. For the different types of evidence refer to the `evidence.go` file in the types package -or https://github.com/tendermint/spec/blob/master/spec/consensus/light-client/accountability.md. +or https://github.com/tendermint/tendermint/blob/v0.35.x/spec/consensus/light-client/accountability.md. Gossiping diff --git a/ivy-proofs/Dockerfile b/ivy-proofs/Dockerfile new file mode 100644 index 000000000..be60151fd --- /dev/null +++ b/ivy-proofs/Dockerfile @@ -0,0 +1,37 @@ +# we need python2 support, which was dropped after buster: +FROM debian:buster + +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections +RUN apt-get update +RUN apt-get install -y apt-utils + +# Install and configure locale `en_US.UTF-8` +RUN apt-get install -y locales && \ + sed -i -e "s/# $en_US.*/en_US.UTF-8 UTF-8/" /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales && \ + update-locale LANG=en_US.UTF-8 +ENV LANG=en_US.UTF-8 + +RUN apt-get update +RUN apt-get install -y git python2 python-pip g++ cmake python-ply python-tk tix pkg-config libssl-dev python-setuptools + +# create a user: +RUN useradd -ms /bin/bash user +USER user +WORKDIR /home/user + +RUN git clone --recurse-submodules https://github.com/kenmcmil/ivy.git +WORKDIR /home/user/ivy/ +RUN git checkout 271ee38980699115508eb90a0dd01deeb750a94b + +RUN python2.7 build_submodules.py +RUN mkdir -p "/home/user/python/lib/python2.7/site-packages" +ENV PYTHONPATH="/home/user/python/lib/python2.7/site-packages" +# need to install pyparsing manually because otherwise wrong version found +RUN pip install pyparsing +RUN python2.7 setup.py install --prefix="/home/user/python/" +ENV PATH=$PATH:"/home/user/python/bin/" +WORKDIR /home/user/tendermint-proof/ + +ENTRYPOINT ["/home/user/tendermint-proof/check_proofs.sh"] + diff --git a/ivy-proofs/README.md b/ivy-proofs/README.md new file mode 100644 index 000000000..00a4bed25 --- /dev/null +++ b/ivy-proofs/README.md @@ -0,0 +1,33 @@ +# Ivy Proofs + +```copyright +Copyright (c) 2020 Galois, Inc. +SPDX-License-Identifier: Apache-2.0 +``` + +## Contents + +This folder contains: + +* `tendermint.ivy`, a specification of Tendermint algorithm as described in *The latest gossip on BFT consensus* by E. Buchman, J. Kwon, Z. Milosevic. +* `abstract_tendermint.ivy`, a more abstract specification of Tendermint that is more verification-friendly. +* `classic_safety.ivy`, a proof that Tendermint satisfies the classic safety property of BFT consensus: if every two quorums have a well-behaved node in common, then no two well-behaved nodes ever disagree. +* `accountable_safety_1.ivy`, a proof that, assuming every quorum contains at least one well-behaved node, if two well-behaved nodes disagree, then there is evidence demonstrating at least f+1 nodes misbehaved. +* `accountable_safety_2.ivy`, a proof that, regardless of any assumption about quorums, well-behaved nodes cannot be framed by malicious nodes. In other words, malicious nodes can never construct evidence that incriminates a well-behaved node. +* `network_shim.ivy`, the network model and a convenience `shim` object to interface with the Tendermint specification. +* `domain_model.ivy`, a specification of the domain model underlying the Tendermint specification, i.e. rounds, value, quorums, etc. + +All specifications and proofs are written in [Ivy](https://github.com/kenmcmil/ivy). + +The license above applies to all files in this folder. + + +## Building and running + +The easiest way to check the proofs is to use [Docker](https://www.docker.com/). + +1. Install [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/). +2. Build a Docker image: `docker-compose build` +3. Run the proofs inside the Docker container: `docker-compose run +tendermint-proof`. This will check all the proofs with the `ivy_check` +command and write the output of `ivy_check` to a subdirectory of `./output/' diff --git a/ivy-proofs/abstract_tendermint.ivy b/ivy-proofs/abstract_tendermint.ivy new file mode 100644 index 000000000..4a160be2a --- /dev/null +++ b/ivy-proofs/abstract_tendermint.ivy @@ -0,0 +1,178 @@ +#lang ivy1.7 +# --- +# layout: page +# title: Abstract specification of Tendermint in Ivy +# --- + +# Here we define an abstract version of the Tendermint specification. We use +# two main forms of abstraction: a) We abstract over how information is +# transmitted (there is no network). b) We abstract functions using relations. +# For example, we abstract over a node's current round, instead only tracking +# with a relation which rounds the node has left. We do something similar for +# the `lockedRound` variable. This is in order to avoid using a function from +# node to round, and it allows us to emit verification conditions that are +# efficiently solvable by Z3. + +# This specification also defines the observations that are used to adjudicate +# misbehavior. Well-behaved nodes faithfully observe every message that they +# use to take a step, while Byzantine nodes can fake observations about +# themselves (including withholding observations). Misbehavior is defined using +# the collection of all observations made (in reality, those observations must +# be collected first, but we do not model this process). + +include domain_model + +module abstract_tendermint = { + +# Protocol state +# ############## + + relation left_round(N:node, R:round) + relation prevoted(N:node, R:round, V:value) + relation precommitted(N:node, R:round, V:value) + relation decided(N:node, R:round, V:value) + relation locked(N:node, R:round, V:value) + +# Accountability relations +# ######################## + + relation observed_prevoted(N:node, R:round, V:value) + relation observed_precommitted(N:node, R:round, V:value) + +# relations that are defined in terms of the previous two: + relation observed_equivocation(N:node) + relation observed_unlawful_prevote(N:node) + relation agreement + relation accountability_violation + + object defs = { # we hide those definitions and use them only when needed + private { + definition [observed_equivocation_def] observed_equivocation(N) = exists V1,V2,R . + V1 ~= V2 & (observed_precommitted(N,R,V1) & observed_precommitted(N,R,V2) | observed_prevoted(N,R,V1) & observed_prevoted(N,R,V2)) + + definition [observed_unlawful_prevote_def] observed_unlawful_prevote(N) = exists V1,V2,R1,R2 . + V1 ~= value.nil & V2 ~= value.nil & V1 ~= V2 & R1 < R2 & observed_precommitted(N,R1,V1) & observed_prevoted(N,R2,V2) + & forall Q,R . R1 <= R & R < R2 & nset.is_quorum(Q) -> exists N2 . nset.member(N2,Q) & ~observed_prevoted(N2,R,V2) + + definition [agreement_def] agreement = forall N1,N2,R1,R2,V1,V2 . well_behaved(N1) & well_behaved(N2) & decided(N1,R1,V1) & decided(N2,R2,V2) -> V1 = V2 + + definition [accountability_violation_def] accountability_violation = exists Q1,Q2 . nset.is_quorum(Q1) & nset.is_quorum(Q2) & (forall N . nset.member(N,Q1) & nset.member(N,Q2) -> observed_equivocation(N) | observed_unlawful_prevote(N)) + } + } + +# Protocol transitions +# #################### + + after init { + left_round(N,R) := R < 0; + prevoted(N,R,V) := false; + precommitted(N,R,V) := false; + decided(N,R,V) := false; + locked(N,R,V) := false; + + observed_prevoted(N,R,V) := false; + observed_precommitted(N,R,V) := false; + } + +# Actions are named after the corresponding line numbers in the Tendermint +# arXiv paper. + + action l_11(n:node, r:round) = { # start round r + require ~left_round(n,r); + left_round(n,R) := R < r; + } + + action l_22(n:node, rp:round, v:value) = { + require ~left_round(n,rp); + require ~prevoted(n,rp,V) & ~precommitted(n,rp,V); + require (forall R,V . locked(n,R,V) -> V = v) | v = value.nil; + prevoted(n, rp, v) := true; + left_round(n, R) := R < rp; # leave all lower rounds. + + observed_prevoted(n, rp, v) := observed_prevoted(n, rp, v) | well_behaved(n); # the node observes itself + } + + action l_28(n:node, rp:round, v:value, vr:round, q:nset) = { + require ~left_round(n,rp) & ~prevoted(n,rp,V); + require ~prevoted(n,rp,V) & ~precommitted(n,rp,V); + require vr < rp; + require nset.is_quorum(q) & (forall N . nset.member(N,q) -> (prevoted(N,vr,v) | ~well_behaved(N))); + var proposal:value; + if value.valid(v) & ((forall R0,V0 . locked(n,R0,V0) -> R0 <= vr) | (forall R,V . locked(n,R,V) -> V = v)) { + proposal := v; + } + else { + proposal := value.nil; + }; + prevoted(n, rp, proposal) := true; + left_round(n, R) := R < rp; # leave all lower rounds + + observed_prevoted(N, vr, v) := observed_prevoted(N, vr, v) | (well_behaved(n) & nset.member(N,q)); # the node observes the prevotes of quorum q + observed_prevoted(n, rp, proposal) := observed_prevoted(n, rp, proposal) | well_behaved(n); # the node observes itself + } + + action l_36(n:node, rp:round, v:value, q:nset) = { + require v ~= value.nil; + require ~left_round(n,rp); + require exists V . prevoted(n,rp,V); + require ~precommitted(n,rp,V); + require nset.is_quorum(q) & (forall N . nset.member(N,q) -> (prevoted(N,rp,v) | ~well_behaved(N))); + precommitted(n, rp, v) := true; + left_round(n, R) := R < rp; # leave all lower rounds + locked(n,R,V) := R <= rp & V = v; + + observed_prevoted(N, rp, v) := observed_prevoted(N, rp, v) | (well_behaved(n) & nset.member(N,q)); # the node observes the prevotes of quorum q + observed_precommitted(n, rp, v) := observed_precommitted(n, rp, v) | well_behaved(n); # the node observes itself + } + + action l_44(n:node, rp:round, q:nset) = { + require ~left_round(n,rp); + require ~precommitted(n,rp,V); + require nset.is_quorum(q) & (forall N .nset.member(N,q) -> (prevoted(N,rp,value.nil) | ~well_behaved(N))); + precommitted(n, rp, value.nil) := true; + left_round(n, R) := R < rp; # leave all lower rounds + + observed_prevoted(N, rp, value.nil) := observed_prevoted(N, rp, value.nil) | (well_behaved(n) & nset.member(N,q)); # the node observes the prevotes of quorum q + observed_precommitted(n, rp, value.nil) := observed_precommitted(n, rp, value.nil) | well_behaved(n); # the node observes itself + } + + action l_57(n:node, rp:round) = { + require ~left_round(n,rp); + require ~prevoted(n,rp,V); + prevoted(n, rp, value.nil) := true; + left_round(n, R) := R < rp; # leave all lower rounds + + observed_prevoted(n, rp, value.nil) := observed_prevoted(n, rp, value.nil) | well_behaved(n); # the node observes itself + } + + action l_61(n:node, rp:round) = { + require ~left_round(n,rp); + require ~precommitted(n,rp,V); + precommitted(n, rp, value.nil) := true; + left_round(n, R) := R < rp; # leave all lower rounds + + observed_precommitted(n, rp, value.nil) := observed_precommitted(n, rp, value.nil) | well_behaved(n); # the node observes itself + } + + action decide(n:node, r:round, v:value, q:nset) = { + require v ~= value.nil; + require nset.is_quorum(q) & (forall N . nset.member(N, q) -> (precommitted(N, r, v) | ~well_behaved(N))); + decided(n, r, v) := true; + + observed_precommitted(N, r, v) := observed_precommitted(N, r, v) | (well_behaved(n) & nset.member(N,q)); # the node observes the precommits of quorum q + + } + + action misbehave = { +# Byzantine nodes can claim they observed whatever they want about themselves, +# but they cannot remove observations. Note that we use assume because we don't +# want those to be checked; we just want them to be true (that's the model of +# Byzantine behavior). + observed_prevoted(N,R,V) := *; + assume (old observed_prevoted(N,R,V)) -> observed_prevoted(N,R,V); + assume well_behaved(N) -> old observed_prevoted(N,R,V) = observed_prevoted(N,R,V); + observed_precommitted(N,R,V) := *; + assume (old observed_precommitted(N,R,V)) -> observed_precommitted(N,R,V); + assume well_behaved(N) -> old observed_precommitted(N,R,V) = observed_precommitted(N,R,V); + } +} diff --git a/ivy-proofs/accountable_safety_1.ivy b/ivy-proofs/accountable_safety_1.ivy new file mode 100644 index 000000000..02bdf1add --- /dev/null +++ b/ivy-proofs/accountable_safety_1.ivy @@ -0,0 +1,143 @@ +#lang ivy1.7 +# --- +# layout: page +# title: Proof of Classic Safety +# --- + +include tendermint +include abstract_tendermint + +# Here we prove the first accountability property: if two well-behaved nodes +# disagree, then there are two quorums Q1 and Q2 such that all members of the +# intersection of Q1 and Q2 have violated the accountability properties. + +# The proof is done in two steps: first we prove the abstract specification +# satisfies the property, and then we show by refinement that this property +# also holds in the concrete specification. + +# To see what is checked in the refinement proof, use `ivy_show isolate=accountable_safety_1 accountable_safety_1.ivy` +# To see what is checked in the abstract correctness proof, use `ivy_show isolate=abstract_accountable_safety_1 accountable_safety_1.ivy` +# To check the whole proof, use `ivy_check accountable_safety_1.ivy`. + + +# Proof of the accountability property in the abstract specification +# ================================================================== + +# We prove with tactics (see `lemma_1` and `lemma_2`) that, if some basic +# invariants hold (see `invs` below), then the accountability property holds. + +isolate abstract_accountable_safety = { + + instantiate abstract_tendermint + +# The main property +# ----------------- + +# If there is disagreement, then there is evidence that a third of the nodes +# have violated the protocol: + invariant [accountability] agreement | accountability_violation + proof { + apply lemma_1.thm # this reduces to goal to three subgoals: p1, p2, and p3 (see their definition below) + proof [p1] { + assume invs.inv1 + } + proof [p2] { + assume invs.inv2 + } + proof [p3] { + assume invs.inv3 + } + } + +# The invariants +# -------------- + + isolate invs = { + + # well-behaved nodes observe their own actions faithfully: + invariant [inv1] well_behaved(N) -> (observed_precommitted(N,R,V) = precommitted(N,R,V)) + # if a value is precommitted by a well-behaved node, then a quorum is observed to prevote it: + invariant [inv2] (exists N . well_behaved(N) & precommitted(N,R,V)) & V ~= value.nil -> exists Q . nset.is_quorum(Q) & forall N2 . nset.member(N2,Q) -> observed_prevoted(N2,R,V) + # if a value is decided by a well-behaved node, then a quorum is observed to precommit it: + invariant [inv3] (exists N . well_behaved(N) & decided(N,R,V)) -> 0 <= R & V ~= value.nil & exists Q . nset.is_quorum(Q) & forall N2 . nset.member(N2,Q) -> observed_precommitted(N2,R,V) + private { + invariant (precommitted(N,R,V) | prevoted(N,R,V)) -> 0 <= R + invariant R < 0 -> left_round(N,R) + } + + } with this, nset, round, accountable_bft.max_2f_byzantine + +# The theorems proved with tactics +# -------------------------------- + +# Using complete induction on rounds, we prove that, assuming that the +# invariants inv1, inv2, and inv3 hold, the accountability property holds. + +# For technical reasons, we separate the proof in two steps + isolate lemma_1 = { + + specification { + theorem [thm] { + property [p1] forall N,R,V . well_behaved(N) -> (observed_precommitted(N,R,V) = precommitted(N,R,V)) + property [p2] forall R,V . (exists N . well_behaved(N) & precommitted(N,R,V)) & V ~= value.nil -> exists Q . nset.is_quorum(Q) & forall N2 . nset.member(N2,Q) -> observed_prevoted(N2,R,V) + property [p3] forall R,V. (exists N . well_behaved(N) & decided(N,R,V)) -> 0 <= R & V ~= value.nil & exists Q . nset.is_quorum(Q) & forall N2 . nset.member(N2,Q) -> observed_precommitted(N2,R,V) + #------------------------------------------------------------------------------------------------------------------------------------------- + property agreement | accountability_violation + } + proof { + assume inductive_property # the theorem follows from what we prove by induction below + } + } + + implementation { + # complete induction is not built-in, so we introduce it with an axiom. Note that this only holds for a type where 0 is the smallest element + axiom [complete_induction] { + relation p(X:round) + { # base case + property p(0) + } + { # inductive step: show that if the property is true for all X lower or equal to x and y=x+1, then the property is true of y + individual a:round + individual b:round + property (forall X. 0 <= X & X <= a -> p(X)) & round.succ(a,b) -> p(b) + } + #-------------------------- + property forall X . 0 <= X -> p(X) + } + + # The main lemma: if inv1 and inv2 below hold and a quorum is observed to + # precommit V1 at R1 and another quorum is observed to precommit V2~=V1 at + # R2>=R1, then the intersection of two quorums (i.e. f+1 nodes) is observed to + # violate the protocol. We prove this by complete induction on R2. + theorem [inductive_property] { + property [p1] forall N,R,V . well_behaved(N) -> (observed_precommitted(N,R,V) = precommitted(N,R,V)) + property [p2] forall R,V . (exists N . well_behaved(N) & precommitted(N,R,V)) -> V = value.nil | exists Q . nset.is_quorum(Q) & forall N2 . nset.member(N2,Q) -> observed_prevoted(N2,R,V) + #----------------------------------------------------------------------------------------------------------------------- + property forall R2. 0 <= R2 -> ((exists V2,Q1,R1,V1,Q1 . V1 ~= value.nil & V2 ~= value.nil & V1 ~= V2 & 0 <= R1 & R1 <= R2 & nset.is_quorum(Q1) & (forall N . nset.member(N,Q1) -> observed_precommitted(N,R1,V1)) & (exists Q2 . nset.is_quorum(Q2) & forall N . nset.member(N,Q2) -> observed_prevoted(N,R2,V2))) -> accountability_violation) + } + proof { + apply complete_induction # the two subgoals (base case and inductive case) are then discharged automatically + # NOTE: this can take a long time depending on the SMT random seed (to try a different seed, use `ivy_check seed=$RANDOM` + } + } + } with this, round, nset, accountable_bft.max_2f_byzantine, defs.observed_equivocation_def, defs.observed_unlawful_prevote_def, defs.accountability_violation_def, defs.agreement_def + +} with round + +# The final proof +# =============== + +isolate accountable_safety_1 = { + +# First we instantiate the concrete protocol: + instantiate tendermint(abstract_accountable_safety) + +# We then define what we mean by agreement + relation agreement + definition [agreement_def] agreement = forall N1,N2. well_behaved(N1) & well_behaved(N2) & server.decision(N1) ~= value.nil & server.decision(N2) ~= value.nil -> server.decision(N1) = server.decision(N2) + + invariant abstract_accountable_safety.agreement -> agreement + + invariant [accountability] agreement | abstract_accountable_safety.accountability_violation + +} with value, round, proposers, shim, abstract_accountable_safety, abstract_accountable_safety.defs.agreement_def, accountable_safety_1.agreement_def diff --git a/ivy-proofs/accountable_safety_2.ivy b/ivy-proofs/accountable_safety_2.ivy new file mode 100644 index 000000000..7fb928909 --- /dev/null +++ b/ivy-proofs/accountable_safety_2.ivy @@ -0,0 +1,52 @@ +#lang ivy1.7 + +include tendermint +include abstract_tendermint + +# Here we prove the second accountability property: no well-behaved node is +# ever observed to violate the accountability properties. + +# The proof is done in two steps: first we prove the the abstract specification +# satisfies the property, and then we show by refinement that this property +# also holds in the concrete specification. + +# To see what is checked in the refinement proof, use `ivy_show isolate=accountable_safety_2 accountable_safety_2.ivy` +# To see what is checked in the abstract correctness proof, use `ivy_show isolate=abstract_accountable_safety_2 accountable_safety_2.ivy` +# To check the whole proof, use `ivy_check complete=fo accountable_safety_2.ivy`. + +# Proof that the property holds in the abstract specification +# ============================================================ + +isolate abstract_accountable_safety_2 = { + + instantiate abstract_tendermint + +# the main property: + invariant [wb_never_punished] well_behaved(N) -> ~(observed_equivocation(N) | observed_unlawful_prevote(N)) + +# the main invariant for proving wb_not_punished: + invariant well_behaved(N) & precommitted(N,R,V) & ~locked(N,R,V) & V ~= value.nil -> exists R2,V2 . V2 ~= value.nil & R < R2 & precommitted(N,R2,V2) & locked(N,R2,V2) + + invariant (exists N . well_behaved(N) & precommitted(N,R,V) & V ~= value.nil) -> exists Q . nset.is_quorum(Q) & forall N . nset.member(N,Q) -> observed_prevoted(N,R,V) + + invariant well_behaved(N) -> (observed_prevoted(N,R,V) <-> prevoted(N,R,V)) + invariant well_behaved(N) -> (observed_precommitted(N,R,V) <-> precommitted(N,R,V)) + +# nodes stop prevoting or precommitting in lower rounds when doing so in a higher round: + invariant well_behaved(N) & prevoted(N,R2,V2) & R1 < R2 -> left_round(N,R1) + invariant well_behaved(N) & locked(N,R2,V2) & R1 < R2 -> left_round(N,R1) + + invariant [precommit_unique_per_round] well_behaved(N) & precommitted(N,R,V1) & precommitted(N,R,V2) -> V1 = V2 + +} with nset, round, abstract_accountable_safety_2.defs.observed_equivocation_def, abstract_accountable_safety_2.defs.observed_unlawful_prevote_def + +# Proof that the property holds in the concrete specification +# =========================================================== + +isolate accountable_safety_2 = { + + instantiate tendermint(abstract_accountable_safety_2) + + invariant well_behaved(N) -> ~(abstract_accountable_safety_2.observed_equivocation(N) | abstract_accountable_safety_2.observed_unlawful_prevote(N)) + +} with round, value, shim, abstract_accountable_safety_2, abstract_accountable_safety_2.defs.observed_equivocation_def, abstract_accountable_safety_2.defs.observed_unlawful_prevote_def diff --git a/ivy-proofs/check_proofs.sh b/ivy-proofs/check_proofs.sh new file mode 100755 index 000000000..6afd1a962 --- /dev/null +++ b/ivy-proofs/check_proofs.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# returns non-zero error code if any proof fails + +success=0 +log_dir=$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 6) +cmd="ivy_check seed=$RANDOM" +mkdir -p output/$log_dir + +echo "Checking classic safety:" +res=$($cmd classic_safety.ivy | tee "output/$log_dir/classic_safety.txt" | tail -n 1) +if [ "$res" = "OK" ]; then + echo "OK" +else + echo "FAILED" + success=1 +fi + +echo "Checking accountable safety 1:" +res=$($cmd accountable_safety_1.ivy | tee "output/$log_dir/accountable_safety_1.txt" | tail -n 1) +if [ "$res" = "OK" ]; then + echo "OK" +else + echo "FAILED" + success=1 +fi + +echo "Checking accountable safety 2:" +res=$($cmd complete=fo accountable_safety_2.ivy | tee "output/$log_dir/accountable_safety_2.txt" | tail -n 1) +if [ "$res" = "OK" ]; then + echo "OK" +else + echo "FAILED" + success=1 +fi + +echo +echo "See ivy_check output in the output/ folder" +exit $success diff --git a/ivy-proofs/classic_safety.ivy b/ivy-proofs/classic_safety.ivy new file mode 100644 index 000000000..b422a2c17 --- /dev/null +++ b/ivy-proofs/classic_safety.ivy @@ -0,0 +1,85 @@ +#lang ivy1.7 +# --- +# layout: page +# title: Proof of Classic Safety +# --- + +include tendermint +include abstract_tendermint + +# Here we prove the classic safety property: assuming that every two quorums +# have a well-behaved node in common, no two well-behaved nodes ever disagree. + +# The proof is done in two steps: first we prove the the abstract specification +# satisfies the property, and then we show by refinement that this property +# also holds in the concrete specification. + +# To see what is checked in the refinement proof, use `ivy_show isolate=classic_safety classic_safety.ivy` +# To see what is checked in the abstract correctness proof, use `ivy_show isolate=abstract_classic_safety classic_safety.ivy` + +# To check the whole proof, use `ivy_check classic_safety.ivy`. + +# Note that all the verification conditions sent to Z3 for this proof are in +# EPR. + +# Classic safety in the abstract model +# ==================================== + +# We start by proving that classic safety holds in the abstract model. + +isolate abstract_classic_safety = { + + instantiate abstract_tendermint + + invariant [classic_safety] classic_bft.quorum_intersection & decided(N1,R1,V1) & decided(N2,R2,V2) -> V1 = V2 + +# The notion of choosable value +# ----------------------------- + + relation choosable(R:round, V:value) + definition choosable(R,V) = exists Q . nset.is_quorum(Q) & forall N . well_behaved(N) & nset.member(N,Q) -> ~left_round(N,R) | precommitted(N,R,V) + +# Main invariants +# --------------- + +# `classic_safety` is inductive relative to those invariants + + invariant [decision_is_quorum_precommit] (exists N1 . decided(N1,R,V)) -> exists Q. nset.is_quorum(Q) & forall N2. well_behaved(N2) & nset.member(N2, Q) -> precommitted(N2,R,V) + + invariant [precommitted_is_quorum_prevote] V ~= value.nil & (exists N1 . precommitted(N1,R,V)) -> exists Q. nset.is_quorum(Q) & forall N2. well_behaved(N2) & nset.member(N2, Q) -> prevoted(N2,R,V) + + invariant [prevote_unique_per_round] prevoted(N,R,V1) & prevoted(N,R,V2) -> V1 = V2 + +# This is the core invariant: as long as a precommitted value is still choosable, it remains protected by a lock and prevents any new value from being prevoted: + invariant [locks] classic_bft.quorum_intersection & V ~= value.nil & precommitted(N,R,V) & choosable(R,V) -> locked(N,R,V) & forall R2,V2 . R < R2 & prevoted(N,R2,V2) -> V2 = V | V2 = value.nil + +# Supporting invariants +# --------------------- + +# The main invariants are inductive relative to those + + invariant decided(N,R,V) -> V ~= value.nil + + invariant left_round(N,R2) & R1 < R2 -> left_round(N,R1) # if a node left round R2>R1, then it also left R1: + + invariant prevoted(N,R2,V2) & R1 < R2 -> left_round(N,R1) + invariant precommitted(N,R2,V2) & R1 < R2 -> left_round(N,R1) + +} with round, nset, classic_bft.quorum_intersection_def + +# The refinement proof +# ==================== + +# Now, thanks to the refinement relation that we establish in +# `concrete_tendermint.ivy`, we prove that classic safety transfers to the +# concrete specification: +isolate classic_safety = { + + # We instantiate the `tendermint` module providing `abstract_classic_safety` as abstract model. + instantiate tendermint(abstract_classic_safety) + + # We prove that if every two quorums have a well-behaved node in common, + # then well-behaved nodes never disagree: + invariant [classic_safety] classic_bft.quorum_intersection & server.decision(N1) ~= value.nil & server.decision(N2) ~= value.nil -> server.decision(N1) = server.decision(N2) + +} with value, round, proposers, shim, abstract_classic_safety # here we list all the specifications that we rely on for this proof diff --git a/ivy-proofs/count_lines.sh b/ivy-proofs/count_lines.sh new file mode 100755 index 000000000..b2c457e21 --- /dev/null +++ b/ivy-proofs/count_lines.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +r='^\s*$\|^\s*\#\|^\s*\}\s*$\|^\s*{\s*$' # removes comments and blank lines and lines that contain only { or } +N1=`cat tendermint.ivy domain_model.ivy network_shim.ivy | grep -v $r'\|.*invariant.*' | wc -l` +N2=`cat abstract_tendermint.ivy | grep "observed_" | wc -l` # the observed_* variables specify the observations of the nodes +SPEC_LINES=`expr $N1 + $N2` +echo "spec lines: $SPEC_LINES" +N3=`cat abstract_tendermint.ivy | grep -v $r'\|.*observed_.*' | wc -l` +N4=`cat accountable_safety_1.ivy | grep -v $r | wc -l` +PROOF_LINES=`expr $N3 + $N4` +echo "proof lines: $PROOF_LINES" +RATIO=`bc <<< "scale=2;$PROOF_LINES / $SPEC_LINES"` +echo "proof-to-code ratio for the accountable-safety property: $RATIO" diff --git a/ivy-proofs/docker-compose.yml b/ivy-proofs/docker-compose.yml new file mode 100644 index 000000000..1d4a8ffe1 --- /dev/null +++ b/ivy-proofs/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + tendermint-proof: + build: . + volumes: + - ./:/home/user/tendermint-proof:ro + - ./output:/home/user/tendermint-proof/output:rw + diff --git a/ivy-proofs/domain_model.ivy b/ivy-proofs/domain_model.ivy new file mode 100644 index 000000000..0f12f7288 --- /dev/null +++ b/ivy-proofs/domain_model.ivy @@ -0,0 +1,143 @@ +#lang ivy1.7 + +include order # this is a file from the standard library (`ivy/ivy/include/1.7/order.ivy`) + +isolate round = { + type this + individual minus_one:this + relation succ(R1:round, R2:round) + action incr(i:this) returns (j:this) + specification { +# to simplify verification, we treat rounds as an abstract totally ordered set with a successor relation. + instantiate totally_ordered(this) + property minus_one < 0 + property succ(X,Z) -> (X < Z & ~(X < Y & Y < Z)) + after incr { + ensure succ(i,j) + } + } + implementation { +# here we prove that the abstraction is sound. + interpret this -> int # rounds are integers in the Tendermint specification. + definition minus_one = 0-1 + definition succ(R1,R2) = R2 = R1 + 1 + implement incr { + j := i+1; + } + } +} + +instance node : iterable # nodes are a set with an order, that can be iterated over (see order.ivy in the standard library) + +relation well_behaved(N:node) # whether a node is well-behaved or not. NOTE: Used only in the proof and the Byzantine model; Nodes do know know who is well-behaved and who is not. + +isolate proposers = { + # each round has a unique proposer in Tendermint. In order to avoid a + # function from round to node (which makes verification more difficult), we + # abstract over this function using a relation. + relation is_proposer(N:node, R:round) + export action get_proposer(r:round) returns (n:node) + specification { + property is_proposer(N1,R) & is_proposer(N2,R) -> N1 = N2 + after get_proposer { + ensure is_proposer(n,r); + } + } + implementation { + function f(R:round):node + definition f(r:round) = <<>> + definition is_proposer(N,R) = N = f(R) + implement get_proposer { + n := f(r); + } + } +} + +isolate value = { # the type of values + type this + relation valid(V:value) + individual nil:value + specification { + property ~valid(nil) + } + implementation { + interpret value -> bv[2] + definition nil = <<< -1 >>> # let's say nil is -1 + definition valid(V) = V ~= nil + } +} + +object nset = { # the type of node sets + type this # a set of N=3f+i nodes for 0 + #include + namespace hash_space { + template + class hash > { + public: + size_t operator()(const std::set &s) const { + hash h; + size_t res = 0; + for (const T &e : s) + res += h(e); + return res; + } + }; + } + >>> + interpret nset -> <<< std::set<`node`> >>> + definition member(n:node, s:nset) = <<< `s`.find(`n`) != `s`.end() >>> + definition is_quorum(s:nset) = <<< 3*`s`.size() > 2*`node.size` >>> + definition is_blocking(s:nset) = <<< 3*`s`.size() > `node.size` >>> + implement empty { + <<< + >>> + } + implement insert { + <<< + `t` = `s`; + `t`.insert(`n`); + >>> + } + <<< encode `nset` + + std::ostream &operator <<(std::ostream &s, const `nset` &a) { + s << "{"; + for (auto iter = a.begin(); iter != a.end(); iter++) { + if (iter != a.begin()) s << ", "; + s << *iter; + } + s << "}"; + return s; + } + + template <> + `nset` _arg<`nset`>(std::vector &args, unsigned idx, long long bound) { + throw std::invalid_argument("Not implemented"); // no syntax for nset values in the REPL + } + + >>> + } +} + +object classic_bft = { + relation quorum_intersection + private { + definition [quorum_intersection_def] quorum_intersection = forall Q1,Q2. exists N. well_behaved(N) & nset.member(N, Q1) & nset.member(N, Q2) # every two quorums have a well-behaved node in common + } +} + +trusted isolate accountable_bft = { + # this is our baseline assumption about quorums: + private { + property [max_2f_byzantine] exists N . well_behaved(N) & nset.member(N,Q) # every quorum has a well-behaved member + } +} diff --git a/ivy-proofs/network_shim.ivy b/ivy-proofs/network_shim.ivy new file mode 100644 index 000000000..ebc3a04fc --- /dev/null +++ b/ivy-proofs/network_shim.ivy @@ -0,0 +1,133 @@ +#lang ivy1.7 +# --- +# layout: page +# title: Network model and network shim +# --- + +# Here we define a network module, which is our model of the network, and a +# shim module that sits on top of the network and which, upon receiving a +# message, calls the appropriate protocol handler. + +include domain_model + +# Here we define an enumeration type for identifying the 3 different types of +# messages that nodes send. +object msg_kind = { # TODO: merge with step_t + type this = {proposal, prevote, precommit} +} + +# Here we define the type of messages `msg`. Its members are structs with the fields described below. +object msg = { + type this = struct { + m_kind : msg_kind, + m_src : node, + m_round : round, + m_value : value, + m_vround : round + } +} + +# This is our model of the network: +isolate net = { + + export action recv(dst:node,v:msg) + action send(src:node,dst:node,v:msg) + # Note that the `recv` action is exported, meaning that it can be called + # non-deterministically by the environment any time it is enabled. In other + # words, a packet that is in flight can be received at any time. In this + # sense, the network is fully asynchronous. Moreover, there is no + # requirement that a given message will be received at all. + + # The state of the network consists of all the packets that have been + # sent so far, along with their destination. + relation sent(V:msg, N:node) + + after init { + sent(V, N) := false + } + + before send { + sent(v,dst) := true + } + + before recv { + require sent(v,dst) # only sent messages can be received. + } +} + +# The network shim sits on top of the network and, upon receiving a message, +# calls the appropriate protocol handler. It also exposes a `broadcast` action +# that sends to all nodes. + +isolate shim = { + + # In order not repeat the same code for each handler, we use a handler + # module parameterized by the type of message it will handle. Below we + # instantiate this module for the 3 types of messages of Tendermint + module handler(p_kind) = { + action handle(dst:node,m:msg) + object spec = { + before handle { + assert sent(m,dst) & m.m_kind = p_kind + } + } + } + + instance proposal_handler : handler(msg_kind.proposal) + instance prevote_handler : handler(msg_kind.prevote) + instance precommit_handler : handler(msg_kind.precommit) + + relation sent(M:msg,N:node) + + action broadcast(src:node,m:msg) + action send(src:node,dst:node,m:msg) + + specification { + after init { + sent(M,D) := false; + } + before broadcast { + sent(m,D) := true + } + before send { + sent(m,dst) := true + } + } + + # Here we give an implementation of it that satisfies its specification: + implementation { + + implement net.recv(dst:node,m:msg) { + + if m.m_kind = msg_kind.proposal { + call proposal_handler.handle(dst,m) + } + else if m.m_kind = msg_kind.prevote { + call prevote_handler.handle(dst,m) + } + else if m.m_kind = msg_kind.precommit { + call precommit_handler.handle(dst,m) + } + } + + implement broadcast { # broadcast sends to all nodes, including the sender. + var iter := node.iter.create(0); + while ~iter.is_end + invariant net.sent(M,D) -> sent(M,D) + { + var n := iter.val; + call net.send(src,n,m); + iter := iter.next; + } + } + + implement send { + call net.send(src,dst,m) + } + + private { + invariant net.sent(M,D) -> sent(M,D) + } + } + +} with net, node # to prove that the shim implementation satisfies the shim specification, we rely on the specification of net and node. diff --git a/ivy-proofs/output/.gitignore b/ivy-proofs/output/.gitignore new file mode 100644 index 000000000..5e7d2734c --- /dev/null +++ b/ivy-proofs/output/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/ivy-proofs/tendermint.ivy b/ivy-proofs/tendermint.ivy new file mode 100644 index 000000000..b7678bef9 --- /dev/null +++ b/ivy-proofs/tendermint.ivy @@ -0,0 +1,420 @@ +#lang ivy1.7 +# --- +# layout: page +# title: Specification of Tendermint in Ivy +# --- + +# This specification closely follows the pseudo-code given in "The latest +# gossip on BFT consensus" by E. Buchman, J. Kwon, Z. Milosevic +# + +include domain_model +include network_shim + +# We model the Tendermint protocol as an Ivy object. Like in Object-Oriented +# Programming, the basic structuring unit in Ivy is the object. Objects have +# internal state and actions (i.e. methods in OO parlance) that modify their +# state. We model Tendermint as an object whose actions represent steps taken +# by individual nodes in the protocol. Actions in Ivy can have preconditions, +# and a valid execution is a sequence of actions whose preconditions are all +# satisfied in the state in which they are called. + +# For technical reasons, we define below a `tendermint` module instead of an +# object. Ivy modules are a little bit like classes in OO programs, and like +# classes they can be instantiated to obtain objects. To instantiate the +# `tendermint` module, we must provide an abstract-protocol object. This allows +# us to use different abstract-protocol objects for different parts of the +# proof, and to do so without too much notational burden (we could have used +# Ivy monitors, but then we would need to prefix every variable name by the +# name of the object containing it, which clutters things a bit compared to the +# approach we took). + +# The abstract-protocol object is called by the resulting tendermint object so +# as to run the abstract protocol alongside the concrete protocol. This allows +# us to transfer properties proved of the abstract protocol to the concrete +# protocol, as follows. First, we prove that running the abstract protocol in +# this way results in a valid execution of the abstract protocol. This is done +# by checking that all preconditions of the abstract actions are satisfied at +# their call sites. Second, we establish a relation between abstract state and +# concrete state (in the form of invariants of the resulting, two-object +# transition system) that allow us to transfer properties proved in the +# abstract protocol to the concrete protocol (for example, we prove that any +# decision made in the Tendermint protocol is also made in the abstract +# protocol; if the abstract protocol satisfies the agreement property, this +# allows us to conclude that the Tendermint protocol also does). + +# The abstract protocol object that we will use is always the same, and only +# the abstract properties that we prove about it change in the different +# instantiations of the `tendermint` module. Thus we provide common invariants +# that a) allow to prove that the abstract preconditions are met, and b) +# provide a refinement relation (see end of the module) relating the state of +# Tendermint to the state of the abstract protocol. + +# In the model, Byzantine nodes can send whatever messages they want, except +# that they cannot forge sender identities. This reflects the fact that, in +# practice, nodes use public key cryptography to sign their messages. + +# Finally, note that the observations that serve to adjudicate misbehavior are +# defined only in the abstract protocol (they happen in the abstract actions). + +module tendermint(abstract_protocol) = { + + # the initial value of a node: + function init_val(N:node): value + + # the three type of steps + object step_t = { + type this = {propose, prevote, precommit} + } # refer to those e.g. as step_t.propose + + object server(n:node) = { + + # the current round of a node + individual round_p: round + + individual step: step_t + + individual decision: value + + individual lockedValue: value + individual lockedRound: round + + individual validValue: value + individual validRound: round + + + relation done_l34(R:round) + relation done_l36(R:round, V:value) + relation done_l47(R:round) + + # variables for scheduling request + relation propose_timer_scheduled(R:round) + relation prevote_timer_scheduled(R:round) + relation precommit_timer_scheduled(R:round) + + relation _recved_proposal(Sender:node, R:round, V:value, VR:round) + relation _recved_prevote(Sender:node, R:round, V:value) + relation _recved_precommit(Sender:node, R:round, V:value) + + relation _has_started + + after init { + round_p := 0; + step := step_t.propose; + decision := value.nil; + + lockedValue := value.nil; + lockedRound := round.minus_one; + + validValue := value.nil; + validRound := round.minus_one; + + done_l34(R) := false; + done_l36(R, V) := false; + done_l47(R) := false; + + propose_timer_scheduled(R) := false; + prevote_timer_scheduled(R) := false; + precommit_timer_scheduled(R) := false; + + _recved_proposal(Sender, R, V, VR) := false; + _recved_prevote(Sender, R, V) := false; + _recved_precommit(Sender, R, V) := false; + + _has_started := false; + } + + action getValue returns (v:value) = { + v := init_val(n) + } + + export action start = { + require ~_has_started; + _has_started := true; + # line 10 + call startRound(0); + } + + # line 11-21 + action startRound(r:round) = { + # line 12 + round_p := r; + + # line 13 + step := step_t.propose; + + var proposal : value; + + # line 14 + if (proposers.get_proposer(r) = n) { + if validValue ~= value.nil { # line 15 + proposal := validValue; # line 16 + } else { + proposal := getValue(); # line 18 + }; + call broadcast_proposal(r, proposal, validRound); # line 19 + } else { + propose_timer_scheduled(r) := true; # line 21 + }; + + call abstract_protocol.l_11(n, r); + } + + # This action, as not exported, can only be called at specific call sites. + action broadcast_proposal(r:round, v:value, vr:round) = { + var m: msg; + m.m_kind := msg_kind.proposal; + m.m_src := n; + m.m_round := r; + m.m_value := v; + m.m_vround := vr; + call shim.broadcast(n,m); + } + + implement shim.proposal_handler.handle(msg:msg) { + _recved_proposal(msg.m_src, msg.m_round, msg.m_value, msg.m_vround) := true; + } + + # line 22-27 + export action l_22(v:value) = { + require _has_started; + require _recved_proposal(proposers.get_proposer(round_p), round_p, v, round.minus_one); + require step = step_t.propose; + + if (value.valid(v) & (lockedRound = round.minus_one | lockedValue = v)) { + call broadcast_prevote(round_p, v); # line 24 + call abstract_protocol.l_22(n, round_p, v); + } else { + call broadcast_prevote(round_p, value.nil); # line 26 + call abstract_protocol.l_22(n, round_p, value.nil); + }; + + # line 27 + step := step_t.prevote; + } + + # line 28-33 + export action l_28(r:round, v:value, vr:round, q:nset) = { + require _has_started; + require r = round_p; + require _recved_proposal(proposers.get_proposer(r), r, v, vr); + require nset.is_quorum(q); + require nset.member(N,q) -> _recved_prevote(N,vr,v); + require step = step_t.propose; + require vr >= 0 & vr < r; + + # line 29 + if (value.valid(v) & (lockedRound <= vr | lockedValue = v)) { + call broadcast_prevote(r, v); + } else { + call broadcast_prevote(r, value.nil); + }; + + call abstract_protocol.l_28(n,r,v,vr,q); + step := step_t.prevote; + } + + action broadcast_prevote(r:round, v:value) = { + var m: msg; + m.m_kind := msg_kind.prevote; + m.m_src := n; + m.m_round := r; + m.m_value := v; + call shim.broadcast(n,m); + } + + implement shim.prevote_handler.handle(msg:msg) { + _recved_prevote(msg.m_src, msg.m_round, msg.m_value) := true; + } + + # line 34-35 + export action l_34(r:round, q:nset) = { + require _has_started; + require round_p = r; + require nset.is_quorum(q); + require exists V . nset.member(N,q) -> _recved_prevote(N,r,V); + require step = step_t.prevote; + require ~done_l34(r); + done_l34(r) := true; + + prevote_timer_scheduled(r) := true; + } + + + # line 36-43 + export action l_36(r:round, v:value, q:nset) = { + require _has_started; + require r = round_p; + require exists VR . round.minus_one <= VR & VR < r & _recved_proposal(proposers.get_proposer(r), r, v, VR); + require nset.is_quorum(q); + require nset.member(N,q) -> _recved_prevote(N,r,v); + require value.valid(v); + require step = step_t.prevote | step = step_t.precommit; + + require ~done_l36(r,v); + done_l36(r, v) := true; + + if step = step_t.prevote { + lockedValue := v; # line 38 + lockedRound := r; # line 39 + call broadcast_precommit(r, v); # line 40 + step := step_t.precommit; # line 41 + call abstract_protocol.l_36(n, r, v, q); + }; + + validValue := v; # line 42 + validRound := r; # line 43 + } + + # line 44-46 + export action l_44(r:round, q:nset) = { + require _has_started; + require r = round_p; + require nset.is_quorum(q); + require nset.member(N,q) -> _recved_prevote(N,r,value.nil); + require step = step_t.prevote; + + call broadcast_precommit(r, value.nil); # line 45 + step := step_t.precommit; # line 46 + + call abstract_protocol.l_44(n, r, q); + } + + action broadcast_precommit(r:round, v:value) = { + var m: msg; + m.m_kind := msg_kind.precommit; + m.m_src := n; + m.m_round := r; + m.m_value := v; + call shim.broadcast(n,m); + } + + implement shim.precommit_handler.handle(msg:msg) { + _recved_precommit(msg.m_src, msg.m_round, msg.m_value) := true; + } + + + # line 47-48 + export action l_47(r:round, q:nset) = { + require _has_started; + require round_p = r; + require nset.is_quorum(q); + require nset.member(N,q) -> exists V . _recved_precommit(N,r,V); + require ~done_l47(r); + done_l47(r) := true; + + precommit_timer_scheduled(r) := true; + } + + + # line 49-54 + export action l_49_decide(r:round, v:value, q:nset) = { + require _has_started; + require exists VR . round.minus_one <= VR & VR < r & _recved_proposal(proposers.get_proposer(r), r, v, VR); + require nset.is_quorum(q); + require nset.member(N,q) -> _recved_precommit(N,r,v); + require decision = value.nil; + + if value.valid(v) { + decision := v; + # MORE for next height + call abstract_protocol.decide(n, r, v, q); + } + } + + # line 55-56 + export action l_55(r:round, b:nset) = { + require _has_started; + require nset.is_blocking(b); + require nset.member(N,b) -> exists VR . round.minus_one <= VR & VR < r & exists V . _recved_proposal(N,r,V,VR) | _recved_prevote(N,r,V) | _recved_precommit(N,r,V); + require r > round_p; + call startRound(r); # line 56 + } + + # line 57-60 + export action onTimeoutPropose(r:round) = { + require _has_started; + require propose_timer_scheduled(r); + require r = round_p; + require step = step_t.propose; + call broadcast_prevote(r,value.nil); + step := step_t.prevote; + + call abstract_protocol.l_57(n,r); + + propose_timer_scheduled(r) := false; + } + + # line 61-64 + export action onTimeoutPrevote(r:round) = { + require _has_started; + require prevote_timer_scheduled(r); + require r = round_p; + require step = step_t.prevote; + call broadcast_precommit(r,value.nil); + step := step_t.precommit; + + call abstract_protocol.l_61(n,r); + + prevote_timer_scheduled(r) := false; + } + + # line 65-67 + export action onTimeoutPrecommit(r:round) = { + require _has_started; + require precommit_timer_scheduled(r); + require r = round_p; + call startRound(round.incr(r)); + + precommit_timer_scheduled(r) := false; + } + +# The Byzantine actions +# --------------------- + +# Byzantine nodes can send whatever they want, but they cannot send +# messages on behalf of well-behaved nodes. In practice this is implemented +# using cryptography (e.g. public-key cryptography). + + export action byzantine_send(m:msg, dst:node) = { + require ~well_behaved(n); + require ~well_behaved(m.m_src); # cannot forge the identity of well-behaved nodes + call shim.send(n,dst,m); + } + +# Byzantine nodes can also report fake observations, as defined in the abstract protocol. + export action fake_observations = { + call abstract_protocol.misbehave + } + +# Invariants +# ---------- + +# We provide common invariants that a) allow to prove that the abstract +# preconditions are met, and b) provide a refinement relation. + + + specification { + + invariant 0 <= round_p + invariant abstract_protocol.left_round(n,R) <-> R < round_p + + invariant lockedRound ~= round.minus_one -> forall R,V . abstract_protocol.locked(n,R,V) <-> R <= lockedRound & lockedValue = V + invariant lockedRound = round.minus_one -> forall R,V . ~abstract_protocol.locked(n,R,V) + + invariant forall M:msg . well_behaved(M.m_src) & M.m_kind = msg_kind.prevote & shim.sent(M,N) -> abstract_protocol.prevoted(M.m_src,M.m_round,M.m_value) + invariant well_behaved(N) & _recved_prevote(N,R,V) -> abstract_protocol.prevoted(N,R,V) + invariant forall M:msg . well_behaved(M.m_src) & M.m_kind = msg_kind.precommit & shim.sent(M,N) -> abstract_protocol.precommitted(M.m_src,M.m_round,M.m_value) + invariant well_behaved(N) & _recved_precommit(N,R,V) -> abstract_protocol.precommitted(N,R,V) + + invariant (step = step_t.prevote | step = step_t.propose) -> ~abstract_protocol.precommitted(n,round_p,V) + invariant step = step_t.propose -> ~abstract_protocol.prevoted(n,round_p,V) + invariant step = step_t.prevote -> exists V . abstract_protocol.prevoted(n,round_p,V) + + invariant round_p < R -> ~(abstract_protocol.prevoted(n,R,V) | abstract_protocol.precommitted(n,R,V)) + invariant ~_has_started -> step = step_t.propose & ~(abstract_protocol.prevoted(n,R,V) | abstract_protocol.precommitted(n,R,V)) & round_p = 0 + + invariant decision ~= value.nil -> exists R . abstract_protocol.decided(n,R,decision) + } + } +} diff --git a/ivy-proofs/tendermint_test.ivy b/ivy-proofs/tendermint_test.ivy new file mode 100644 index 000000000..1299fc086 --- /dev/null +++ b/ivy-proofs/tendermint_test.ivy @@ -0,0 +1,127 @@ +#lang ivy1.7 + +include tendermint +include abstract_tendermint + +isolate ghost_ = { + instantiate abstract_tendermint +} + +isolate protocol = { + instantiate tendermint(ghost_) # here we instantiate the parameter of the tendermint module with `ghost_`; however note that we don't extract any code for `ghost_` (it's not in the list of object in the extract, and it's thus sliced away). + implementation { + definition init_val(n:node) = <<< `n`%2 >>> + } + # attribute test = impl +} with ghost_, shim, value, round, proposers + +# Here we run a simple scenario that exhibits an execution in which nodes make +# a decision. We do this to rule out trivial modeling errors. + +# One option to check that this scenario is valid is to run it in Ivy's REPL. +# For this, first compile the scenario: +#```ivyc target=repl isolate=code trace=true tendermint_test.ivy +# Then, run the produced binary (e.g. for 4 nodes): +#``` ./tendermint_test 4 +# Finally, call the action: +#``` scenarios.scenario_1 +# Note that Ivy will check at runtime that all action preconditions are +# satisfied. For example, runing the scenario twice will cause a violation of +# the precondition of the `start` action, because a node cannot start twice +# (see `require ~_has_started` in action `start`). + +# Another possibility would be to run `ivy_check` on the scenario, but that +# does not seem to work at the moment. + +isolate scenarios = { + individual all:nset # will be used as parameter to actions requiring a quorum + + after init { + var iter := node.iter.create(0); + while ~iter.is_end + { + all := all.insert(iter.val); + iter := iter.next; + }; + assert nset.is_quorum(all); # we can also use asserts to make sure we are getting what we expect + } + + export action scenario_1 = { + # all nodes start: + var iter := node.iter.create(0); + while ~iter.is_end + { + call protocol.server.start(iter.val); + iter := iter.next; + }; + # all nodes receive the leader's proposal: + var m:msg; + m.m_kind := msg_kind.proposal; + m.m_src := 0; + m.m_round := 0; + m.m_value := 0; + m.m_vround := round.minus_one; + iter := node.iter.create(0); + while ~iter.is_end + { + call net.recv(iter.val,m); + iter := iter.next; + }; + # all nodes prevote: + iter := node.iter.create(0); + while ~iter.is_end + { + call protocol.server.l_22(iter.val,0); + iter := iter.next; + }; + # all nodes receive each other's prevote messages; + m.m_kind := msg_kind.prevote; + m.m_vround := 0; + iter := node.iter.create(0); + while ~iter.is_end + { + var iter2 := node.iter.create(0); # the sender + while ~iter2.is_end + { + m.m_src := iter2.val; + call net.recv(iter.val,m); + iter2 := iter2.next; + }; + iter := iter.next; + }; + # all nodes precommit: + iter := node.iter.create(0); + while ~iter.is_end + { + call protocol.server.l_36(iter.val,0,0,all); + iter := iter.next; + }; + # all nodes receive each other's pre-commits + m.m_kind := msg_kind.precommit; + iter := node.iter.create(0); + while ~iter.is_end + { + var iter2 := node.iter.create(0); # the sender + while ~iter2.is_end + { + m.m_src := iter2.val; + call net.recv(iter.val,m); + iter2 := iter2.next; + }; + iter := iter.next; + }; + # now all nodes can decide: + iter := node.iter.create(0); + while ~iter.is_end + { + call protocol.server.l_49_decide(iter.val,0,0,all); + iter := iter.next; + }; + } + + # TODO: add more scenarios + +} with round, node, proposers, value, nset, protocol, shim, net + +# extract code = protocol, shim, round, node +extract code = round, node, proposers, value, nset, protocol, shim, net, scenarios diff --git a/light/client.go b/light/client.go index 32d4c669b..983ce3209 100644 --- a/light/client.go +++ b/light/client.go @@ -428,7 +428,7 @@ func (c *Client) VerifyLightBlockAtHeight(ctx context.Context, height int64, now // headers are not adjacent, verifySkipping is performed and necessary (not all) // intermediate headers will be requested. See the specification for details. // Intermediate headers are not saved to database. -// https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md +// https://github.com/tendermint/tendermint/blob/v0.35.x/spec/consensus/light-client.md // // If the header, which is older than the currently trusted header, is // requested and the light client does not have it, VerifyHeader will perform: diff --git a/light/doc.go b/light/doc.go index c30c68eb0..853a50b3a 100644 --- a/light/doc.go +++ b/light/doc.go @@ -94,7 +94,7 @@ Check out other examples in example_test.go ## 2. Pure functions to verify a new header (see verifier.go) Verify function verifies a new header against some trusted header. See -https://github.com/tendermint/spec/blob/master/spec/light-client/verification/README.md +https://github.com/tendermint/tendermint/blob/v0.35.x/spec/light-client/verification/README.md for details. There are two methods of verification: sequential and bisection diff --git a/proto/Dockerfile b/proto/Dockerfile new file mode 100644 index 000000000..92fff39e6 --- /dev/null +++ b/proto/Dockerfile @@ -0,0 +1,20 @@ +# This Dockerfile defines an image containing tools for linting, formatting, +# and compiling the Tendermint protos. +FROM golang:1.17-alpine + +# Install a commonly used set of programs for use with our protos. +# clang-extra-tools is included here because it provides clang-format, +# used to format the .proto files. +RUN apk add --no-cache build-base clang-extra-tools curl git + +ENV GOLANG_PROTOBUF_VERSION=1.3.1 \ + GOGO_PROTOBUF_VERSION=1.3.2 + +# Retrieve the go protoc programs and copy them into the PATH +RUN go install github.com/golang/protobuf/protoc-gen-go@v${GOLANG_PROTOBUF_VERSION} && \ + go install github.com/gogo/protobuf/protoc-gen-gogo@v${GOGO_PROTOBUF_VERSION} && \ + go install github.com/gogo/protobuf/protoc-gen-gogofaster@v${GOGO_PROTOBUF_VERSION} && \ + mv "$(go env GOPATH)"/bin/* /usr/local/bin/ + +# Copy the 'buf' program out of the buildbuf/buf container. +COPY --from=bufbuild/buf:latest /usr/local/bin/* /usr/local/bin/ diff --git a/proto/README.md b/proto/README.md new file mode 100644 index 000000000..a0701d3bc --- /dev/null +++ b/proto/README.md @@ -0,0 +1,21 @@ +# Protocol Buffers + +This sections defines the protocol buffers used in Tendermint. This is split into two directories: `spec`, the types required for all implementations and `tendermint`, a set of types internal to the Go implementation. All generated go code is also stored in `tendermint`. +More descriptions of the data structures are located in the spec directory as follows: + +- [Block](../spec/core/data_structures.md) +- [ABCI](../spec/abci/README.md) +- [P2P](../spec/p2p/messages/README.md) + +## Process to generate protos + +The `.proto` files within this section are core to the protocol and updates must be treated as such. + +### Steps + +1. Make an issue with the proposed change. + - Within the issue members, from the Tendermint team will leave comments. If there is not consensus on the change an [RFC](../docs/rfc/README.md) may be requested. + 1a. Submission of an RFC as a pull request should be made to facilitate further discussion. + 1b. Merge the RFC. +2. Make the necessary changes to the `.proto` file(s), [core data structures](../spec/core/data_structures.md) and/or [ABCI protocol](../spec/abci/apps.md). +3. Rebuild the Go protocol buffers by running `make proto-gen`. Ensure that the project builds correctly by running `make build`. diff --git a/proto/buf.lock b/proto/buf.lock new file mode 100644 index 000000000..6b033f6f6 --- /dev/null +++ b/proto/buf.lock @@ -0,0 +1,7 @@ +# Generated by buf. DO NOT EDIT. +version: v1 +deps: + - remote: buf.build + owner: gogo + repository: protobuf + commit: 5461a3dfa9d941da82028ab185dc2a0e diff --git a/buf.yaml b/proto/buf.yaml similarity index 50% rename from buf.yaml rename to proto/buf.yaml index cc4aced57..816db10f7 100644 --- a/buf.yaml +++ b/proto/buf.yaml @@ -1,16 +1,11 @@ -version: v1beta1 - -build: - roots: - - proto - - third_party/proto +version: v1 +deps: + - buf.build/gogo/protobuf +breaking: + use: + - FILE lint: use: - BASIC - FILE_LOWER_SNAKE_CASE - UNARY_RPC - ignore: - - gogoproto -breaking: - use: - - FILE diff --git a/proto/tendermint/blocksync/types.pb.go b/proto/tendermint/blocksync/types.pb.go index fcbef7107..43ad139ec 100644 --- a/proto/tendermint/blocksync/types.pb.go +++ b/proto/tendermint/blocksync/types.pb.go @@ -899,7 +899,10 @@ func (m *BlockRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -968,7 +971,10 @@ func (m *NoBlockResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1054,7 +1060,10 @@ func (m *BlockResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1104,7 +1113,10 @@ func (m *StatusRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1192,7 +1204,10 @@ func (m *StatusResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1417,7 +1432,10 @@ func (m *Message) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/consensus/types.pb.go b/proto/tendermint/consensus/types.pb.go index 6372a88d4..67efd1c2c 100644 --- a/proto/tendermint/consensus/types.pb.go +++ b/proto/tendermint/consensus/types.pb.go @@ -1932,7 +1932,10 @@ func (m *NewRoundStep) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2109,7 +2112,10 @@ func (m *NewValidBlock) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2192,7 +2198,10 @@ func (m *Proposal) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2313,7 +2322,10 @@ func (m *ProposalPOL) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2434,7 +2446,10 @@ func (m *BlockPart) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2520,7 +2535,10 @@ func (m *Vote) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2646,7 +2664,10 @@ func (m *HasVote) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2786,7 +2807,10 @@ func (m *VoteSetMaj23) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2959,7 +2983,10 @@ func (m *VoteSetBits) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -3324,7 +3351,10 @@ func (m *Message) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/consensus/wal.pb.go b/proto/tendermint/consensus/wal.pb.go index fd80819cd..86ff1be01 100644 --- a/proto/tendermint/consensus/wal.pb.go +++ b/proto/tendermint/consensus/wal.pb.go @@ -921,7 +921,10 @@ func (m *MsgInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthWal + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthWal } if (iNdEx + skippy) > l { @@ -1061,7 +1064,10 @@ func (m *TimeoutInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthWal + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthWal } if (iNdEx + skippy) > l { @@ -1130,7 +1136,10 @@ func (m *EndHeight) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthWal + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthWal } if (iNdEx + skippy) > l { @@ -1320,7 +1329,10 @@ func (m *WALMessage) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthWal + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthWal } if (iNdEx + skippy) > l { @@ -1439,7 +1451,10 @@ func (m *TimedWALMessage) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthWal + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthWal } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/crypto/keys.pb.go b/proto/tendermint/crypto/keys.pb.go index 24c6c1b1b..8ff4c4a4f 100644 --- a/proto/tendermint/crypto/keys.pb.go +++ b/proto/tendermint/crypto/keys.pb.go @@ -687,7 +687,10 @@ func (m *PublicKey) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthKeys + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthKeys } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/crypto/proof.pb.go b/proto/tendermint/crypto/proof.pb.go index 82fb943fc..97350c64c 100644 --- a/proto/tendermint/crypto/proof.pb.go +++ b/proto/tendermint/crypto/proof.pb.go @@ -820,7 +820,10 @@ func (m *Proof) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthProof + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthProof } if (iNdEx + skippy) > l { @@ -940,7 +943,10 @@ func (m *ValueOp) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthProof + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthProof } if (iNdEx + skippy) > l { @@ -1086,7 +1092,10 @@ func (m *DominoOp) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthProof + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthProof } if (iNdEx + skippy) > l { @@ -1236,7 +1245,10 @@ func (m *ProofOp) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthProof + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthProof } if (iNdEx + skippy) > l { @@ -1320,7 +1332,10 @@ func (m *ProofOps) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthProof + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthProof } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/libs/bits/types.pb.go b/proto/tendermint/libs/bits/types.pb.go index c0ebcb976..ad87f854f 100644 --- a/proto/tendermint/libs/bits/types.pb.go +++ b/proto/tendermint/libs/bits/types.pb.go @@ -307,7 +307,10 @@ func (m *BitArray) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/mempool/types.pb.go b/proto/tendermint/mempool/types.pb.go index 11e259551..3487652bc 100644 --- a/proto/tendermint/mempool/types.pb.go +++ b/proto/tendermint/mempool/types.pb.go @@ -370,7 +370,10 @@ func (m *Txs) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -455,7 +458,10 @@ func (m *Message) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/p2p/conn.pb.go b/proto/tendermint/p2p/conn.pb.go index 47a3bb0cd..7c26d3fcd 100644 --- a/proto/tendermint/p2p/conn.pb.go +++ b/proto/tendermint/p2p/conn.pb.go @@ -723,7 +723,10 @@ func (m *PacketPing) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthConn + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthConn } if (iNdEx + skippy) > l { @@ -773,7 +776,10 @@ func (m *PacketPong) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthConn + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthConn } if (iNdEx + skippy) > l { @@ -896,7 +902,10 @@ func (m *PacketMsg) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthConn + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthConn } if (iNdEx + skippy) > l { @@ -1051,7 +1060,10 @@ func (m *Packet) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthConn + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthConn } if (iNdEx + skippy) > l { @@ -1168,7 +1180,10 @@ func (m *AuthSigMessage) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthConn + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthConn } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/p2p/pex.pb.go b/proto/tendermint/p2p/pex.pb.go index 63882c364..e7eab3845 100644 --- a/proto/tendermint/p2p/pex.pb.go +++ b/proto/tendermint/p2p/pex.pb.go @@ -1025,7 +1025,10 @@ func (m *PexAddress) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthPex + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthPex } if (iNdEx + skippy) > l { @@ -1075,7 +1078,10 @@ func (m *PexRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthPex + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthPex } if (iNdEx + skippy) > l { @@ -1159,7 +1165,10 @@ func (m *PexResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthPex + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthPex } if (iNdEx + skippy) > l { @@ -1241,7 +1250,10 @@ func (m *PexAddressV2) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthPex + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthPex } if (iNdEx + skippy) > l { @@ -1291,7 +1303,10 @@ func (m *PexRequestV2) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthPex + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthPex } if (iNdEx + skippy) > l { @@ -1375,7 +1390,10 @@ func (m *PexResponseV2) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthPex + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthPex } if (iNdEx + skippy) > l { @@ -1565,7 +1583,10 @@ func (m *PexMessage) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthPex + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthPex } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/p2p/types.pb.go b/proto/tendermint/p2p/types.pb.go index 7965b668b..c04436400 100644 --- a/proto/tendermint/p2p/types.pb.go +++ b/proto/tendermint/p2p/types.pb.go @@ -938,7 +938,10 @@ func (m *ProtocolVersion) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1248,7 +1251,10 @@ func (m *NodeInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1362,7 +1368,10 @@ func (m *NodeInfoOther) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1534,7 +1543,10 @@ func (m *PeerInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1707,7 +1719,10 @@ func (m *PeerAddressInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/privval/types.pb.go b/proto/tendermint/privval/types.pb.go index 56b35e727..da30f7527 100644 --- a/proto/tendermint/privval/types.pb.go +++ b/proto/tendermint/privval/types.pb.go @@ -1708,7 +1708,10 @@ func (m *RemoteSignerError) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1790,7 +1793,10 @@ func (m *PubKeyRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1909,7 +1915,10 @@ func (m *PubKeyResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2027,7 +2036,10 @@ func (m *SignVoteRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2146,7 +2158,10 @@ func (m *SignedVoteResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2264,7 +2279,10 @@ func (m *SignProposalRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2383,7 +2401,10 @@ func (m *SignedProposalResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2433,7 +2454,10 @@ func (m *PingRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2483,7 +2507,10 @@ func (m *PingResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2813,7 +2840,10 @@ func (m *Message) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2930,7 +2960,10 @@ func (m *AuthSigMessage) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/rpc/grpc/types.pb.go b/proto/tendermint/rpc/grpc/types.pb.go new file mode 100644 index 000000000..13b2e71a0 --- /dev/null +++ b/proto/tendermint/rpc/grpc/types.pb.go @@ -0,0 +1,936 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: tendermint/rpc/grpc/types.proto + +package coregrpc + +import ( + context "context" + fmt "fmt" + proto "github.com/gogo/protobuf/proto" + types "github.com/tendermint/tendermint/abci/types" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type RequestPing struct { +} + +func (m *RequestPing) Reset() { *m = RequestPing{} } +func (m *RequestPing) String() string { return proto.CompactTextString(m) } +func (*RequestPing) ProtoMessage() {} +func (*RequestPing) Descriptor() ([]byte, []int) { + return fileDescriptor_0ffff5682c662b95, []int{0} +} +func (m *RequestPing) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *RequestPing) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_RequestPing.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *RequestPing) XXX_Merge(src proto.Message) { + xxx_messageInfo_RequestPing.Merge(m, src) +} +func (m *RequestPing) XXX_Size() int { + return m.Size() +} +func (m *RequestPing) XXX_DiscardUnknown() { + xxx_messageInfo_RequestPing.DiscardUnknown(m) +} + +var xxx_messageInfo_RequestPing proto.InternalMessageInfo + +type RequestBroadcastTx struct { + Tx []byte `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` +} + +func (m *RequestBroadcastTx) Reset() { *m = RequestBroadcastTx{} } +func (m *RequestBroadcastTx) String() string { return proto.CompactTextString(m) } +func (*RequestBroadcastTx) ProtoMessage() {} +func (*RequestBroadcastTx) Descriptor() ([]byte, []int) { + return fileDescriptor_0ffff5682c662b95, []int{1} +} +func (m *RequestBroadcastTx) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *RequestBroadcastTx) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_RequestBroadcastTx.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *RequestBroadcastTx) XXX_Merge(src proto.Message) { + xxx_messageInfo_RequestBroadcastTx.Merge(m, src) +} +func (m *RequestBroadcastTx) XXX_Size() int { + return m.Size() +} +func (m *RequestBroadcastTx) XXX_DiscardUnknown() { + xxx_messageInfo_RequestBroadcastTx.DiscardUnknown(m) +} + +var xxx_messageInfo_RequestBroadcastTx proto.InternalMessageInfo + +func (m *RequestBroadcastTx) GetTx() []byte { + if m != nil { + return m.Tx + } + return nil +} + +type ResponsePing struct { +} + +func (m *ResponsePing) Reset() { *m = ResponsePing{} } +func (m *ResponsePing) String() string { return proto.CompactTextString(m) } +func (*ResponsePing) ProtoMessage() {} +func (*ResponsePing) Descriptor() ([]byte, []int) { + return fileDescriptor_0ffff5682c662b95, []int{2} +} +func (m *ResponsePing) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ResponsePing) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ResponsePing.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ResponsePing) XXX_Merge(src proto.Message) { + xxx_messageInfo_ResponsePing.Merge(m, src) +} +func (m *ResponsePing) XXX_Size() int { + return m.Size() +} +func (m *ResponsePing) XXX_DiscardUnknown() { + xxx_messageInfo_ResponsePing.DiscardUnknown(m) +} + +var xxx_messageInfo_ResponsePing proto.InternalMessageInfo + +type ResponseBroadcastTx struct { + CheckTx *types.ResponseCheckTx `protobuf:"bytes,1,opt,name=check_tx,json=checkTx,proto3" json:"check_tx,omitempty"` + DeliverTx *types.ResponseDeliverTx `protobuf:"bytes,2,opt,name=deliver_tx,json=deliverTx,proto3" json:"deliver_tx,omitempty"` +} + +func (m *ResponseBroadcastTx) Reset() { *m = ResponseBroadcastTx{} } +func (m *ResponseBroadcastTx) String() string { return proto.CompactTextString(m) } +func (*ResponseBroadcastTx) ProtoMessage() {} +func (*ResponseBroadcastTx) Descriptor() ([]byte, []int) { + return fileDescriptor_0ffff5682c662b95, []int{3} +} +func (m *ResponseBroadcastTx) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ResponseBroadcastTx) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ResponseBroadcastTx.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ResponseBroadcastTx) XXX_Merge(src proto.Message) { + xxx_messageInfo_ResponseBroadcastTx.Merge(m, src) +} +func (m *ResponseBroadcastTx) XXX_Size() int { + return m.Size() +} +func (m *ResponseBroadcastTx) XXX_DiscardUnknown() { + xxx_messageInfo_ResponseBroadcastTx.DiscardUnknown(m) +} + +var xxx_messageInfo_ResponseBroadcastTx proto.InternalMessageInfo + +func (m *ResponseBroadcastTx) GetCheckTx() *types.ResponseCheckTx { + if m != nil { + return m.CheckTx + } + return nil +} + +func (m *ResponseBroadcastTx) GetDeliverTx() *types.ResponseDeliverTx { + if m != nil { + return m.DeliverTx + } + return nil +} + +func init() { + proto.RegisterType((*RequestPing)(nil), "tendermint.rpc.grpc.RequestPing") + proto.RegisterType((*RequestBroadcastTx)(nil), "tendermint.rpc.grpc.RequestBroadcastTx") + proto.RegisterType((*ResponsePing)(nil), "tendermint.rpc.grpc.ResponsePing") + proto.RegisterType((*ResponseBroadcastTx)(nil), "tendermint.rpc.grpc.ResponseBroadcastTx") +} + +func init() { proto.RegisterFile("tendermint/rpc/grpc/types.proto", fileDescriptor_0ffff5682c662b95) } + +var fileDescriptor_0ffff5682c662b95 = []byte{ + // 316 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x2f, 0x49, 0xcd, 0x4b, + 0x49, 0x2d, 0xca, 0xcd, 0xcc, 0x2b, 0xd1, 0x2f, 0x2a, 0x48, 0xd6, 0x4f, 0x07, 0x11, 0x25, 0x95, + 0x05, 0xa9, 0xc5, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0xc2, 0x08, 0x05, 0x7a, 0x45, 0x05, + 0xc9, 0x7a, 0x20, 0x05, 0x52, 0xd2, 0x48, 0xba, 0x12, 0x93, 0x92, 0x33, 0x91, 0x75, 0x28, 0xf1, + 0x72, 0x71, 0x07, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x04, 0x64, 0xe6, 0xa5, 0x2b, 0xa9, 0x70, + 0x09, 0x41, 0xb9, 0x4e, 0x45, 0xf9, 0x89, 0x29, 0xc9, 0x89, 0xc5, 0x25, 0x21, 0x15, 0x42, 0x7c, + 0x5c, 0x4c, 0x25, 0x15, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x3c, 0x41, 0x4c, 0x25, 0x15, 0x4a, 0x7c, + 0x5c, 0x3c, 0x41, 0xa9, 0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x60, 0x5d, 0x53, 0x19, 0xb9, 0x84, + 0x61, 0x02, 0xc8, 0xfa, 0xac, 0xb9, 0x38, 0x92, 0x33, 0x52, 0x93, 0xb3, 0xe3, 0xa1, 0xba, 0xb9, + 0x8d, 0x14, 0xf4, 0x90, 0x5c, 0x08, 0x72, 0x8c, 0x1e, 0x4c, 0x9f, 0x33, 0x48, 0x61, 0x48, 0x45, + 0x10, 0x7b, 0x32, 0x84, 0x21, 0xe4, 0xc8, 0xc5, 0x95, 0x92, 0x9a, 0x93, 0x59, 0x96, 0x5a, 0x04, + 0xd2, 0xce, 0x04, 0xd6, 0xae, 0x84, 0x53, 0xbb, 0x0b, 0x44, 0x69, 0x48, 0x45, 0x10, 0x67, 0x0a, + 0x8c, 0x69, 0xb4, 0x97, 0x91, 0x8b, 0x07, 0xee, 0x1e, 0xc7, 0x00, 0x4f, 0x21, 0x6f, 0x2e, 0x16, + 0x90, 0x83, 0x85, 0x50, 0x9c, 0x01, 0x0b, 0x28, 0x3d, 0xa4, 0x80, 0x90, 0x52, 0xc4, 0xa1, 0x02, + 0xe1, 0x6b, 0xa1, 0x04, 0x2e, 0x6e, 0x64, 0xcf, 0xaa, 0xe3, 0x33, 0x13, 0x49, 0xa1, 0x94, 0x06, + 0x5e, 0xa3, 0x91, 0x54, 0x3a, 0xf9, 0x9c, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, + 0x47, 0x72, 0x8c, 0x13, 0x1e, 0xcb, 0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, + 0x94, 0x51, 0x7a, 0x66, 0x49, 0x46, 0x69, 0x92, 0x5e, 0x72, 0x7e, 0xae, 0x3e, 0x52, 0xf4, 0x62, + 0x49, 0x1f, 0xd6, 0xc9, 0xf9, 0x45, 0xa9, 0x20, 0x46, 0x12, 0x1b, 0x38, 0xc6, 0x8d, 0x01, 0x01, + 0x00, 0x00, 0xff, 0xff, 0xf6, 0x4b, 0x02, 0xd8, 0x46, 0x02, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// BroadcastAPIClient is the client API for BroadcastAPI service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type BroadcastAPIClient interface { + Ping(ctx context.Context, in *RequestPing, opts ...grpc.CallOption) (*ResponsePing, error) + BroadcastTx(ctx context.Context, in *RequestBroadcastTx, opts ...grpc.CallOption) (*ResponseBroadcastTx, error) +} + +type broadcastAPIClient struct { + cc *grpc.ClientConn +} + +func NewBroadcastAPIClient(cc *grpc.ClientConn) BroadcastAPIClient { + return &broadcastAPIClient{cc} +} + +func (c *broadcastAPIClient) Ping(ctx context.Context, in *RequestPing, opts ...grpc.CallOption) (*ResponsePing, error) { + out := new(ResponsePing) + err := c.cc.Invoke(ctx, "/tendermint.rpc.grpc.BroadcastAPI/Ping", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *broadcastAPIClient) BroadcastTx(ctx context.Context, in *RequestBroadcastTx, opts ...grpc.CallOption) (*ResponseBroadcastTx, error) { + out := new(ResponseBroadcastTx) + err := c.cc.Invoke(ctx, "/tendermint.rpc.grpc.BroadcastAPI/BroadcastTx", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// BroadcastAPIServer is the server API for BroadcastAPI service. +type BroadcastAPIServer interface { + Ping(context.Context, *RequestPing) (*ResponsePing, error) + BroadcastTx(context.Context, *RequestBroadcastTx) (*ResponseBroadcastTx, error) +} + +// UnimplementedBroadcastAPIServer can be embedded to have forward compatible implementations. +type UnimplementedBroadcastAPIServer struct { +} + +func (*UnimplementedBroadcastAPIServer) Ping(ctx context.Context, req *RequestPing) (*ResponsePing, error) { + return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") +} +func (*UnimplementedBroadcastAPIServer) BroadcastTx(ctx context.Context, req *RequestBroadcastTx) (*ResponseBroadcastTx, error) { + return nil, status.Errorf(codes.Unimplemented, "method BroadcastTx not implemented") +} + +func RegisterBroadcastAPIServer(s *grpc.Server, srv BroadcastAPIServer) { + s.RegisterService(&_BroadcastAPI_serviceDesc, srv) +} + +func _BroadcastAPI_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RequestPing) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BroadcastAPIServer).Ping(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tendermint.rpc.grpc.BroadcastAPI/Ping", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BroadcastAPIServer).Ping(ctx, req.(*RequestPing)) + } + return interceptor(ctx, in, info, handler) +} + +func _BroadcastAPI_BroadcastTx_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RequestBroadcastTx) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BroadcastAPIServer).BroadcastTx(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tendermint.rpc.grpc.BroadcastAPI/BroadcastTx", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BroadcastAPIServer).BroadcastTx(ctx, req.(*RequestBroadcastTx)) + } + return interceptor(ctx, in, info, handler) +} + +var _BroadcastAPI_serviceDesc = grpc.ServiceDesc{ + ServiceName: "tendermint.rpc.grpc.BroadcastAPI", + HandlerType: (*BroadcastAPIServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ping", + Handler: _BroadcastAPI_Ping_Handler, + }, + { + MethodName: "BroadcastTx", + Handler: _BroadcastAPI_BroadcastTx_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "tendermint/rpc/grpc/types.proto", +} + +func (m *RequestPing) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RequestPing) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *RequestPing) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *RequestBroadcastTx) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RequestBroadcastTx) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *RequestBroadcastTx) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Tx) > 0 { + i -= len(m.Tx) + copy(dAtA[i:], m.Tx) + i = encodeVarintTypes(dAtA, i, uint64(len(m.Tx))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *ResponsePing) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ResponsePing) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ResponsePing) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *ResponseBroadcastTx) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ResponseBroadcastTx) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ResponseBroadcastTx) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.DeliverTx != nil { + { + size, err := m.DeliverTx.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if m.CheckTx != nil { + { + size, err := m.CheckTx.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintTypes(dAtA []byte, offset int, v uint64) int { + offset -= sovTypes(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *RequestPing) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *RequestBroadcastTx) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Tx) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + return n +} + +func (m *ResponsePing) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *ResponseBroadcastTx) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.CheckTx != nil { + l = m.CheckTx.Size() + n += 1 + l + sovTypes(uint64(l)) + } + if m.DeliverTx != nil { + l = m.DeliverTx.Size() + n += 1 + l + sovTypes(uint64(l)) + } + return n +} + +func sovTypes(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozTypes(x uint64) (n int) { + return sovTypes(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *RequestPing) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RequestPing: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RequestPing: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RequestBroadcastTx) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RequestBroadcastTx: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RequestBroadcastTx: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Tx", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Tx = append(m.Tx[:0], dAtA[iNdEx:postIndex]...) + if m.Tx == nil { + m.Tx = []byte{} + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ResponsePing) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ResponsePing: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ResponsePing: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ResponseBroadcastTx) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ResponseBroadcastTx: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ResponseBroadcastTx: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CheckTx", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.CheckTx == nil { + m.CheckTx = &types.ResponseCheckTx{} + } + if err := m.CheckTx.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DeliverTx", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.DeliverTx == nil { + m.DeliverTx = &types.ResponseDeliverTx{} + } + if err := m.DeliverTx.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipTypes(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTypes + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTypes + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTypes + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthTypes + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupTypes + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthTypes + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthTypes = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowTypes = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupTypes = fmt.Errorf("proto: unexpected end of group") +) diff --git a/proto/tendermint/state/types.pb.go b/proto/tendermint/state/types.pb.go index 85f38cada..d94724fff 100644 --- a/proto/tendermint/state/types.pb.go +++ b/proto/tendermint/state/types.pb.go @@ -1069,7 +1069,10 @@ func (m *ABCIResponses) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1174,7 +1177,10 @@ func (m *ValidatorsInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1276,7 +1282,10 @@ func (m *ConsensusParamsInfo) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1391,7 +1400,10 @@ func (m *Version) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1857,7 +1869,10 @@ func (m *State) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/statesync/types.pb.go b/proto/tendermint/statesync/types.pb.go index 5541c2803..93e844730 100644 --- a/proto/tendermint/statesync/types.pb.go +++ b/proto/tendermint/statesync/types.pb.go @@ -1740,7 +1740,10 @@ func (m *Message) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1790,7 +1793,10 @@ func (m *SnapshotsRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -1965,7 +1971,10 @@ func (m *SnapshotsResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2072,7 +2081,10 @@ func (m *ChunkRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2233,7 +2245,10 @@ func (m *ChunkResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2302,7 +2317,10 @@ func (m *LightBlockRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2388,7 +2406,10 @@ func (m *LightBlockResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2457,7 +2478,10 @@ func (m *ParamsRequest) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2559,7 +2583,10 @@ func (m *ParamsResponse) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/statesync/types.proto b/proto/tendermint/statesync/types.proto index fcfd05f68..ac21b5d7b 100644 --- a/proto/tendermint/statesync/types.proto +++ b/proto/tendermint/statesync/types.proto @@ -59,4 +59,4 @@ message ParamsRequest { message ParamsResponse { uint64 height = 1; tendermint.types.ConsensusParams consensus_params = 2 [(gogoproto.nullable) = false]; -} \ No newline at end of file +} diff --git a/proto/tendermint/types/block.pb.go b/proto/tendermint/types/block.pb.go index f2077aad8..aacb90fab 100644 --- a/proto/tendermint/types/block.pb.go +++ b/proto/tendermint/types/block.pb.go @@ -389,7 +389,10 @@ func (m *Block) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthBlock + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthBlock } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/types/canonical.pb.go b/proto/tendermint/types/canonical.pb.go index 709831043..38b17ddb1 100644 --- a/proto/tendermint/types/canonical.pb.go +++ b/proto/tendermint/types/canonical.pb.go @@ -775,7 +775,10 @@ func (m *CanonicalBlockID) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthCanonical + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthCanonical } if (iNdEx + skippy) > l { @@ -878,7 +881,10 @@ func (m *CanonicalPartSetHeader) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthCanonical + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthCanonical } if (iNdEx + skippy) > l { @@ -1087,7 +1093,10 @@ func (m *CanonicalProposal) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthCanonical + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthCanonical } if (iNdEx + skippy) > l { @@ -1277,7 +1286,10 @@ func (m *CanonicalVote) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthCanonical + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthCanonical } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/types/events.pb.go b/proto/tendermint/types/events.pb.go index a9aa26a79..1c49aef64 100644 --- a/proto/tendermint/types/events.pb.go +++ b/proto/tendermint/types/events.pb.go @@ -285,7 +285,10 @@ func (m *EventDataRoundState) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthEvents + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthEvents } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/types/evidence.pb.go b/proto/tendermint/types/evidence.pb.go index daab3dc34..3d9e8f2c5 100644 --- a/proto/tendermint/types/evidence.pb.go +++ b/proto/tendermint/types/evidence.pb.go @@ -825,7 +825,10 @@ func (m *Evidence) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthEvidence + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthEvidence } if (iNdEx + skippy) > l { @@ -1018,7 +1021,10 @@ func (m *DuplicateVoteEvidence) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthEvidence + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthEvidence } if (iNdEx + skippy) > l { @@ -1209,7 +1215,10 @@ func (m *LightClientAttackEvidence) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthEvidence + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthEvidence } if (iNdEx + skippy) > l { @@ -1293,7 +1302,10 @@ func (m *EvidenceList) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthEvidence + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthEvidence } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/types/params.pb.go b/proto/tendermint/types/params.pb.go index 5a9f103a9..a295bca9e 100644 --- a/proto/tendermint/types/params.pb.go +++ b/proto/tendermint/types/params.pb.go @@ -1123,7 +1123,10 @@ func (m *ConsensusParams) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthParams + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthParams } if (iNdEx + skippy) > l { @@ -1211,7 +1214,10 @@ func (m *BlockParams) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthParams + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthParams } if (iNdEx + skippy) > l { @@ -1332,7 +1338,10 @@ func (m *EvidenceParams) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthParams + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthParams } if (iNdEx + skippy) > l { @@ -1414,7 +1423,10 @@ func (m *ValidatorParams) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthParams + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthParams } if (iNdEx + skippy) > l { @@ -1483,7 +1495,10 @@ func (m *VersionParams) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthParams + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthParams } if (iNdEx + skippy) > l { @@ -1571,7 +1586,10 @@ func (m *HashedParams) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthParams + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthParams } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/types/types.pb.go b/proto/tendermint/types/types.pb.go index 73090558e..27a936097 100644 --- a/proto/tendermint/types/types.pb.go +++ b/proto/tendermint/types/types.pb.go @@ -2267,7 +2267,10 @@ func (m *PartSetHeader) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2403,7 +2406,10 @@ func (m *Part) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -2520,7 +2526,10 @@ func (m *BlockID) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -3026,7 +3035,10 @@ func (m *Header) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -3108,7 +3120,10 @@ func (m *Data) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -3368,7 +3383,10 @@ func (m *Vote) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -3523,7 +3541,10 @@ func (m *Commit) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -3693,7 +3714,10 @@ func (m *CommitSig) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -3919,7 +3943,10 @@ func (m *Proposal) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -4041,7 +4068,10 @@ func (m *SignedHeader) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -4163,7 +4193,10 @@ func (m *LightBlock) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -4317,7 +4350,10 @@ func (m *BlockMeta) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { @@ -4471,7 +4507,10 @@ func (m *TxProof) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/types/validator.pb.go b/proto/tendermint/types/validator.pb.go index 23b30ed3c..2c3468b83 100644 --- a/proto/tendermint/types/validator.pb.go +++ b/proto/tendermint/types/validator.pb.go @@ -583,7 +583,10 @@ func (m *ValidatorSet) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthValidator + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthValidator } if (iNdEx + skippy) > l { @@ -738,7 +741,10 @@ func (m *Validator) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthValidator + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthValidator } if (iNdEx + skippy) > l { @@ -843,7 +849,10 @@ func (m *SimpleValidator) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthValidator + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthValidator } if (iNdEx + skippy) > l { diff --git a/proto/tendermint/version/types.pb.go b/proto/tendermint/version/types.pb.go index 6e224392e..9aeb3ae1a 100644 --- a/proto/tendermint/version/types.pb.go +++ b/proto/tendermint/version/types.pb.go @@ -265,7 +265,10 @@ func (m *Consensus) Unmarshal(dAtA []byte) error { if err != nil { return err } - if (skippy < 0) || (iNdEx+skippy) < 0 { + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) < 0 { return ErrInvalidLengthTypes } if (iNdEx + skippy) > l { diff --git a/scripts/protocgen.sh b/scripts/protocgen.sh deleted file mode 100755 index 51b1cc6d3..000000000 --- a/scripts/protocgen.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -eo pipefail - -buf generate --path proto/tendermint - -mv ./proto/tendermint/abci/types.pb.go ./abci/types - -mv ./proto/tendermint/rpc/grpc/types.pb.go ./rpc/grpc diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 000000000..8ca0f19e0 --- /dev/null +++ b/spec/README.md @@ -0,0 +1,93 @@ +# Tendermint Specifications + +This directory hosts the canonical Markdown specifications of the Tendermint Protocol. + +It shall be used to describe protocol semantics, namely the BFT consensus engine, leader election, block propagation and light client verification. The specification includes encoding descriptions used in interprocess communication to comply with the protocol. It defines the interface between the application and Tendermint. The english specifications are often accompanies with a TLA+ specification. + +## Contents + +- [Overview](#overview) + +### Data Structures + +- [Encoding and Digests](./core/encoding.md) +- [Blockchain](./core/data_structures.md) +- [State](./core/state.md) + +### Consensus Protocol + +- [Consensus Algorithm](./consensus/consensus.md) +- [Creating a proposal](./consensus/creating-proposal.md) +- [Time](./consensus/bft-time.md) +- [Light-Client](./consensus/light-client/README.md) + +### P2P and Network Protocols + +- [The Base P2P Layer](./p2p/node.md): multiplex the protocols ("reactors") on authenticated and encrypted TCP connections +- [Peer Exchange (PEX)](./p2p/messages/pex.md): gossip known peer addresses so peers can find each other +- [Block Sync](./p2p/messages/block-sync.md): gossip blocks so peers can catch up quickly +- [Consensus](./p2p/messages/consensus.md): gossip votes and block parts so new blocks can be committed +- [Mempool](./p2p/messages/mempool.md): gossip transactions so they get included in blocks +- [Evidence](./p2p/messages/evidence.md): sending invalid evidence will stop the peer + +### RPC + +- [RPC SPEC](./rpc/README.md): Specification of the Tendermint remote procedure call interface. + +### Software + +- [ABCI](./abci/README.md): Details about interactions between the + application and consensus engine over ABCI +- [Write-Ahead Log](./consensus/wal.md): Details about how the consensus + engine preserves data and recovers from crash failures + +## Contibuting + +Contributions are welcome. + +Proposals at an early stage can first be drafted as Github issues. To progress, a proposal will often need to be written out and approved as a [Request For Comment (RFC)](../docs/rfc/README.md). + +The standard language for coding blocks is Golang. + +If you find discrepancies between the spec and the code that +do not have an associated issue or pull request on github, +please submit them to our [bug bounty](https://tendermint.com/security)! + +## Overview + +Tendermint provides Byzantine Fault Tolerant State Machine Replication using +hash-linked batches of transactions. Such transaction batches are called "blocks". +Hence, Tendermint defines a "blockchain". + +Each block in Tendermint has a unique index - its Height. +Height's in the blockchain are monotonic. +Each block is committed by a known set of weighted Validators. +Membership and weighting within this validator set may change over time. +Tendermint guarantees the safety and liveness of the blockchain +so long as less than 1/3 of the total weight of the Validator set +is malicious or faulty. + +A commit in Tendermint is a set of signed messages from more than 2/3 of +the total weight of the current Validator set. Validators take turns proposing +blocks and voting on them. Once enough votes are received, the block is considered +committed. These votes are included in the _next_ block as proof that the previous block +was committed - they cannot be included in the current block, as that block has already been +created. + +Once a block is committed, it can be executed against an application. +The application returns results for each of the transactions in the block. +The application can also return changes to be made to the validator set, +as well as a cryptographic digest of its latest state. + +Tendermint is designed to enable efficient verification and authentication +of the latest state of the blockchain. To achieve this, it embeds +cryptographic commitments to certain information in the block "header". +This information includes the contents of the block (eg. the transactions), +the validator set committing the block, as well as the various results returned by the application. +Note, however, that block execution only occurs _after_ a block is committed. +Thus, application results can only be included in the _next_ block. + +Also note that information like the transaction results and the validator set are never +directly included in the block - only their cryptographic digests (Merkle roots) are. +Hence, verification of a block requires a separate data structure to store this information. +We call this the `State`. Block verification also requires access to the previous block. diff --git a/spec/abci/README.md b/spec/abci/README.md new file mode 100644 index 000000000..37bf02ba2 --- /dev/null +++ b/spec/abci/README.md @@ -0,0 +1,27 @@ +--- +order: 1 +parent: + title: ABCI + order: 2 +--- + +# ABCI + +ABCI stands for "**A**pplication **B**lock**c**hain **I**nterface". +ABCI is the interface between Tendermint (a state-machine replication engine) +and your application (the actual state machine). It consists of a set of +_methods_, each with a corresponding `Request` and `Response`message type. +To perform state-machine replication, Tendermint calls the ABCI methods on the +ABCI application by sending the `Request*` messages and receiving the `Response*` messages in return. + +All ABCI messages and methods are defined in [protocol buffers](https://github.com/tendermint/tendermint/blob/v0.35.x/proto/abci/types.proto). +This allows Tendermint to run with applications written in many programming languages. + +This specification is split as follows: + +- [Methods and Types](./abci.md) - complete details on all ABCI methods and + message types +- [Applications](./apps.md) - how to manage ABCI application state and other + details about building ABCI applications +- [Client and Server](./client-server.md) - for those looking to implement their + own ABCI application servers diff --git a/spec/abci/abci.md b/spec/abci/abci.md new file mode 100644 index 000000000..a7f9d386c --- /dev/null +++ b/spec/abci/abci.md @@ -0,0 +1,775 @@ +--- +order: 1 +title: Method and Types +--- + +# Methods and Types + +## Connections + +ABCI applications can run either within the _same_ process as the Tendermint +state-machine replication engine, or as a _separate_ process from the state-machine +replication engine. When run within the same process, Tendermint will call the ABCI +application methods directly as Go method calls. + +When Tendermint and the ABCI application are run as separate processes, Tendermint +opens four connections to the application for ABCI methods. The connections each +handle a subset of the ABCI method calls. These subsets are defined as follows: + +#### **Consensus** connection + +* Driven by a consensus protocol and is responsible for block execution. +* Handles the `InitChain`, `BeginBlock`, `DeliverTx`, `EndBlock`, and `Commit` method +calls. + +#### **Mempool** connection + +* For validating new transactions, before they're shared or included in a block. +* Handles the `CheckTx` calls. + +#### **Info** connection + +* For initialization and for queries from the user. +* Handles the `Info` and `Query` calls. + +#### **Snapshot** connection + +* For serving and restoring [state sync snapshots](apps.md#state-sync). +* Handles the `ListSnapshots`, `LoadSnapshotChunk`, `OfferSnapshot`, and `ApplySnapshotChunk` calls. + +Additionally, there is a `Flush` method that is called on every connection, +and an `Echo` method that is just for debugging. + +More details on managing state across connections can be found in the section on +[ABCI Applications](apps.md). + +## Errors + +The `Query`, `CheckTx` and `DeliverTx` methods include a `Code` field in their `Response*`. +This field is meant to contain an application-specific response code. +A response code of `0` indicates no error. Any other response code +indicates to Tendermint that an error occurred. + +These methods also return a `Codespace` string to Tendermint. This field is +used to disambiguate `Code` values returned by different domains of the +application. The `Codespace` is a namespace for the `Code`. + +The `Echo`, `Info`, `InitChain`, `BeginBlock`, `EndBlock`, `Commit` methods +do not return errors. An error in any of these methods represents a critical +issue that Tendermint has no reasonable way to handle. If there is an error in one +of these methods, the application must crash to ensure that the error is safely +handled by an operator. + +The handling of non-zero response codes by Tendermint is described below + +### CheckTx + +The `CheckTx` ABCI method controls what transactions are considered for inclusion in a block. +When Tendermint receives a `ResponseCheckTx` with a non-zero `Code`, the associated +transaction will be not be added to Tendermint's mempool or it will be removed if +it is already included. + +### DeliverTx + +The `DeliverTx` ABCI method delivers transactions from Tendermint to the application. +When Tendermint recieves a `ResponseDeliverTx` with a non-zero `Code`, the response code is logged. +The transaction was already included in a block, so the `Code` does not influence +Tendermint consensus. + +### Query + +The `Query` ABCI method query queries the application for information about application state. +When Tendermint receives a `ResponseQuery` with a non-zero `Code`, this code is +returned directly to the client that initiated the query. + +## Events + +The `CheckTx`, `BeginBlock`, `DeliverTx`, `EndBlock` methods include an `Events` +field in their `Response*`. Applications may respond to these ABCI methods with a set of events. +Events allow applications to associate metadata about ABCI method execution with the +transactions and blocks this metadata relates to. +Events returned via these ABCI methods do not impact Tendermint consensus in any way +and instead exist to power subscriptions and queries of Tendermint state. + +An `Event` contains a `type` and a list of `EventAttributes`, which are key-value +string pairs denoting metadata about what happened during the method's execution. +`Event` values can be used to index transactions and blocks according to what happened +during their execution. Note that the set of events returned for a block from +`BeginBlock` and `EndBlock` are merged. In case both methods return the same +key, only the value defined in `EndBlock` is used. + +Each event has a `type` which is meant to categorize the event for a particular +`Response*` or `Tx`. A `Response*` or `Tx` may contain multiple events with duplicate +`type` values, where each distinct entry is meant to categorize attributes for a +particular event. Every key and value in an event's attributes must be UTF-8 +encoded strings along with the event type itself. + +```protobuf +message Event { + string type = 1; + repeated EventAttribute attributes = 2; +} +``` + +The attributes of an `Event` consist of a `key`, a `value`, and an `index` flag. The +index flag notifies the Tendermint indexer to index the attribute. The value of +the `index` flag is non-deterministic and may vary across different nodes in the network. + +```protobuf +message EventAttribute { + bytes key = 1; + bytes value = 2; + bool index = 3; // nondeterministic +} +``` + +Example: + +```go + abci.ResponseDeliverTx{ + // ... + Events: []abci.Event{ + { + Type: "validator.provisions", + Attributes: []abci.EventAttribute{ + abci.EventAttribute{Key: []byte("address"), Value: []byte("..."), Index: true}, + abci.EventAttribute{Key: []byte("amount"), Value: []byte("..."), Index: true}, + abci.EventAttribute{Key: []byte("balance"), Value: []byte("..."), Index: true}, + }, + }, + { + Type: "validator.provisions", + Attributes: []abci.EventAttribute{ + abci.EventAttribute{Key: []byte("address"), Value: []byte("..."), Index: true}, + abci.EventAttribute{Key: []byte("amount"), Value: []byte("..."), Index: false}, + abci.EventAttribute{Key: []byte("balance"), Value: []byte("..."), Index: false}, + }, + }, + { + Type: "validator.slashed", + Attributes: []abci.EventAttribute{ + abci.EventAttribute{Key: []byte("address"), Value: []byte("..."), Index: false}, + abci.EventAttribute{Key: []byte("amount"), Value: []byte("..."), Index: true}, + abci.EventAttribute{Key: []byte("reason"), Value: []byte("..."), Index: true}, + }, + }, + // ... + }, +} +``` + +## EvidenceType + +Tendermint's security model relies on the use of "evidence". Evidence is proof of +malicious behaviour by a network participant. It is the responsibility of Tendermint +to detect such malicious behaviour. When malicious behavior is detected, Tendermint +will gossip evidence of the behavior to other nodes and commit the evidence to +the chain once it is verified by all validators. This evidence will then be +passed it on to the application through the ABCI. It is the responsibility of the +application to handle the evidence and exercise punishment. + +EvidenceType has the following protobuf format: + +```proto +enum EvidenceType { + UNKNOWN = 0; + DUPLICATE_VOTE = 1; + LIGHT_CLIENT_ATTACK = 2; +} +``` + +There are two forms of evidence: Duplicate Vote and Light Client Attack. More +information can be found in either [data structures](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/core/data_structures.md) +or [accountability](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/light-client/accountability/) + +## Determinism + +ABCI applications must implement deterministic finite-state machines to be +securely replicated by the Tendermint consensus engine. This means block execution +over the Consensus Connection must be strictly deterministic: given the same +ordered set of requests, all nodes will compute identical responses, for all +BeginBlock, DeliverTx, EndBlock, and Commit. This is critical, because the +responses are included in the header of the next block, either via a Merkle root +or directly, so all nodes must agree on exactly what they are. + +For this reason, it is recommended that applications not be exposed to any +external user or process except via the ABCI connections to a consensus engine +like Tendermint Core. The application must only change its state based on input +from block execution (BeginBlock, DeliverTx, EndBlock, Commit), and not through +any other kind of request. This is the only way to ensure all nodes see the same +transactions and compute the same results. + +If there is some non-determinism in the state machine, consensus will eventually +fail as nodes disagree over the correct values for the block header. The +non-determinism must be fixed and the nodes restarted. + +Sources of non-determinism in applications may include: + +* Hardware failures + * Cosmic rays, overheating, etc. +* Node-dependent state + * Random numbers + * Time +* Underspecification + * Library version changes + * Race conditions + * Floating point numbers + * JSON serialization + * Iterating through hash-tables/maps/dictionaries +* External Sources + * Filesystem + * Network calls (eg. some external REST API service) + +See [#56](https://github.com/tendermint/abci/issues/56) for original discussion. + +Note that some methods (`Query, CheckTx, DeliverTx`) return +explicitly non-deterministic data in the form of `Info` and `Log` fields. The `Log` is +intended for the literal output from the application's logger, while the +`Info` is any additional info that should be returned. These are the only fields +that are not included in block header computations, so we don't need agreement +on them. All other fields in the `Response*` must be strictly deterministic. + +## Block Execution + +The first time a new blockchain is started, Tendermint calls +`InitChain`. From then on, the following sequence of methods is executed for each +block: + +`BeginBlock, [DeliverTx], EndBlock, Commit` + +where one `DeliverTx` is called for each transaction in the block. +The result is an updated application state. +Cryptographic commitments to the results of DeliverTx, EndBlock, and +Commit are included in the header of the next block. + +## State Sync + +State sync allows new nodes to rapidly bootstrap by discovering, fetching, and applying +state machine snapshots instead of replaying historical blocks. For more details, see the +[state sync section](../spec/p2p/messages/state-sync.md). + +New nodes will discover and request snapshots from other nodes in the P2P network. +A Tendermint node that receives a request for snapshots from a peer will call +`ListSnapshots` on its application to retrieve any local state snapshots. After receiving + snapshots from peers, the new node will offer each snapshot received from a peer +to its local application via the `OfferSnapshot` method. + +Snapshots may be quite large and are thus broken into smaller "chunks" that can be +assembled into the whole snapshot. Once the application accepts a snapshot and +begins restoring it, Tendermint will fetch snapshot "chunks" from existing nodes. +The node providing "chunks" will fetch them from its local application using +the `LoadSnapshotChunk` method. + +As the new node receives "chunks" it will apply them sequentially to the local +application with `ApplySnapshotChunk`. When all chunks have been applied, the application +`AppHash` is retrieved via an `Info` query. The `AppHash` is then compared to +the blockchain's `AppHash` which is verified via [light client verification](../spec/light-client/verification/README.md). + +## Messages + +### Echo + +* **Request**: + * `Message (string)`: A string to echo back +* **Response**: + * `Message (string)`: The input string +* **Usage**: + * Echo a string to test an abci client/server implementation + +### Flush + +* **Usage**: + * Signals that messages queued on the client should be flushed to + the server. It is called periodically by the client + implementation to ensure asynchronous requests are actually + sent, and is called immediately to make a synchronous request, + which returns when the Flush response comes back. + +### Info + +* **Request**: + + | Name | Type | Description | Field Number | + |---------------|--------|------------------------------------------|--------------| + | version | string | The Tendermint software semantic version | 1 | + | block_version | uint64 | The Tendermint Block Protocol version | 2 | + | p2p_version | uint64 | The Tendermint P2P Protocol version | 3 | + | abci_version | string | The Tendermint ABCI semantic version | 4 | + +* **Response**: + + | Name | Type | Description | Field Number | + |---------------------|--------|--------------------------------------------------|--------------| + | data | string | Some arbitrary information | 1 | + | version | string | The application software semantic version | 2 | + | app_version | uint64 | The application protocol version | 3 | + | last_block_height | int64 | Latest block for which the app has called Commit | 4 | + | last_block_app_hash | bytes | Latest result of Commit | 5 | + +* **Usage**: + * Return information about the application state. + * Used to sync Tendermint with the application during a handshake + that happens on startup. + * The returned `app_version` will be included in the Header of every block. + * Tendermint expects `last_block_app_hash` and `last_block_height` to + be updated during `Commit`, ensuring that `Commit` is never + called twice for the same block height. + +> Note: Semantic version is a reference to [semantic versioning](https://semver.org/). Semantic versions in info will be displayed as X.X.x. + +### InitChain + +* **Request**: + + | Name | Type | Description | Field Number | + |------------------|--------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|--------------| + | time | [google.protobuf.Timestamp](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp) | Genesis time | 1 | + | chain_id | string | ID of the blockchain. | 2 | + | consensus_params | [ConsensusParams](#consensusparams) | Initial consensus-critical parameters. | 3 | + | validators | repeated [ValidatorUpdate](#validatorupdate) | Initial genesis validators, sorted by voting power. | 4 | + | app_state_bytes | bytes | Serialized initial application state. JSON bytes. | 5 | + | initial_height | int64 | Height of the initial block (typically `1`). | 6 | + +* **Response**: + + | Name | Type | Description | Field Number | + |------------------|----------------------------------------------|-------------------------------------------------|--------------| + | consensus_params | [ConsensusParams](#consensusparams) | Initial consensus-critical parameters (optional | 1 | + | validators | repeated [ValidatorUpdate](#validatorupdate) | Initial validator set (optional). | 2 | + | app_hash | bytes | Initial application hash. | 3 | + +* **Usage**: + * Called once upon genesis. + * If ResponseInitChain.Validators is empty, the initial validator set will be the RequestInitChain.Validators + * If ResponseInitChain.Validators is not empty, it will be the initial + validator set (regardless of what is in RequestInitChain.Validators). + * This allows the app to decide if it wants to accept the initial validator + set proposed by tendermint (ie. in the genesis file), or if it wants to use + a different one (perhaps computed based on some application specific + information in the genesis file). + +### Query + +* **Request**: + + | Name | Type | Description | Field Number | + |--------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| + | data | bytes | Raw query bytes. Can be used with or in lieu of Path. | 1 | + | path | string | Path field of the request URI. Can be used with or in lieu of `data`. Apps MUST interpret `/store` as a query by key on the underlying store. The key SHOULD be specified in the `data` field. Apps SHOULD allow queries over specific types like `/accounts/...` or `/votes/...` | 2 | + | height | int64 | The block height for which you want the query (default=0 returns data for the latest committed block). Note that this is the height of the block containing the application's Merkle root hash, which represents the state as it was after committing the block at Height-1 | 3 | + | prove | bool | Return Merkle proof with response if possible | 4 | + +* **Response**: + + | Name | Type | Description | Field Number | + |-----------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| + | code | uint32 | Response code. | 1 | + | log | string | The output of the application's logger. **May be non-deterministic.** | 3 | + | info | string | Additional information. **May be non-deterministic.** | 4 | + | index | int64 | The index of the key in the tree. | 5 | + | key | bytes | The key of the matching data. | 6 | + | value | bytes | The value of the matching data. | 7 | + | proof_ops | [ProofOps](#proofops) | Serialized proof for the value data, if requested, to be verified against the `app_hash` for the given Height. | 8 | + | height | int64 | The block height from which data was derived. Note that this is the height of the block containing the application's Merkle root hash, which represents the state as it was after committing the block at Height-1 | 9 | + | codespace | string | Namespace for the `code`. | 10 | + +* **Usage**: + * Query for data from the application at current or past height. + * Optionally return Merkle proof. + * Merkle proof includes self-describing `type` field to support many types + of Merkle trees and encoding formats. + +### BeginBlock + +* **Request**: + + | Name | Type | Description | Field Number | + |----------------------|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------|--------------| + | hash | bytes | The block's hash. This can be derived from the block header. | 1 | + | header | [Header](../core/data_structures.md#header) | The block header. | 2 | + | last_commit_info | [LastCommitInfo](#lastcommitinfo) | Info about the last commit, including the round, and the list of validators and which ones signed the last block. | 3 | + | byzantine_validators | repeated [Evidence](#evidence) | List of evidence of validators that acted maliciously. | 4 | + +* **Response**: + + | Name | Type | Description | Field Number | + |--------|---------------------------|-------------------------------------|--------------| + | events | repeated [Event](#events) | type & Key-Value events for indexing | 1 | + +* **Usage**: + * Signals the beginning of a new block. + * Called prior to any `DeliverTx` method calls. + * The header contains the height, timestamp, and more - it exactly matches the + Tendermint block header. We may seek to generalize this in the future. + * The `LastCommitInfo` and `ByzantineValidators` can be used to determine + rewards and punishments for the validators. + +### CheckTx + +* **Request**: + + | Name | Type | Description | Field Number | + |------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| + | tx | bytes | The request transaction bytes | 1 | + | type | CheckTxType | One of `CheckTx_New` or `CheckTx_Recheck`. `CheckTx_New` is the default and means that a full check of the tranasaction is required. `CheckTx_Recheck` types are used when the mempool is initiating a normal recheck of a transaction. | 2 | + +* **Response**: + + | Name | Type | Description | Field Number | + |------------|---------------------------|-----------------------------------------------------------------------|--------------| + | code | uint32 | Response code. | 1 | + | data | bytes | Result bytes, if any. | 2 | + | log | string | The output of the application's logger. **May be non-deterministic.** | 3 | + | info | string | Additional information. **May be non-deterministic.** | 4 | + | gas_wanted | int64 | Amount of gas requested for transaction. | 5 | + | gas_used | int64 | Amount of gas consumed by transaction. | 6 | + | events | repeated [Event](#events) | Type & Key-Value events for indexing transactions (eg. by account). | 7 | + | codespace | string | Namespace for the `code`. | 8 | + | sender | string | The transaction's sender (e.g. the signer) | 9 | + | priority | int64 | The transaction's priority (for mempool ordering) | 10 | + +* **Usage**: + + * Technically optional - not involved in processing blocks. + * Guardian of the mempool: every node runs `CheckTx` before letting a + transaction into its local mempool. + * The transaction may come from an external user or another node + * `CheckTx` validates the transaction against the current state of the application, + for example, checking signatures and account balances, but does not apply any + of the state changes described in the transaction. + not running code in a virtual machine. + * Transactions where `ResponseCheckTx.Code != 0` will be rejected - they will not be broadcast to + other nodes or included in a proposal block. + * Tendermint attributes no other value to the response code + +### DeliverTx + +* **Request**: + + | Name | Type | Description | Field Number | + |------|-------|--------------------------------|--------------| + | tx | bytes | The request transaction bytes. | 1 | + +* **Response**: + + | Name | Type | Description | Field Number | + |------------|---------------------------|-----------------------------------------------------------------------|--------------| + | code | uint32 | Response code. | 1 | + | data | bytes | Result bytes, if any. | 2 | + | log | string | The output of the application's logger. **May be non-deterministic.** | 3 | + | info | string | Additional information. **May be non-deterministic.** | 4 | + | gas_wanted | int64 | Amount of gas requested for transaction. | 5 | + | gas_used | int64 | Amount of gas consumed by transaction. | 6 | + | events | repeated [Event](#events) | Type & Key-Value events for indexing transactions (eg. by account). | 7 | + | codespace | string | Namespace for the `code`. | 8 | + +* **Usage**: + * [**Required**] The core method of the application. + * When `DeliverTx` is called, the application must execute the transaction in full before returning control to Tendermint. + * `ResponseDeliverTx.Code == 0` only if the transaction is fully valid. + +### EndBlock + +* **Request**: + + | Name | Type | Description | Field Number | + |--------|-------|------------------------------------|--------------| + | height | int64 | Height of the block just executed. | 1 | + +* **Response**: + + | Name | Type | Description | Field Number | + |-------------------------|----------------------------------------------|-----------------------------------------------------------------|--------------| + | validator_updates | repeated [ValidatorUpdate](#validatorupdate) | Changes to validator set (set voting power to 0 to remove). | 1 | + | consensus_param_updates | [ConsensusParams](#consensusparams) | Changes to consensus-critical time, size, and other parameters. | 2 | + | events | repeated [Event](#events) | Type & Key-Value events for indexing | 3 | + +* **Usage**: + * Signals the end of a block. + * Called after all the transactions for the current block have been delivered, prior to the block's `Commit` message. + * Optional `validator_updates` triggered by block `H`. These updates affect validation + for blocks `H+1`, `H+2`, and `H+3`. + * Heights following a validator update are affected in the following way: + * `H+1`: `NextValidatorsHash` includes the new `validator_updates` value. + * `H+2`: The validator set change takes effect and `ValidatorsHash` is updated. + * `H+3`: `LastCommitInfo` is changed to include the altered validator set. + * `consensus_param_updates` returned for block `H` apply to the consensus + params for block `H+1`. For more information on the consensus parameters, + see the [application spec entry on consensus parameters](../spec/abci/apps.md#consensus-parameters). + +### Commit + +* **Request**: + + | Name | Type | Description | Field Number | + |--------|-------|------------------------------------|--------------| + + Commit signals the application to persist application state. It takes no parameters. +* **Response**: + + | Name | Type | Description | Field Number | + |---------------|-------|------------------------------------------------------------------------|--------------| + | data | bytes | The Merkle root hash of the application state. | 2 | + | retain_height | int64 | Blocks below this height may be removed. Defaults to `0` (retain all). | 3 | + +* **Usage**: + * Signal the application to persist the application state. + * Return an (optional) Merkle root hash of the application state + * `ResponseCommit.Data` is included as the `Header.AppHash` in the next block + * it may be empty + * Later calls to `Query` can return proofs about the application state anchored + in this Merkle root hash + * Note developers can return whatever they want here (could be nothing, or a + constant string, etc.), so long as it is deterministic - it must not be a + function of anything that did not come from the + BeginBlock/DeliverTx/EndBlock methods. + * Use `RetainHeight` with caution! If all nodes in the network remove historical + blocks then this data is permanently lost, and no new nodes will be able to + join the network and bootstrap. Historical blocks may also be required for + other purposes, e.g. auditing, replay of non-persisted heights, light client + verification, and so on. + +### ListSnapshots + +* **Request**: + + | Name | Type | Description | Field Number | + |--------|-------|------------------------------------|--------------| + + Empty request asking the application for a list of snapshots. + +* **Response**: + + | Name | Type | Description | Field Number | + |-----------|--------------------------------|--------------------------------|--------------| + | snapshots | repeated [Snapshot](#snapshot) | List of local state snapshots. | 1 | + +* **Usage**: + * Used during state sync to discover available snapshots on peers. + * See `Snapshot` data type for details. + +### LoadSnapshotChunk + +* **Request**: + + | Name | Type | Description | Field Number | + |--------|--------|-----------------------------------------------------------------------|--------------| + | height | uint64 | The height of the snapshot the chunks belongs to. | 1 | + | format | uint32 | The application-specific format of the snapshot the chunk belongs to. | 2 | + | chunk | uint32 | The chunk index, starting from `0` for the initial chunk. | 3 | + +* **Response**: + + | Name | Type | Description | Field Number | + |-------|-------|-------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| + | chunk | bytes | The binary chunk contents, in an arbitray format. Chunk messages cannot be larger than 16 MB _including metadata_, so 10 MB is a good starting point. | 1 | + +* **Usage**: + * Used during state sync to retrieve snapshot chunks from peers. + +### OfferSnapshot + +* **Request**: + + | Name | Type | Description | Field Number | + |----------|-----------------------|--------------------------------------------------------------------------|--------------| + | snapshot | [Snapshot](#snapshot) | The snapshot offered for restoration. | 1 | + | app_hash | bytes | The light client-verified app hash for this height, from the blockchain. | 2 | + +* **Response**: + + | Name | Type | Description | Field Number | + |--------|-------------------|-----------------------------------|--------------| + | result | [Result](#result) | The result of the snapshot offer. | 1 | + +#### Result + +```proto + enum Result { + UNKNOWN = 0; // Unknown result, abort all snapshot restoration + ACCEPT = 1; // Snapshot is accepted, start applying chunks. + ABORT = 2; // Abort snapshot restoration, and don't try any other snapshots. + REJECT = 3; // Reject this specific snapshot, try others. + REJECT_FORMAT = 4; // Reject all snapshots with this `format`, try others. + REJECT_SENDER = 5; // Reject all snapshots from all senders of this snapshot, try others. + } +``` + +* **Usage**: + * `OfferSnapshot` is called when bootstrapping a node using state sync. The application may + accept or reject snapshots as appropriate. Upon accepting, Tendermint will retrieve and + apply snapshot chunks via `ApplySnapshotChunk`. The application may also choose to reject a + snapshot in the chunk response, in which case it should be prepared to accept further + `OfferSnapshot` calls. + * Only `AppHash` can be trusted, as it has been verified by the light client. Any other data + can be spoofed by adversaries, so applications should employ additional verification schemes + to avoid denial-of-service attacks. The verified `AppHash` is automatically checked against + the restored application at the end of snapshot restoration. + * For more information, see the `Snapshot` data type or the [state sync section](../spec/p2p/messages/state-sync.md). + +### ApplySnapshotChunk + +* **Request**: + + | Name | Type | Description | Field Number | + |--------|--------|-----------------------------------------------------------------------------|--------------| + | index | uint32 | The chunk index, starting from `0`. Tendermint applies chunks sequentially. | 1 | + | chunk | bytes | The binary chunk contents, as returned by `LoadSnapshotChunk`. | 2 | + | sender | string | The P2P ID of the node who sent this chunk. | 3 | + +* **Response**: + + | Name | Type | Description | Field Number | + |----------------|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| + | result | Result (see below) | The result of applying this chunk. | 1 | + | refetch_chunks | repeated uint32 | Refetch and reapply the given chunks, regardless of `result`. Only the listed chunks will be refetched, and reapplied in sequential order. | 2 | + | reject_senders | repeated string | Reject the given P2P senders, regardless of `Result`. Any chunks already applied will not be refetched unless explicitly requested, but queued chunks from these senders will be discarded, and new chunks or other snapshots rejected. | 3 | + +```proto + enum Result { + UNKNOWN = 0; // Unknown result, abort all snapshot restoration + ACCEPT = 1; // The chunk was accepted. + ABORT = 2; // Abort snapshot restoration, and don't try any other snapshots. + RETRY = 3; // Reapply this chunk, combine with `RefetchChunks` and `RejectSenders` as appropriate. + RETRY_SNAPSHOT = 4; // Restart this snapshot from `OfferSnapshot`, reusing chunks unless instructed otherwise. + REJECT_SNAPSHOT = 5; // Reject this snapshot, try a different one. + } +``` + +* **Usage**: + * The application can choose to refetch chunks and/or ban P2P peers as appropriate. Tendermint + will not do this unless instructed by the application. + * The application may want to verify each chunk, e.g. by attaching chunk hashes in + `Snapshot.Metadata` and/or incrementally verifying contents against `AppHash`. + * When all chunks have been accepted, Tendermint will make an ABCI `Info` call to verify that + `LastBlockAppHash` and `LastBlockHeight` matches the expected values, and record the + `AppVersion` in the node state. It then switches to fast sync or consensus and joins the + network. + * If Tendermint is unable to retrieve the next chunk after some time (e.g. because no suitable + peers are available), it will reject the snapshot and try a different one via `OfferSnapshot`. + The application should be prepared to reset and accept it or abort as appropriate. + +## Data Types + +Most of the data structures used in ABCI are shared [common data structures](../spec/core/data_structures.md). In certain cases, ABCI uses different data structures which are documented here: + +### Validator + +* **Fields**: + + | Name | Type | Description | Field Number | + |---------|-------|---------------------------------------------------------------------|--------------| + | address | bytes | [Address](../core/data_structures.md#address) of validator | 1 | + | power | int64 | Voting power of the validator | 3 | + +* **Usage**: + * Validator identified by address + * Used in RequestBeginBlock as part of VoteInfo + * Does not include PubKey to avoid sending potentially large quantum pubkeys + over the ABCI + +### ValidatorUpdate + +* **Fields**: + + | Name | Type | Description | Field Number | + |---------|--------------------------------------------------|-------------------------------|--------------| + | pub_key | [Public Key](../core/data_structures.md#pub_key) | Public key of the validator | 1 | + | power | int64 | Voting power of the validator | 2 | + +* **Usage**: + * Validator identified by PubKey + * Used to tell Tendermint to update the validator set + +### VoteInfo + +* **Fields**: + + | Name | Type | Description | Field Number | + |-------------------|-------------------------|--------------------------------------------------------------|--------------| + | validator | [Validator](#validator) | A validator | 1 | + | signed_last_block | bool | Indicates whether or not the validator signed the last block | 2 | + +* **Usage**: + * Indicates whether a validator signed the last block, allowing for rewards + based on validator availability + +### Evidence + +* **Fields**: + + | Name | Type | Description | Field Number | + |--------------------|--------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|--------------| + | type | [EvidenceType](#evidencetype) | Type of the evidence. An enum of possible evidence's. | 1 | + | validator | [Validator](#validator) | The offending validator | 2 | + | height | int64 | Height when the offense occurred | 3 | + | time | [google.protobuf.Timestamp](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp) | Time of the block that was committed at the height that the offense occurred | 4 | + | total_voting_power | int64 | Total voting power of the validator set at height `Height` | 5 | + +#### EvidenceType + +* **Fields** + + EvidenceType is an enum with the listed fields: + + | Name | Field Number | + |---------------------|--------------| + | UNKNOWN | 0 | + | DUPLICATE_VOTE | 1 | + | LIGHT_CLIENT_ATTACK | 2 | + +### LastCommitInfo + +* **Fields**: + + | Name | Type | Description | Field Number | + |-------|--------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------| + | round | int32 | Commit round. Reflects the total amount of rounds it took to come to consensus for the current block. | 1 | + | votes | repeated [VoteInfo](#voteinfo) | List of validators addresses in the last validator set with their voting power and whether or not they signed a vote. | 2 | + +### ConsensusParams + +* **Fields**: + + | Name | Type | Description | Field Number | + |-----------|---------------------------------------------------------------|------------------------------------------------------------------------------|--------------| + | block | [BlockParams](../core/data_structures.md#blockparams) | Parameters limiting the size of a block and time between consecutive blocks. | 1 | + | evidence | [EvidenceParams](../core/data_structures.md#evidenceparams) | Parameters limiting the validity of evidence of byzantine behaviour. | 2 | + | validator | [ValidatorParams](../core/data_structures.md#validatorparams) | Parameters limiting the types of public keys validators can use. | 3 | + | version | [VersionsParams](../core/data_structures.md#versionparams) | The ABCI application version. | 4 | + +### ProofOps + +* **Fields**: + + | Name | Type | Description | Field Number | + |------|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| + | ops | repeated [ProofOp](#proofop) | List of chained Merkle proofs, of possibly different types. The Merkle root of one op is the value being proven in the next op. The Merkle root of the final op should equal the ultimate root hash being verified against.. | 1 | + +### ProofOp + +* **Fields**: + + | Name | Type | Description | Field Number | + |------|--------|------------------------------------------------|--------------| + | type | string | Type of Merkle proof and how it's encoded. | 1 | + | key | bytes | Key in the Merkle tree that this proof is for. | 2 | + | data | bytes | Encoded Merkle proof for the key. | 3 | + +### Snapshot + +* **Fields**: + + | Name | Type | Description | Field Number | + |----------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| + | height | uint64 | The height at which the snapshot was taken (after commit). | 1 | + | format | uint32 | An application-specific snapshot format, allowing applications to version their snapshot data format and make backwards-incompatible changes. Tendermint does not interpret this. | 2 | + | chunks | uint32 | The number of chunks in the snapshot. Must be at least 1 (even if empty). | 3 | + | hash | bytes | TAn arbitrary snapshot hash. Must be equal only for identical snapshots across nodes. Tendermint does not interpret the hash, it only compares them. | 3 | + | metadata | bytes | Arbitrary application metadata, for example chunk hashes or other verification data. | 3 | + +* **Usage**: + * Used for state sync snapshots, see the [state sync section](../spec/p2p/messages/state-sync.md) for details. + * A snapshot is considered identical across nodes only if _all_ fields are equal (including + `Metadata`). Chunks may be retrieved from all nodes that have the same snapshot. + * When sent across the network, a snapshot message can be at most 4 MB. diff --git a/spec/abci/apps.md b/spec/abci/apps.md new file mode 100644 index 000000000..d1b85a740 --- /dev/null +++ b/spec/abci/apps.md @@ -0,0 +1,671 @@ +--- +order: 2 +title: Applications +--- + +# Applications + +Please ensure you've first read the spec for [ABCI Methods and Types](abci.md) + +Here we cover the following components of ABCI applications: + +- [Connection State](#connection-state) - the interplay between ABCI connections and application state + and the differences between `CheckTx` and `DeliverTx`. +- [Transaction Results](#transaction-results) - rules around transaction + results and validity +- [Validator Set Updates](#validator-updates) - how validator sets are + changed during `InitChain` and `EndBlock` +- [Query](#query) - standards for using the `Query` method and proofs about the + application state +- [Crash Recovery](#crash-recovery) - handshake protocol to synchronize + Tendermint and the application on startup. +- [State Sync](#state-sync) - rapid bootstrapping of new nodes by restoring state machine snapshots + +## Connection State + +Since Tendermint maintains four concurrent ABCI connections, it is typical +for an application to maintain a distinct state for each, and for the states to +be synchronized during `Commit`. + +### Concurrency + +In principle, each of the four ABCI connections operate concurrently with one +another. This means applications need to ensure access to state is +thread safe. In practice, both the +[default in-process ABCI client](https://github.com/tendermint/tendermint/blob/v0.34.4/abci/client/local_client.go#L18) +and the +[default Go ABCI +server](https://github.com/tendermint/tendermint/blob/v0.34.4/abci/server/socket_server.go#L32) +use global locks across all connections, so they are not +concurrent at all. This means if your app is written in Go, and compiled in-process with Tendermint +using the default `NewLocalClient`, or run out-of-process using the default `SocketServer`, +ABCI messages from all connections will be linearizable (received one at a +time). + +The existence of this global mutex means Go application developers can get +thread safety for application state by routing *all* reads and writes through the ABCI +system. Thus it may be *unsafe* to expose application state directly to an RPC +interface, and unless explicit measures are taken, all queries should be routed through the ABCI Query method. + +### BeginBlock + +The BeginBlock request can be used to run some code at the beginning of +every block. It also allows Tendermint to send the current block hash +and header to the application, before it sends any of the transactions. + +The app should remember the latest height and header (ie. from which it +has run a successful Commit) so that it can tell Tendermint where to +pick up from when it restarts. See information on the Handshake, below. + +### Commit + +Application state should only be persisted to disk during `Commit`. + +Before `Commit` is called, Tendermint locks and flushes the mempool so that no new messages will +be received on the mempool connection. This provides an opportunity to safely update all four connection +states to the latest committed state at once. + +When `Commit` completes, it unlocks the mempool. + +WARNING: if the ABCI app logic processing the `Commit` message sends a +`/broadcast_tx_sync` or `/broadcast_tx_commit` and waits for the response +before proceeding, it will deadlock. Executing those `broadcast_tx` calls +involves acquiring a lock that is held during the `Commit` call, so it's not +possible. If you make the call to the `broadcast_tx` endpoints concurrently, +that's no problem, it just can't be part of the sequential logic of the +`Commit` function. + +### Consensus Connection + +The Consensus Connection should maintain a `DeliverTxState` - the working state +for block execution. It should be updated by the calls to `BeginBlock`, `DeliverTx`, +and `EndBlock` during block execution and committed to disk as the "latest +committed state" during `Commit`. + +Updates made to the `DeliverTxState` by each method call must be readable by each subsequent method - +ie. the updates are linearizable. + +### Mempool Connection + +The mempool Connection should maintain a `CheckTxState` +to sequentially process pending transactions in the mempool that have +not yet been committed. It should be initialized to the latest committed state +at the end of every `Commit`. + +Before calling `Commit`, Tendermint will lock and flush the mempool connection, +ensuring that all existing CheckTx are responded to and no new ones can begin. +The `CheckTxState` may be updated concurrently with the `DeliverTxState`, as +messages may be sent concurrently on the Consensus and Mempool connections. + +After `Commit`, while still holding the mempool lock, CheckTx is run again on all transactions that remain in the +node's local mempool after filtering those included in the block. +An additional `Type` parameter is made available to the CheckTx function that +indicates whether an incoming transaction is new (`CheckTxType_New`), or a +recheck (`CheckTxType_Recheck`). + +Finally, after re-checking transactions in the mempool, Tendermint will unlock +the mempool connection. New transactions are once again able to be processed through CheckTx. + +Note that CheckTx is just a weak filter to keep invalid transactions out of the block chain. +CheckTx doesn't have to check everything that affects transaction validity; the +expensive things can be skipped. It's weak because a Byzantine node doesn't +care about CheckTx; it can propose a block full of invalid transactions if it wants. + +#### Replay Protection + +To prevent old transactions from being replayed, CheckTx must implement +replay protection. + +It is possible for old transactions to be sent to the application. So +it is important CheckTx implements some logic to handle them. + +### Query Connection + +The Info Connection should maintain a `QueryState` for answering queries from the user, +and for initialization when Tendermint first starts up (both described further +below). +It should always contain the latest committed state associated with the +latest committed block. + +`QueryState` should be set to the latest `DeliverTxState` at the end of every `Commit`, +after the full block has been processed and the state committed to disk. +Otherwise it should never be modified. + +Tendermint Core currently uses the Query connection to filter peers upon +connecting, according to IP address or node ID. For instance, +returning non-OK ABCI response to either of the following queries will +cause Tendermint to not connect to the corresponding peer: + +- `p2p/filter/addr/`, where `` is an IP address. +- `p2p/filter/id/`, where `` is the hex-encoded node ID (the hash of + the node's p2p pubkey). + +Note: these query formats are subject to change! + +### Snapshot Connection + +The Snapshot Connection is optional, and is only used to serve state sync snapshots for other nodes +and/or restore state sync snapshots to a local node being bootstrapped. + +For more information, see [the state sync section of this document](#state-sync). + +## Transaction Results + +The `Info` and `Log` fields are non-deterministic values for debugging/convenience purposes +that are otherwise ignored. + +The `Data` field must be strictly deterministic, but can be arbitrary data. + +### Gas + +Ethereum introduced the notion of `gas` as an abstract representation of the +cost of resources used by nodes when processing transactions. Every operation in the +Ethereum Virtual Machine uses some amount of gas, and gas can be accepted at a market-variable price. +Users propose a maximum amount of gas for their transaction; if the tx uses less, they get +the difference credited back. Tendermint adopts a similar abstraction, +though uses it only optionally and weakly, allowing applications to define +their own sense of the cost of execution. + +In Tendermint, the [ConsensusParams.Block.MaxGas](../proto/types/params.proto) limits the amount of `gas` that can be used in a block. +The default value is `-1`, meaning no limit, or that the concept of gas is +meaningless. + +Responses contain a `GasWanted` and `GasUsed` field. The former is the maximum +amount of gas the sender of a tx is willing to use, and the later is how much it actually +used. Applications should enforce that `GasUsed <= GasWanted` - ie. tx execution +should halt before it can use more resources than it requested. + +When `MaxGas > -1`, Tendermint enforces the following rules: + +- `GasWanted <= MaxGas` for all txs in the mempool +- `(sum of GasWanted in a block) <= MaxGas` when proposing a block + +If `MaxGas == -1`, no rules about gas are enforced. + +Note that Tendermint does not currently enforce anything about Gas in the consensus, only the mempool. +This means it does not guarantee that committed blocks satisfy these rules! +It is the application's responsibility to return non-zero response codes when gas limits are exceeded. + +The `GasUsed` field is ignored completely by Tendermint. That said, applications should enforce: + +- `GasUsed <= GasWanted` for any given transaction +- `(sum of GasUsed in a block) <= MaxGas` for every block + +In the future, we intend to add a `Priority` field to the responses that can be +used to explicitly prioritize txs in the mempool for inclusion in a block +proposal. See [#1861](https://github.com/tendermint/tendermint/issues/1861). + +### CheckTx + +If `Code != 0`, it will be rejected from the mempool and hence +not broadcasted to other peers and not included in a proposal block. + +`Data` contains the result of the CheckTx transaction execution, if any. It is +semantically meaningless to Tendermint. + +`Events` include any events for the execution, though since the transaction has not +been committed yet, they are effectively ignored by Tendermint. + +### DeliverTx + +DeliverTx is the workhorse of the blockchain. Tendermint sends the +DeliverTx requests asynchronously but in order, and relies on the +underlying socket protocol (ie. TCP) to ensure they are received by the +app in order. They have already been ordered in the global consensus by +the Tendermint protocol. + +If DeliverTx returns `Code != 0`, the transaction will be considered invalid, +though it is still included in the block. + +DeliverTx also returns a [Code, Data, and Log](../../proto/abci/types.proto#L189-L191). + +`Data` contains the result of the CheckTx transaction execution, if any. It is +semantically meaningless to Tendermint. + +Both the `Code` and `Data` are included in a structure that is hashed into the +`LastResultsHash` of the next block header. + +`Events` include any events for the execution, which Tendermint will use to index +the transaction by. This allows transactions to be queried according to what +events took place during their execution. + +## Updating the Validator Set + +The application may set the validator set during InitChain, and may update it during +EndBlock. + +Note that the maximum total power of the validator set is bounded by +`MaxTotalVotingPower = MaxInt64 / 8`. Applications are responsible for ensuring +they do not make changes to the validator set that cause it to exceed this +limit. + +Additionally, applications must ensure that a single set of updates does not contain any duplicates - +a given public key can only appear once within a given update. If an update includes +duplicates, the block execution will fail irrecoverably. + +### InitChain + +The `InitChain` method can return a list of validators. +If the list is empty, Tendermint will use the validators loaded in the genesis +file. +If the list returned by `InitChain` is not empty, Tendermint will use its contents as the validator set. +This way the application can set the initial validator set for the +blockchain. + +### EndBlock + +Updates to the Tendermint validator set can be made by returning +`ValidatorUpdate` objects in the `ResponseEndBlock`: + +```protobuf +message ValidatorUpdate { + tendermint.crypto.keys.PublicKey pub_key + int64 power +} + +message PublicKey { + oneof { + ed25519 bytes = 1; + } +``` + +The `pub_key` currently supports only one type: + +- `type = "ed25519"` + +The `power` is the new voting power for the validator, with the +following rules: + +- power must be non-negative +- if power is 0, the validator must already exist, and will be removed from the + validator set +- if power is non-0: + - if the validator does not already exist, it will be added to the validator + set with the given power + - if the validator does already exist, its power will be adjusted to the given power +- the total power of the new validator set must not exceed MaxTotalVotingPower + +Note the updates returned in block `H` will only take effect at block `H+2`. + +## Consensus Parameters + +ConsensusParams enforce certain limits in the blockchain, like the maximum size +of blocks, amount of gas used in a block, and the maximum acceptable age of +evidence. They can be set in InitChain and updated in EndBlock. + +### BlockParams.MaxBytes + +The maximum size of a complete Protobuf encoded block. +This is enforced by Tendermint consensus. + +This implies a maximum transaction size that is this MaxBytes, less the expected size of +the header, the validator set, and any included evidence in the block. + +Must have `0 < MaxBytes < 100 MB`. + +### BlockParams.MaxGas + +The maximum of the sum of `GasWanted` that will be allowed in a proposed block. +This is *not* enforced by Tendermint consensus. +It is left to the app to enforce (ie. if txs are included past the +limit, they should return non-zero codes). It is used by Tendermint to limit the +txs included in a proposed block. + +Must have `MaxGas >= -1`. +If `MaxGas == -1`, no limit is enforced. + +### EvidenceParams.MaxAgeDuration + +This is the maximum age of evidence in time units. +This is enforced by Tendermint consensus. + +If a block includes evidence older than this (AND the evidence was created more +than `MaxAgeNumBlocks` ago), the block will be rejected (validators won't vote +for it). + +Must have `MaxAgeDuration > 0`. + +### EvidenceParams.MaxAgeNumBlocks + +This is the maximum age of evidence in blocks. +This is enforced by Tendermint consensus. + +If a block includes evidence older than this (AND the evidence was created more +than `MaxAgeDuration` ago), the block will be rejected (validators won't vote +for it). + +Must have `MaxAgeNumBlocks > 0`. + +### EvidenceParams.MaxNum + +This is the maximum number of evidence that can be committed to a single block. + +The product of this and the `MaxEvidenceBytes` must not exceed the size of +a block minus it's overhead ( ~ `MaxBytes`). + +Must have `MaxNum > 0`. + +### Updates + +The application may set the ConsensusParams during InitChain, and update them during +EndBlock. If the ConsensusParams is empty, it will be ignored. Each field +that is not empty will be applied in full. For instance, if updating the +Block.MaxBytes, applications must also set the other Block fields (like +Block.MaxGas), even if they are unchanged, as they will otherwise cause the +value to be updated to 0. + +#### InitChain + +ResponseInitChain includes a ConsensusParams. +If ConsensusParams is nil, Tendermint will use the params loaded in the genesis +file. If ConsensusParams is not nil, Tendermint will use it. +This way the application can determine the initial consensus params for the +blockchain. + +#### EndBlock + +ResponseEndBlock includes a ConsensusParams. +If ConsensusParams nil, Tendermint will do nothing. +If ConsensusParam is not nil, Tendermint will use it. +This way the application can update the consensus params over time. + +Note the updates returned in block `H` will take effect right away for block +`H+1`. + +## Query + +Query is a generic method with lots of flexibility to enable diverse sets +of queries on application state. Tendermint makes use of Query to filter new peers +based on ID and IP, and exposes Query to the user over RPC. + +Note that calls to Query are not replicated across nodes, but rather query the +local node's state - hence they may return stale reads. For reads that require +consensus, use a transaction. + +The most important use of Query is to return Merkle proofs of the application state at some height +that can be used for efficient application-specific light-clients. + +Note Tendermint has technically no requirements from the Query +message for normal operation - that is, the ABCI app developer need not implement +Query functionality if they do not wish too. + +### Query Proofs + +The Tendermint block header includes a number of hashes, each providing an +anchor for some type of proof about the blockchain. The `ValidatorsHash` enables +quick verification of the validator set, the `DataHash` gives quick +verification of the transactions included in the block, etc. + +The `AppHash` is unique in that it is application specific, and allows for +application-specific Merkle proofs about the state of the application. +While some applications keep all relevant state in the transactions themselves +(like Bitcoin and its UTXOs), others maintain a separated state that is +computed deterministically *from* transactions, but is not contained directly in +the transactions themselves (like Ethereum contracts and accounts). +For such applications, the `AppHash` provides a much more efficient way to verify light-client proofs. + +ABCI applications can take advantage of more efficient light-client proofs for +their state as follows: + +- return the Merkle root of the deterministic application state in +`ResponseCommit.Data`. This Merkle root will be included as the `AppHash` in the next block. +- return efficient Merkle proofs about that application state in `ResponseQuery.Proof` + that can be verified using the `AppHash` of the corresponding block. + +For instance, this allows an application's light-client to verify proofs of +absence in the application state, something which is much less efficient to do using the block hash. + +Some applications (eg. Ethereum, Cosmos-SDK) have multiple "levels" of Merkle trees, +where the leaves of one tree are the root hashes of others. To support this, and +the general variability in Merkle proofs, the `ResponseQuery.Proof` has some minimal structure: + +```protobuf +message ProofOps { + repeated ProofOp ops +} + +message ProofOp { + string type = 1; + bytes key = 2; + bytes data = 3; +} +``` + +Each `ProofOp` contains a proof for a single key in a single Merkle tree, of the specified `type`. +This allows ABCI to support many different kinds of Merkle trees, encoding +formats, and proofs (eg. of presence and absence) just by varying the `type`. +The `data` contains the actual encoded proof, encoded according to the `type`. +When verifying the full proof, the root hash for one ProofOp is the value being +verified for the next ProofOp in the list. The root hash of the final ProofOp in +the list should match the `AppHash` being verified against. + +### Peer Filtering + +When Tendermint connects to a peer, it sends two queries to the ABCI application +using the following paths, with no additional data: + +- `/p2p/filter/addr/`, where `` denote the IP address and + the port of the connection +- `p2p/filter/id/`, where `` is the peer node ID (ie. the + pubkey.Address() for the peer's PubKey) + +If either of these queries return a non-zero ABCI code, Tendermint will refuse +to connect to the peer. + +### Paths + +Queries are directed at paths, and may optionally include additional data. + +The expectation is for there to be some number of high level paths +differentiating concerns, like `/p2p`, `/store`, and `/app`. Currently, +Tendermint only uses `/p2p`, for filtering peers. For more advanced use, see the +implementation of +[Query in the Cosmos-SDK](https://github.com/cosmos/cosmos-sdk/blob/v0.23.1/baseapp/baseapp.go#L333). + +## Crash Recovery + +On startup, Tendermint calls the `Info` method on the Info Connection to get the latest +committed state of the app. The app MUST return information consistent with the +last block it succesfully completed Commit for. + +If the app succesfully committed block H, then `last_block_height = H` and `last_block_app_hash = `. If the app +failed during the Commit of block H, then `last_block_height = H-1` and +`last_block_app_hash = `. + +We now distinguish three heights, and describe how Tendermint syncs itself with +the app. + +```md +storeBlockHeight = height of the last block Tendermint saw a commit for +stateBlockHeight = height of the last block for which Tendermint completed all + block processing and saved all ABCI results to disk +appBlockHeight = height of the last block for which ABCI app succesfully + completed Commit + +``` + +Note we always have `storeBlockHeight >= stateBlockHeight` and `storeBlockHeight >= appBlockHeight` +Note also Tendermint never calls Commit on an ABCI app twice for the same height. + +The procedure is as follows. + +First, some simple start conditions: + +If `appBlockHeight == 0`, then call InitChain. + +If `storeBlockHeight == 0`, we're done. + +Now, some sanity checks: + +If `storeBlockHeight < appBlockHeight`, error +If `storeBlockHeight < stateBlockHeight`, panic +If `storeBlockHeight > stateBlockHeight+1`, panic + +Now, the meat: + +If `storeBlockHeight == stateBlockHeight && appBlockHeight < storeBlockHeight`, +replay all blocks in full from `appBlockHeight` to `storeBlockHeight`. +This happens if we completed processing the block, but the app forgot its height. + +If `storeBlockHeight == stateBlockHeight && appBlockHeight == storeBlockHeight`, we're done. +This happens if we crashed at an opportune spot. + +If `storeBlockHeight == stateBlockHeight+1` +This happens if we started processing the block but didn't finish. + +If `appBlockHeight < stateBlockHeight` + replay all blocks in full from `appBlockHeight` to `storeBlockHeight-1`, + and replay the block at `storeBlockHeight` using the WAL. +This happens if the app forgot the last block it committed. + +If `appBlockHeight == stateBlockHeight`, + replay the last block (storeBlockHeight) in full. +This happens if we crashed before the app finished Commit + +If `appBlockHeight == storeBlockHeight` + update the state using the saved ABCI responses but dont run the block against the real app. +This happens if we crashed after the app finished Commit but before Tendermint saved the state. + +## State Sync + +A new node joining the network can simply join consensus at the genesis height and replay all +historical blocks until it is caught up. However, for large chains this can take a significant +amount of time, often on the order of days or weeks. + +State sync is an alternative mechanism for bootstrapping a new node, where it fetches a snapshot +of the state machine at a given height and restores it. Depending on the application, this can +be several orders of magnitude faster than replaying blocks. + +Note that state sync does not currently backfill historical blocks, so the node will have a +truncated block history - users are advised to consider the broader network implications of this in +terms of block availability and auditability. This functionality may be added in the future. + +For details on the specific ABCI calls and types, see the [methods and types section](abci.md). + +### Taking Snapshots + +Applications that want to support state syncing must take state snapshots at regular intervals. How +this is accomplished is entirely up to the application. A snapshot consists of some metadata and +a set of binary chunks in an arbitrary format: + +- `Height (uint64)`: The height at which the snapshot is taken. It must be taken after the given + height has been committed, and must not contain data from any later heights. + +- `Format (uint32)`: An arbitrary snapshot format identifier. This can be used to version snapshot + formats, e.g. to switch from Protobuf to MessagePack for serialization. The application can use + this when restoring to choose whether to accept or reject a snapshot. + +- `Chunks (uint32)`: The number of chunks in the snapshot. Each chunk contains arbitrary binary + data, and should be less than 16 MB; 10 MB is a good starting point. + +- `Hash ([]byte)`: An arbitrary hash of the snapshot. This is used to check whether a snapshot is + the same across nodes when downloading chunks. + +- `Metadata ([]byte)`: Arbitrary snapshot metadata, e.g. chunk hashes for verification or any other + necessary info. + +For a snapshot to be considered the same across nodes, all of these fields must be identical. When +sent across the network, snapshot metadata messages are limited to 4 MB. + +When a new node is running state sync and discovering snapshots, Tendermint will query an existing +application via the ABCI `ListSnapshots` method to discover available snapshots, and load binary +snapshot chunks via `LoadSnapshotChunk`. The application is free to choose how to implement this +and which formats to use, but must provide the following guarantees: + +- **Consistent:** A snapshot must be taken at a single isolated height, unaffected by + concurrent writes. This can be accomplished by using a data store that supports ACID + transactions with snapshot isolation. + +- **Asynchronous:** Taking a snapshot can be time-consuming, so it must not halt chain progress, + for example by running in a separate thread. + +- **Deterministic:** A snapshot taken at the same height in the same format must be identical + (at the byte level) across nodes, including all metadata. This ensures good availability of + chunks, and that they fit together across nodes. + +A very basic approach might be to use a datastore with MVCC transactions (such as RocksDB), +start a transaction immediately after block commit, and spawn a new thread which is passed the +transaction handle. This thread can then export all data items, serialize them using e.g. +Protobuf, hash the byte stream, split it into chunks, and store the chunks in the file system +along with some metadata - all while the blockchain is applying new blocks in parallel. + +A more advanced approach might include incremental verification of individual chunks against the +chain app hash, parallel or batched exports, compression, and so on. + +Old snapshots should be removed after some time - generally only the last two snapshots are needed +(to prevent the last one from being removed while a node is restoring it). + +### Bootstrapping a Node + +An empty node can be state synced by setting the configuration option `statesync.enabled = +true`. The node also needs the chain genesis file for basic chain info, and configuration for +light client verification of the restored snapshot: a set of Tendermint RPC servers, and a +trusted header hash and corresponding height from a trusted source, via the `statesync` +configuration section. + +Once started, the node will connect to the P2P network and begin discovering snapshots. These +will be offered to the local application via the `OfferSnapshot` ABCI method. Once a snapshot +is accepted Tendermint will fetch and apply the snapshot chunks. After all chunks have been +successfully applied, Tendermint verifies the app's `AppHash` against the chain using the light +client, then switches the node to normal consensus operation. + +#### Snapshot Discovery + +When the empty node join the P2P network, it asks all peers to report snapshots via the +`ListSnapshots` ABCI call (limited to 10 per node). After some time, the node picks the most +suitable snapshot (generally prioritized by height, format, and number of peers), and offers it +to the application via `OfferSnapshot`. The application can choose a number of responses, +including accepting or rejecting it, rejecting the offered format, rejecting the peer who sent +it, and so on. Tendermint will keep discovering and offering snapshots until one is accepted or +the application aborts. + +#### Snapshot Restoration + +Once a snapshot has been accepted via `OfferSnapshot`, Tendermint begins downloading chunks from +any peers that have the same snapshot (i.e. that have identical metadata fields). Chunks are +spooled in a temporary directory, and then given to the application in sequential order via +`ApplySnapshotChunk` until all chunks have been accepted. + +The method for restoring snapshot chunks is entirely up to the application. + +During restoration, the application can respond to `ApplySnapshotChunk` with instructions for how +to continue. This will typically be to accept the chunk and await the next one, but it can also +ask for chunks to be refetched (either the current one or any number of previous ones), P2P peers +to be banned, snapshots to be rejected or retried, and a number of other responses - see the ABCI +reference for details. + +If Tendermint fails to fetch a chunk after some time, it will reject the snapshot and try a +different one via `OfferSnapshot` - the application can choose whether it wants to support +restarting restoration, or simply abort with an error. + +#### Snapshot Verification + +Once all chunks have been accepted, Tendermint issues an `Info` ABCI call to retrieve the +`LastBlockAppHash`. This is compared with the trusted app hash from the chain, retrieved and +verified using the light client. Tendermint also checks that `LastBlockHeight` corresponds to the +height of the snapshot. + +This verification ensures that an application is valid before joining the network. However, the +snapshot restoration may take a long time to complete, so applications may want to employ additional +verification during the restore to detect failures early. This might e.g. include incremental +verification of each chunk against the app hash (using bundled Merkle proofs), checksums to +protect against data corruption by the disk or network, and so on. However, it is important to +note that the only trusted information available is the app hash, and all other snapshot metadata +can be spoofed by adversaries. + +Apps may also want to consider state sync denial-of-service vectors, where adversaries provide +invalid or harmful snapshots to prevent nodes from joining the network. The application can +counteract this by asking Tendermint to ban peers. As a last resort, node operators can use +P2P configuration options to whitelist a set of trusted peers that can provide valid snapshots. + +#### Transition to Consensus + +Once the snapshots have all been restored, Tendermint gathers additional information necessary for +bootstrapping the node (e.g. chain ID, consensus parameters, validator sets, and block headers) +from the genesis file and light client RPC servers. It also fetches and records the `AppVersion` +from the ABCI application. + +Once the state machine has been restored and Tendermint has gathered this additional +information, it transitions to block sync (if enabled) to fetch any remaining blocks up the chain +head, and then transitions to regular consensus operation. At this point the node operates like +any other node, apart from having a truncated block history at the height of the restored snapshot. diff --git a/spec/abci/client-server.md b/spec/abci/client-server.md new file mode 100644 index 000000000..78c226743 --- /dev/null +++ b/spec/abci/client-server.md @@ -0,0 +1,113 @@ +--- +order: 3 +title: Client and Server +--- + +# Client and Server + +This section is for those looking to implement their own ABCI Server, perhaps in +a new programming language. + +You are expected to have read [ABCI Methods and Types](./abci.md) and [ABCI +Applications](./apps.md). + +## Message Protocol + +The message protocol consists of pairs of requests and responses defined in the +[protobuf file](https://github.com/tendermint/tendermint/blob/master/proto/tendermint/abci/types.proto). + +Some messages have no fields, while others may include byte-arrays, strings, integers, +or custom protobuf types. + +For more details on protobuf, see the [documentation](https://developers.google.com/protocol-buffers/docs/overview). + +For each request, a server should respond with the corresponding +response, where the order of requests is preserved in the order of +responses. + +## Server Implementations + +To use ABCI in your programming language of choice, there must be a ABCI +server in that language. Tendermint supports three implementations of the ABCI, written in Go: + +- In-process ([Golang](https://github.com/tendermint/tendermint/tree/master/abci), [Rust](https://github.com/tendermint/rust-abci)) +- ABCI-socket +- GRPC + +The latter two can be tested using the `abci-cli` by setting the `--abci` flag +appropriately (ie. to `socket` or `grpc`). + +See examples, in various stages of maintenance, in +[Go](https://github.com/tendermint/tendermint/tree/master/abci/server), +[JavaScript](https://github.com/tendermint/js-abci), +[C++](https://github.com/mdyring/cpp-tmsp), and +[Java](https://github.com/jTendermint/jabci). + +### In Process + +The simplest implementation uses function calls within Golang. +This means ABCI applications written in Golang can be compiled with Tendermint Core and run as a single binary. + +### GRPC + +If GRPC is available in your language, this is the easiest approach, +though it will have significant performance overhead. + +To get started with GRPC, copy in the [protobuf +file](https://github.com/tendermint/tendermint/blob/master/proto/tendermint/abci/types.proto) +and compile it using the GRPC plugin for your language. For instance, +for golang, the command is `protoc --go_out=plugins=grpc:. types.proto`. +See the [grpc documentation for more details](http://www.grpc.io/docs/). +`protoc` will autogenerate all the necessary code for ABCI client and +server in your language, including whatever interface your application +must satisfy to be used by the ABCI server for handling requests. + +Note the length-prefixing used in the socket implementation (TSP) does not apply for GRPC. + +### TSP + +Tendermint Socket Protocol is an asynchronous, raw socket server which provides ordered message passing over unix or tcp. +Messages are serialized using Protobuf3 and length-prefixed with a [signed Varint](https://developers.google.com/protocol-buffers/docs/encoding?csw=1#signed-integers) + +If GRPC is not available in your language, or you require higher +performance, or otherwise enjoy programming, you may implement your own +ABCI server using the Tendermint Socket Protocol. The first step is still to auto-generate the relevant data +types and codec in your language using `protoc`. In addition to being proto3 encoded, messages coming over +the socket are length-prefixed to facilitate use as a streaming protocol. proto3 doesn't have an +official length-prefix standard, so we use our own. The first byte in +the prefix represents the length of the Big Endian encoded length. The +remaining bytes in the prefix are the Big Endian encoded length. + +For example, if the proto3 encoded ABCI message is 0xDEADBEEF (4 +bytes), the length-prefixed message is 0x0104DEADBEEF. If the proto3 +encoded ABCI message is 65535 bytes long, the length-prefixed message +would be like 0x02FFFF.... + +The benefit of using this `varint` encoding over the old version (where integers were encoded as `` is that +it is the standard way to encode integers in Protobuf. It is also generally shorter. + +As noted above, this prefixing does not apply for GRPC. + +An ABCI server must also be able to support multiple connections, as +Tendermint uses four connections. + +### Async vs Sync + +The main ABCI server (ie. non-GRPC) provides ordered asynchronous messages. +This is useful for DeliverTx and CheckTx, since it allows Tendermint to forward +transactions to the app before it's finished processing previous ones. + +Thus, DeliverTx and CheckTx messages are sent asynchronously, while all other +messages are sent synchronously. + +## Client + +There are currently two use-cases for an ABCI client. One is a testing +tool, as in the `abci-cli`, which allows ABCI requests to be sent via +command line. The other is a consensus engine, such as Tendermint Core, +which makes requests to the application every time a new transaction is +received or a block is committed. + +It is unlikely that you will need to implement a client. For details of +our client, see +[here](https://github.com/tendermint/tendermint/tree/master/abci/client). diff --git a/spec/blockchain/blockchain.md b/spec/blockchain/blockchain.md new file mode 100644 index 000000000..fcc080ee7 --- /dev/null +++ b/spec/blockchain/blockchain.md @@ -0,0 +1,3 @@ +# Blockchain + +Deprecated see [core/data_structures.md](../core/data_structures.md) diff --git a/spec/blockchain/encoding.md b/spec/blockchain/encoding.md new file mode 100644 index 000000000..aa2c9ab3f --- /dev/null +++ b/spec/blockchain/encoding.md @@ -0,0 +1,3 @@ +# Encoding + +Deprecated see [core/data_structures.md](../core/encoding.md) diff --git a/spec/blockchain/readme.md b/spec/blockchain/readme.md new file mode 100644 index 000000000..10ad46690 --- /dev/null +++ b/spec/blockchain/readme.md @@ -0,0 +1,14 @@ +--- +order: 1 +parent: + title: Blockchain + order: false +--- + +# Blockchain + +This section describes the core types and functionality of the Tendermint protocol implementation. + +[Core Data Structures](../core/data_structures.md) +[Encoding](../core/encoding.md) +[State](../core/state.md) diff --git a/spec/blockchain/state.md b/spec/blockchain/state.md new file mode 100644 index 000000000..f4f1d9525 --- /dev/null +++ b/spec/blockchain/state.md @@ -0,0 +1,3 @@ +# State + +Deprecated see [core/state.md](../core/state.md) diff --git a/spec/consensus/bft-time.md b/spec/consensus/bft-time.md new file mode 100644 index 000000000..cec3b91ab --- /dev/null +++ b/spec/consensus/bft-time.md @@ -0,0 +1,55 @@ +--- +order: 2 +--- +# BFT Time + +Tendermint provides a deterministic, Byzantine fault-tolerant, source of time. +Time in Tendermint is defined with the Time field of the block header. + +It satisfies the following properties: + +- Time Monotonicity: Time is monotonically increasing, i.e., given +a header H1 for height h1 and a header H2 for height `h2 = h1 + 1`, `H1.Time < H2.Time`. +- Time Validity: Given a set of Commit votes that forms the `block.LastCommit` field, a range of +valid values for the Time field of the block header is defined only by +Precommit messages (from the LastCommit field) sent by correct processes, i.e., +a faulty process cannot arbitrarily increase the Time value. + +In the context of Tendermint, time is of type int64 and denotes UNIX time in milliseconds, i.e., +corresponds to the number of milliseconds since January 1, 1970. Before defining rules that need to be enforced by the +Tendermint consensus protocol, so the properties above holds, we introduce the following definition: + +- median of a Commit is equal to the median of `Vote.Time` fields of the `Vote` messages, +where the value of `Vote.Time` is counted number of times proportional to the process voting power. As in Tendermint +the voting power is not uniform (one process one vote), a vote message is actually an aggregator of the same votes whose +number is equal to the voting power of the process that has casted the corresponding votes message. + +Let's consider the following example: + +- we have four processes p1, p2, p3 and p4, with the following voting power distribution (p1, 23), (p2, 27), (p3, 10) +and (p4, 10). The total voting power is 70 (`N = 3f+1`, where `N` is the total voting power, and `f` is the maximum voting +power of the faulty processes), so we assume that the faulty processes have at most 23 of voting power. +Furthermore, we have the following vote messages in some LastCommit field (we ignore all fields except Time field): + - (p1, 100), (p2, 98), (p3, 1000), (p4, 500). We assume that p3 and p4 are faulty processes. Let's assume that the + `block.LastCommit` message contains votes of processes p2, p3 and p4. Median is then chosen the following way: + the value 98 is counted 27 times, the value 1000 is counted 10 times and the value 500 is counted also 10 times. + So the median value will be the value 98. No matter what set of messages with at least `2f+1` voting power we + choose, the median value will always be between the values sent by correct processes. + +We ensure Time Monotonicity and Time Validity properties by the following rules: + +- let rs denotes `RoundState` (consensus internal state) of some process. Then +`rs.ProposalBlock.Header.Time == median(rs.LastCommit) && +rs.Proposal.Timestamp == rs.ProposalBlock.Header.Time`. + +- Furthermore, when creating the `vote` message, the following rules for determining `vote.Time` field should hold: + + - if `rs.LockedBlock` is defined then + `vote.Time = max(rs.LockedBlock.Timestamp + time.Millisecond, time.Now())`, where `time.Now()` + denotes local Unix time in milliseconds + + - else if `rs.Proposal` is defined then + `vote.Time = max(rs.Proposal.Timestamp + time.Millisecond,, time.Now())`, + + - otherwise, `vote.Time = time.Now())`. In this case vote is for `nil` so it is not taken into account for + the timestamp of the next block. diff --git a/spec/consensus/consensus-paper/IEEEtran.bst b/spec/consensus/consensus-paper/IEEEtran.bst new file mode 100644 index 000000000..53fbc030a --- /dev/null +++ b/spec/consensus/consensus-paper/IEEEtran.bst @@ -0,0 +1,2417 @@ +%% +%% IEEEtran.bst +%% BibTeX Bibliography Style file for IEEE Journals and Conferences (unsorted) +%% Version 1.12 (2007/01/11) +%% +%% Copyright (c) 2003-2007 Michael Shell +%% +%% Original starting code base and algorithms obtained from the output of +%% Patrick W. Daly's makebst package as well as from prior versions of +%% IEEE BibTeX styles: +%% +%% 1. Howard Trickey and Oren Patashnik's ieeetr.bst (1985/1988) +%% 2. Silvano Balemi and Richard H. Roy's IEEEbib.bst (1993) +%% +%% Support sites: +%% http://www.michaelshell.org/tex/ieeetran/ +%% http://www.ctan.org/tex-archive/macros/latex/contrib/IEEEtran/ +%% and/or +%% http://www.ieee.org/ +%% +%% For use with BibTeX version 0.99a or later +%% +%% This is a numerical citation style. +%% +%%************************************************************************* +%% Legal Notice: +%% This code is offered as-is without any warranty either expressed or +%% implied; without even the implied warranty of MERCHANTABILITY or +%% FITNESS FOR A PARTICULAR PURPOSE! +%% User assumes all risk. +%% In no event shall IEEE or any contributor to this code be liable for +%% any damages or losses, including, but not limited to, incidental, +%% consequential, or any other damages, resulting from the use or misuse +%% of any information contained here. +%% +%% All comments are the opinions of their respective authors and are not +%% necessarily endorsed by the IEEE. +%% +%% This work is distributed under the LaTeX Project Public License (LPPL) +%% ( http://www.latex-project.org/ ) version 1.3, and may be freely used, +%% distributed and modified. A copy of the LPPL, version 1.3, is included +%% in the base LaTeX documentation of all distributions of LaTeX released +%% 2003/12/01 or later. +%% Retain all contribution notices and credits. +%% ** Modified files should be clearly indicated as such, including ** +%% ** renaming them and changing author support contact information. ** +%% +%% File list of work: IEEEabrv.bib, IEEEfull.bib, IEEEexample.bib, +%% IEEEtran.bst, IEEEtranS.bst, IEEEtranSA.bst, +%% IEEEtranN.bst, IEEEtranSN.bst, IEEEtran_bst_HOWTO.pdf +%%************************************************************************* +% +% +% Changelog: +% +% 1.00 (2002/08/13) Initial release +% +% 1.10 (2002/09/27) +% 1. Corrected minor bug for improperly formed warning message when a +% book was not given a title. Thanks to Ming Kin Lai for reporting this. +% 2. Added support for CTLname_format_string and CTLname_latex_cmd fields +% in the BST control entry type. +% +% 1.11 (2003/04/02) +% 1. Fixed bug with URLs containing underscores when using url.sty. Thanks +% to Ming Kin Lai for reporting this. +% +% 1.12 (2007/01/11) +% 1. Fixed bug with unwanted comma before "et al." when an entry contained +% more than two author names. Thanks to Pallav Gupta for reporting this. +% 2. Fixed bug with anomalous closing quote in tech reports that have a +% type, but without a number or address. Thanks to Mehrdad Mirreza for +% reporting this. +% 3. Use braces in \providecommand in begin.bib to better support +% latex2html. TeX style length assignments OK with recent versions +% of latex2html - 1.71 (2002/2/1) or later is strongly recommended. +% Use of the language field still causes trouble with latex2html. +% Thanks to Federico Beffa for reporting this. +% 4. Added IEEEtran.bst ID and version comment string to .bbl output. +% 5. Provide a \BIBdecl hook that allows the user to execute commands +% just prior to the first entry. +% 6. Use default urlstyle (is using url.sty) of "same" rather than rm to +% better work with a wider variety of bibliography styles. +% 7. Changed month abbreviations from Sept., July and June to Sep., Jul., +% and Jun., respectively, as IEEE now does. Thanks to Moritz Borgmann +% for reporting this. +% 8. Control entry types should not be considered when calculating longest +% label width. +% 9. Added alias www for electronic/online. +% 10. Added CTLname_url_prefix control entry type. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% DEFAULTS FOR THE CONTROLS OF THE BST STYLE %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% These are the defaults for the user adjustable controls. The values used +% here can be overridden by the user via IEEEtranBSTCTL entry type. + +% NOTE: The recommended LaTeX command to invoke a control entry type is: +% +%\makeatletter +%\def\bstctlcite{\@ifnextchar[{\@bstctlcite}{\@bstctlcite[@auxout]}} +%\def\@bstctlcite[#1]#2{\@bsphack +% \@for\@citeb:=#2\do{% +% \edef\@citeb{\expandafter\@firstofone\@citeb}% +% \if@filesw\immediate\write\csname #1\endcsname{\string\citation{\@citeb}}\fi}% +% \@esphack} +%\makeatother +% +% It is called at the start of the document, before the first \cite, like: +% \bstctlcite{IEEEexample:BSTcontrol} +% +% IEEEtran.cls V1.6 and later does provide this command. + + + +% #0 turns off the display of the number for articles. +% #1 enables +FUNCTION {default.is.use.number.for.article} { #1 } + + +% #0 turns off the display of the paper and type fields in @inproceedings. +% #1 enables +FUNCTION {default.is.use.paper} { #1 } + + +% #0 turns off the forced use of "et al." +% #1 enables +FUNCTION {default.is.forced.et.al} { #0 } + +% The maximum number of names that can be present beyond which an "et al." +% usage is forced. Be sure that num.names.shown.with.forced.et.al (below) +% is not greater than this value! +% Note: There are many instances of references in IEEE journals which have +% a very large number of authors as well as instances in which "et al." is +% used profusely. +FUNCTION {default.max.num.names.before.forced.et.al} { #10 } + +% The number of names that will be shown with a forced "et al.". +% Must be less than or equal to max.num.names.before.forced.et.al +FUNCTION {default.num.names.shown.with.forced.et.al} { #1 } + + +% #0 turns off the alternate interword spacing for entries with URLs. +% #1 enables +FUNCTION {default.is.use.alt.interword.spacing} { #1 } + +% If alternate interword spacing for entries with URLs is enabled, this is +% the interword spacing stretch factor that will be used. For example, the +% default "4" here means that the interword spacing in entries with URLs can +% stretch to four times normal. Does not have to be an integer. Note that +% the value specified here can be overridden by the user in their LaTeX +% code via a command such as: +% "\providecommand\BIBentryALTinterwordstretchfactor{1.5}" in addition to +% that via the IEEEtranBSTCTL entry type. +FUNCTION {default.ALTinterwordstretchfactor} { "4" } + + +% #0 turns off the "dashification" of repeated (i.e., identical to those +% of the previous entry) names. IEEE normally does this. +% #1 enables +FUNCTION {default.is.dash.repeated.names} { #1 } + + +% The default name format control string. +FUNCTION {default.name.format.string}{ "{f.~}{vv~}{ll}{, jj}" } + + +% The default LaTeX font command for the names. +FUNCTION {default.name.latex.cmd}{ "" } + + +% The default URL prefix. +FUNCTION {default.name.url.prefix}{ "[Online]. Available:" } + + +% Other controls that cannot be accessed via IEEEtranBSTCTL entry type. + +% #0 turns off the terminal startup banner/completed message so as to +% operate more quietly. +% #1 enables +FUNCTION {is.print.banners.to.terminal} { #1 } + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% FILE VERSION AND BANNER %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +FUNCTION{bst.file.version} { "1.12" } +FUNCTION{bst.file.date} { "2007/01/11" } +FUNCTION{bst.file.website} { "http://www.michaelshell.org/tex/ieeetran/bibtex/" } + +FUNCTION {banner.message} +{ is.print.banners.to.terminal + { "-- IEEEtran.bst version" " " * bst.file.version * + " (" * bst.file.date * ") " * "by Michael Shell." * + top$ + "-- " bst.file.website * + top$ + "-- See the " quote$ * "IEEEtran_bst_HOWTO.pdf" * quote$ * " manual for usage information." * + top$ + } + { skip$ } + if$ +} + +FUNCTION {completed.message} +{ is.print.banners.to.terminal + { "" + top$ + "Done." + top$ + } + { skip$ } + if$ +} + + + + +%%%%%%%%%%%%%%%%%%%%%% +%% STRING CONSTANTS %% +%%%%%%%%%%%%%%%%%%%%%% + +FUNCTION {bbl.and}{ "and" } +FUNCTION {bbl.etal}{ "et~al." } +FUNCTION {bbl.editors}{ "eds." } +FUNCTION {bbl.editor}{ "ed." } +FUNCTION {bbl.edition}{ "ed." } +FUNCTION {bbl.volume}{ "vol." } +FUNCTION {bbl.of}{ "of" } +FUNCTION {bbl.number}{ "no." } +FUNCTION {bbl.in}{ "in" } +FUNCTION {bbl.pages}{ "pp." } +FUNCTION {bbl.page}{ "p." } +FUNCTION {bbl.chapter}{ "ch." } +FUNCTION {bbl.paper}{ "paper" } +FUNCTION {bbl.part}{ "pt." } +FUNCTION {bbl.patent}{ "Patent" } +FUNCTION {bbl.patentUS}{ "U.S." } +FUNCTION {bbl.revision}{ "Rev." } +FUNCTION {bbl.series}{ "ser." } +FUNCTION {bbl.standard}{ "Std." } +FUNCTION {bbl.techrep}{ "Tech. Rep." } +FUNCTION {bbl.mthesis}{ "Master's thesis" } +FUNCTION {bbl.phdthesis}{ "Ph.D. dissertation" } +FUNCTION {bbl.st}{ "st" } +FUNCTION {bbl.nd}{ "nd" } +FUNCTION {bbl.rd}{ "rd" } +FUNCTION {bbl.th}{ "th" } + + +% This is the LaTeX spacer that is used when a larger than normal space +% is called for (such as just before the address:publisher). +FUNCTION {large.space} { "\hskip 1em plus 0.5em minus 0.4em\relax " } + +% The LaTeX code for dashes that are used to represent repeated names. +% Note: Some older IEEE journals used something like +% "\rule{0.275in}{0.5pt}\," which is fairly thick and runs right along +% the baseline. However, IEEE now uses a thinner, above baseline, +% six dash long sequence. +FUNCTION {repeated.name.dashes} { "------" } + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% PREDEFINED STRING MACROS %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +MACRO {jan} {"Jan."} +MACRO {feb} {"Feb."} +MACRO {mar} {"Mar."} +MACRO {apr} {"Apr."} +MACRO {may} {"May"} +MACRO {jun} {"Jun."} +MACRO {jul} {"Jul."} +MACRO {aug} {"Aug."} +MACRO {sep} {"Sep."} +MACRO {oct} {"Oct."} +MACRO {nov} {"Nov."} +MACRO {dec} {"Dec."} + + + +%%%%%%%%%%%%%%%%%% +%% ENTRY FIELDS %% +%%%%%%%%%%%%%%%%%% + +ENTRY + { address + assignee + author + booktitle + chapter + day + dayfiled + edition + editor + howpublished + institution + intype + journal + key + language + month + monthfiled + nationality + note + number + organization + pages + paper + publisher + school + series + revision + title + type + url + volume + year + yearfiled + CTLuse_article_number + CTLuse_paper + CTLuse_forced_etal + CTLmax_names_forced_etal + CTLnames_show_etal + CTLuse_alt_spacing + CTLalt_stretch_factor + CTLdash_repeated_names + CTLname_format_string + CTLname_latex_cmd + CTLname_url_prefix + } + {} + { label } + + + + +%%%%%%%%%%%%%%%%%%%%%%% +%% INTEGER VARIABLES %% +%%%%%%%%%%%%%%%%%%%%%%% + +INTEGERS { prev.status.punct this.status.punct punct.std + punct.no punct.comma punct.period + prev.status.space this.status.space space.std + space.no space.normal space.large + prev.status.quote this.status.quote quote.std + quote.no quote.close + prev.status.nline this.status.nline nline.std + nline.no nline.newblock + status.cap cap.std + cap.no cap.yes} + +INTEGERS { longest.label.width multiresult nameptr namesleft number.label numnames } + +INTEGERS { is.use.number.for.article + is.use.paper + is.forced.et.al + max.num.names.before.forced.et.al + num.names.shown.with.forced.et.al + is.use.alt.interword.spacing + is.dash.repeated.names} + + +%%%%%%%%%%%%%%%%%%%%%% +%% STRING VARIABLES %% +%%%%%%%%%%%%%%%%%%%%%% + +STRINGS { bibinfo + longest.label + oldname + s + t + ALTinterwordstretchfactor + name.format.string + name.latex.cmd + name.url.prefix} + + + + +%%%%%%%%%%%%%%%%%%%%%%%%% +%% LOW LEVEL FUNCTIONS %% +%%%%%%%%%%%%%%%%%%%%%%%%% + +FUNCTION {initialize.controls} +{ default.is.use.number.for.article 'is.use.number.for.article := + default.is.use.paper 'is.use.paper := + default.is.forced.et.al 'is.forced.et.al := + default.max.num.names.before.forced.et.al 'max.num.names.before.forced.et.al := + default.num.names.shown.with.forced.et.al 'num.names.shown.with.forced.et.al := + default.is.use.alt.interword.spacing 'is.use.alt.interword.spacing := + default.is.dash.repeated.names 'is.dash.repeated.names := + default.ALTinterwordstretchfactor 'ALTinterwordstretchfactor := + default.name.format.string 'name.format.string := + default.name.latex.cmd 'name.latex.cmd := + default.name.url.prefix 'name.url.prefix := +} + + +% This IEEEtran.bst features a very powerful and flexible mechanism for +% controlling the capitalization, punctuation, spacing, quotation, and +% newlines of the formatted entry fields. (Note: IEEEtran.bst does not need +% or use the newline/newblock feature, but it has been implemented for +% possible future use.) The output states of IEEEtran.bst consist of +% multiple independent attributes and, as such, can be thought of as being +% vectors, rather than the simple scalar values ("before.all", +% "mid.sentence", etc.) used in most other .bst files. +% +% The more flexible and complex design used here was motivated in part by +% IEEE's rather unusual bibliography style. For example, IEEE ends the +% previous field item with a period and large space prior to the publisher +% address; the @electronic entry types use periods as inter-item punctuation +% rather than the commas used by the other entry types; and URLs are never +% followed by periods even though they are the last item in the entry. +% Although it is possible to accommodate these features with the conventional +% output state system, the seemingly endless exceptions make for convoluted, +% unreliable and difficult to maintain code. +% +% IEEEtran.bst's output state system can be easily understood via a simple +% illustration of two most recently formatted entry fields (on the stack): +% +% CURRENT_ITEM +% "PREVIOUS_ITEM +% +% which, in this example, is to eventually appear in the bibliography as: +% +% "PREVIOUS_ITEM," CURRENT_ITEM +% +% It is the job of the output routine to take the previous item off of the +% stack (while leaving the current item at the top of the stack), apply its +% trailing punctuation (including closing quote marks) and spacing, and then +% to write the result to BibTeX's output buffer: +% +% "PREVIOUS_ITEM," +% +% Punctuation (and spacing) between items is often determined by both of the +% items rather than just the first one. The presence of quotation marks +% further complicates the situation because, in standard English, trailing +% punctuation marks are supposed to be contained within the quotes. +% +% IEEEtran.bst maintains two output state (aka "status") vectors which +% correspond to the previous and current (aka "this") items. Each vector +% consists of several independent attributes which track punctuation, +% spacing, quotation, and newlines. Capitalization status is handled by a +% separate scalar because the format routines, not the output routine, +% handle capitalization and, therefore, there is no need to maintain the +% capitalization attribute for both the "previous" and "this" items. +% +% When a format routine adds a new item, it copies the current output status +% vector to the previous output status vector and (usually) resets the +% current (this) output status vector to a "standard status" vector. Using a +% "standard status" vector in this way allows us to redefine what we mean by +% "standard status" at the start of each entry handler and reuse the same +% format routines under the various inter-item separation schemes. For +% example, the standard status vector for the @book entry type may use +% commas for item separators, while the @electronic type may use periods, +% yet both entry handlers exploit many of the exact same format routines. +% +% Because format routines have write access to the output status vector of +% the previous item, they can override the punctuation choices of the +% previous format routine! Therefore, it becomes trivial to implement rules +% such as "Always use a period and a large space before the publisher." By +% pushing the generation of the closing quote mark to the output routine, we +% avoid all the problems caused by having to close a quote before having all +% the information required to determine what the punctuation should be. +% +% The IEEEtran.bst output state system can easily be expanded if needed. +% For instance, it is easy to add a "space.tie" attribute value if the +% bibliography rules mandate that two items have to be joined with an +% unbreakable space. + +FUNCTION {initialize.status.constants} +{ #0 'punct.no := + #1 'punct.comma := + #2 'punct.period := + #0 'space.no := + #1 'space.normal := + #2 'space.large := + #0 'quote.no := + #1 'quote.close := + #0 'cap.no := + #1 'cap.yes := + #0 'nline.no := + #1 'nline.newblock := +} + +FUNCTION {std.status.using.comma} +{ punct.comma 'punct.std := + space.normal 'space.std := + quote.no 'quote.std := + nline.no 'nline.std := + cap.no 'cap.std := +} + +FUNCTION {std.status.using.period} +{ punct.period 'punct.std := + space.normal 'space.std := + quote.no 'quote.std := + nline.no 'nline.std := + cap.yes 'cap.std := +} + +FUNCTION {initialize.prev.this.status} +{ punct.no 'prev.status.punct := + space.no 'prev.status.space := + quote.no 'prev.status.quote := + nline.no 'prev.status.nline := + punct.no 'this.status.punct := + space.no 'this.status.space := + quote.no 'this.status.quote := + nline.no 'this.status.nline := + cap.yes 'status.cap := +} + +FUNCTION {this.status.std} +{ punct.std 'this.status.punct := + space.std 'this.status.space := + quote.std 'this.status.quote := + nline.std 'this.status.nline := +} + +FUNCTION {cap.status.std}{ cap.std 'status.cap := } + +FUNCTION {this.to.prev.status} +{ this.status.punct 'prev.status.punct := + this.status.space 'prev.status.space := + this.status.quote 'prev.status.quote := + this.status.nline 'prev.status.nline := +} + + +FUNCTION {not} +{ { #0 } + { #1 } + if$ +} + +FUNCTION {and} +{ { skip$ } + { pop$ #0 } + if$ +} + +FUNCTION {or} +{ { pop$ #1 } + { skip$ } + if$ +} + + +% convert the strings "yes" or "no" to #1 or #0 respectively +FUNCTION {yes.no.to.int} +{ "l" change.case$ duplicate$ + "yes" = + { pop$ #1 } + { duplicate$ "no" = + { pop$ #0 } + { "unknown boolean " quote$ * swap$ * quote$ * + " in " * cite$ * warning$ + #0 + } + if$ + } + if$ +} + + +% pushes true if the single char string on the stack is in the +% range of "0" to "9" +FUNCTION {is.num} +{ chr.to.int$ + duplicate$ "0" chr.to.int$ < not + swap$ "9" chr.to.int$ > not and +} + +% multiplies the integer on the stack by a factor of 10 +FUNCTION {bump.int.mag} +{ #0 'multiresult := + { duplicate$ #0 > } + { #1 - + multiresult #10 + + 'multiresult := + } + while$ +pop$ +multiresult +} + +% converts a single character string on the stack to an integer +FUNCTION {char.to.integer} +{ duplicate$ + is.num + { chr.to.int$ "0" chr.to.int$ - } + {"noninteger character " quote$ * swap$ * quote$ * + " in integer field of " * cite$ * warning$ + #0 + } + if$ +} + +% converts a string on the stack to an integer +FUNCTION {string.to.integer} +{ duplicate$ text.length$ 'namesleft := + #1 'nameptr := + #0 'numnames := + { nameptr namesleft > not } + { duplicate$ nameptr #1 substring$ + char.to.integer numnames bump.int.mag + + 'numnames := + nameptr #1 + + 'nameptr := + } + while$ +pop$ +numnames +} + + + + +% The output routines write out the *next* to the top (previous) item on the +% stack, adding punctuation and such as needed. Since IEEEtran.bst maintains +% the output status for the top two items on the stack, these output +% routines have to consider the previous output status (which corresponds to +% the item that is being output). Full independent control of punctuation, +% closing quote marks, spacing, and newblock is provided. +% +% "output.nonnull" does not check for the presence of a previous empty +% item. +% +% "output" does check for the presence of a previous empty item and will +% remove an empty item rather than outputing it. +% +% "output.warn" is like "output", but will issue a warning if it detects +% an empty item. + +FUNCTION {output.nonnull} +{ swap$ + prev.status.punct punct.comma = + { "," * } + { skip$ } + if$ + prev.status.punct punct.period = + { add.period$ } + { skip$ } + if$ + prev.status.quote quote.close = + { "''" * } + { skip$ } + if$ + prev.status.space space.normal = + { " " * } + { skip$ } + if$ + prev.status.space space.large = + { large.space * } + { skip$ } + if$ + write$ + prev.status.nline nline.newblock = + { newline$ "\newblock " write$ } + { skip$ } + if$ +} + +FUNCTION {output} +{ duplicate$ empty$ + 'pop$ + 'output.nonnull + if$ +} + +FUNCTION {output.warn} +{ 't := + duplicate$ empty$ + { pop$ "empty " t * " in " * cite$ * warning$ } + 'output.nonnull + if$ +} + +% "fin.entry" is the output routine that handles the last item of the entry +% (which will be on the top of the stack when "fin.entry" is called). + +FUNCTION {fin.entry} +{ this.status.punct punct.no = + { skip$ } + { add.period$ } + if$ + this.status.quote quote.close = + { "''" * } + { skip$ } + if$ +write$ +newline$ +} + + +FUNCTION {is.last.char.not.punct} +{ duplicate$ + "}" * add.period$ + #-1 #1 substring$ "." = +} + +FUNCTION {is.multiple.pages} +{ 't := + #0 'multiresult := + { multiresult not + t empty$ not + and + } + { t #1 #1 substring$ + duplicate$ "-" = + swap$ duplicate$ "," = + swap$ "+" = + or or + { #1 'multiresult := } + { t #2 global.max$ substring$ 't := } + if$ + } + while$ + multiresult +} + +FUNCTION {capitalize}{ "u" change.case$ "t" change.case$ } + +FUNCTION {emphasize} +{ duplicate$ empty$ + { pop$ "" } + { "\emph{" swap$ * "}" * } + if$ +} + +FUNCTION {do.name.latex.cmd} +{ name.latex.cmd + empty$ + { skip$ } + { name.latex.cmd "{" * swap$ * "}" * } + if$ +} + +% IEEEtran.bst uses its own \BIBforeignlanguage command which directly +% invokes the TeX hyphenation patterns without the need of the Babel +% package. Babel does a lot more than switch hyphenation patterns and +% its loading can cause unintended effects in many class files (such as +% IEEEtran.cls). +FUNCTION {select.language} +{ duplicate$ empty$ 'pop$ + { language empty$ 'skip$ + { "\BIBforeignlanguage{" language * "}{" * swap$ * "}" * } + if$ + } + if$ +} + +FUNCTION {tie.or.space.prefix} +{ duplicate$ text.length$ #3 < + { "~" } + { " " } + if$ + swap$ +} + +FUNCTION {get.bbl.editor} +{ editor num.names$ #1 > 'bbl.editors 'bbl.editor if$ } + +FUNCTION {space.word}{ " " swap$ * " " * } + + +% Field Conditioners, Converters, Checkers and External Interfaces + +FUNCTION {empty.field.to.null.string} +{ duplicate$ empty$ + { pop$ "" } + { skip$ } + if$ +} + +FUNCTION {either.or.check} +{ empty$ + { pop$ } + { "can't use both " swap$ * " fields in " * cite$ * warning$ } + if$ +} + +FUNCTION {empty.entry.warn} +{ author empty$ title empty$ howpublished empty$ + month empty$ year empty$ note empty$ url empty$ + and and and and and and + { "all relevant fields are empty in " cite$ * warning$ } + 'skip$ + if$ +} + + +% The bibinfo system provides a way for the electronic parsing/acquisition +% of a bibliography's contents as is done by ReVTeX. For example, a field +% could be entered into the bibliography as: +% \bibinfo{volume}{2} +% Only the "2" would show up in the document, but the LaTeX \bibinfo command +% could do additional things with the information. IEEEtran.bst does provide +% a \bibinfo command via "\providecommand{\bibinfo}[2]{#2}". However, it is +% currently not used as the bogus bibinfo functions defined here output the +% entry values directly without the \bibinfo wrapper. The bibinfo functions +% themselves (and the calls to them) are retained for possible future use. +% +% bibinfo.check avoids acting on missing fields while bibinfo.warn will +% issue a warning message if a missing field is detected. Prior to calling +% the bibinfo functions, the user should push the field value and then its +% name string, in that order. + +FUNCTION {bibinfo.check} +{ swap$ duplicate$ missing$ + { pop$ pop$ "" } + { duplicate$ empty$ + { swap$ pop$ } + { swap$ pop$ } + if$ + } + if$ +} + +FUNCTION {bibinfo.warn} +{ swap$ duplicate$ missing$ + { swap$ "missing " swap$ * " in " * cite$ * warning$ pop$ "" } + { duplicate$ empty$ + { swap$ "empty " swap$ * " in " * cite$ * warning$ } + { swap$ pop$ } + if$ + } + if$ +} + + +% IEEE separates large numbers with more than 4 digits into groups of +% three. IEEE uses a small space to separate these number groups. +% Typical applications include patent and page numbers. + +% number of consecutive digits required to trigger the group separation. +FUNCTION {large.number.trigger}{ #5 } + +% For numbers longer than the trigger, this is the blocksize of the groups. +% The blocksize must be less than the trigger threshold, and 2 * blocksize +% must be greater than the trigger threshold (can't do more than one +% separation on the initial trigger). +FUNCTION {large.number.blocksize}{ #3 } + +% What is actually inserted between the number groups. +FUNCTION {large.number.separator}{ "\," } + +% So as to save on integer variables by reusing existing ones, numnames +% holds the current number of consecutive digits read and nameptr holds +% the number that will trigger an inserted space. +FUNCTION {large.number.separate} +{ 't := + "" + #0 'numnames := + large.number.trigger 'nameptr := + { t empty$ not } + { t #-1 #1 substring$ is.num + { numnames #1 + 'numnames := } + { #0 'numnames := + large.number.trigger 'nameptr := + } + if$ + t #-1 #1 substring$ swap$ * + t #-2 global.max$ substring$ 't := + numnames nameptr = + { duplicate$ #1 nameptr large.number.blocksize - substring$ swap$ + nameptr large.number.blocksize - #1 + global.max$ substring$ + large.number.separator swap$ * * + nameptr large.number.blocksize - 'numnames := + large.number.blocksize #1 + 'nameptr := + } + { skip$ } + if$ + } + while$ +} + +% Converts all single dashes "-" to double dashes "--". +FUNCTION {n.dashify} +{ large.number.separate + 't := + "" + { t empty$ not } + { t #1 #1 substring$ "-" = + { t #1 #2 substring$ "--" = not + { "--" * + t #2 global.max$ substring$ 't := + } + { { t #1 #1 substring$ "-" = } + { "-" * + t #2 global.max$ substring$ 't := + } + while$ + } + if$ + } + { t #1 #1 substring$ * + t #2 global.max$ substring$ 't := + } + if$ + } + while$ +} + + +% This function detects entries with names that are identical to that of +% the previous entry and replaces the repeated names with dashes (if the +% "is.dash.repeated.names" user control is nonzero). +FUNCTION {name.or.dash} +{ 's := + oldname empty$ + { s 'oldname := s } + { s oldname = + { is.dash.repeated.names + { repeated.name.dashes } + { s 'oldname := s } + if$ + } + { s 'oldname := s } + if$ + } + if$ +} + +% Converts the number string on the top of the stack to +% "numerical ordinal form" (e.g., "7" to "7th"). There is +% no artificial limit to the upper bound of the numbers as the +% least significant digit always determines the ordinal form. +FUNCTION {num.to.ordinal} +{ duplicate$ #-1 #1 substring$ "1" = + { bbl.st * } + { duplicate$ #-1 #1 substring$ "2" = + { bbl.nd * } + { duplicate$ #-1 #1 substring$ "3" = + { bbl.rd * } + { bbl.th * } + if$ + } + if$ + } + if$ +} + +% If the string on the top of the stack begins with a number, +% (e.g., 11th) then replace the string with the leading number +% it contains. Otherwise retain the string as-is. s holds the +% extracted number, t holds the part of the string that remains +% to be scanned. +FUNCTION {extract.num} +{ duplicate$ 't := + "" 's := + { t empty$ not } + { t #1 #1 substring$ + t #2 global.max$ substring$ 't := + duplicate$ is.num + { s swap$ * 's := } + { pop$ "" 't := } + if$ + } + while$ + s empty$ + 'skip$ + { pop$ s } + if$ +} + +% Converts the word number string on the top of the stack to +% Arabic string form. Will be successful up to "tenth". +FUNCTION {word.to.num} +{ duplicate$ "l" change.case$ 's := + s "first" = + { pop$ "1" } + { skip$ } + if$ + s "second" = + { pop$ "2" } + { skip$ } + if$ + s "third" = + { pop$ "3" } + { skip$ } + if$ + s "fourth" = + { pop$ "4" } + { skip$ } + if$ + s "fifth" = + { pop$ "5" } + { skip$ } + if$ + s "sixth" = + { pop$ "6" } + { skip$ } + if$ + s "seventh" = + { pop$ "7" } + { skip$ } + if$ + s "eighth" = + { pop$ "8" } + { skip$ } + if$ + s "ninth" = + { pop$ "9" } + { skip$ } + if$ + s "tenth" = + { pop$ "10" } + { skip$ } + if$ +} + + +% Converts the string on the top of the stack to numerical +% ordinal (e.g., "11th") form. +FUNCTION {convert.edition} +{ duplicate$ empty$ 'skip$ + { duplicate$ #1 #1 substring$ is.num + { extract.num + num.to.ordinal + } + { word.to.num + duplicate$ #1 #1 substring$ is.num + { num.to.ordinal } + { "edition ordinal word " quote$ * edition * quote$ * + " may be too high (or improper) for conversion" * " in " * cite$ * warning$ + } + if$ + } + if$ + } + if$ +} + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% LATEX BIBLIOGRAPHY CODE %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +FUNCTION {start.entry} +{ newline$ + "\bibitem{" write$ + cite$ write$ + "}" write$ + newline$ + "" + initialize.prev.this.status +} + +% Here we write out all the LaTeX code that we will need. The most involved +% code sequences are those that control the alternate interword spacing and +% foreign language hyphenation patterns. The heavy use of \providecommand +% gives users a way to override the defaults. Special thanks to Javier Bezos, +% Johannes Braams, Robin Fairbairns, Heiko Oberdiek, Donald Arseneau and all +% the other gurus on comp.text.tex for their help and advice on the topic of +% \selectlanguage, Babel and BibTeX. +FUNCTION {begin.bib} +{ "% Generated by IEEEtran.bst, version: " bst.file.version * " (" * bst.file.date * ")" * + write$ newline$ + preamble$ empty$ 'skip$ + { preamble$ write$ newline$ } + if$ + "\begin{thebibliography}{" longest.label * "}" * + write$ newline$ + "\providecommand{\url}[1]{#1}" + write$ newline$ + "\csname url@samestyle\endcsname" + write$ newline$ + "\providecommand{\newblock}{\relax}" + write$ newline$ + "\providecommand{\bibinfo}[2]{#2}" + write$ newline$ + "\providecommand{\BIBentrySTDinterwordspacing}{\spaceskip=0pt\relax}" + write$ newline$ + "\providecommand{\BIBentryALTinterwordstretchfactor}{" + ALTinterwordstretchfactor * "}" * + write$ newline$ + "\providecommand{\BIBentryALTinterwordspacing}{\spaceskip=\fontdimen2\font plus " + write$ newline$ + "\BIBentryALTinterwordstretchfactor\fontdimen3\font minus \fontdimen4\font\relax}" + write$ newline$ + "\providecommand{\BIBforeignlanguage}[2]{{%" + write$ newline$ + "\expandafter\ifx\csname l@#1\endcsname\relax" + write$ newline$ + "\typeout{** WARNING: IEEEtran.bst: No hyphenation pattern has been}%" + write$ newline$ + "\typeout{** loaded for the language `#1'. Using the pattern for}%" + write$ newline$ + "\typeout{** the default language instead.}%" + write$ newline$ + "\else" + write$ newline$ + "\language=\csname l@#1\endcsname" + write$ newline$ + "\fi" + write$ newline$ + "#2}}" + write$ newline$ + "\providecommand{\BIBdecl}{\relax}" + write$ newline$ + "\BIBdecl" + write$ newline$ +} + +FUNCTION {end.bib} +{ newline$ "\end{thebibliography}" write$ newline$ } + +FUNCTION {if.url.alt.interword.spacing} +{ is.use.alt.interword.spacing + {url empty$ 'skip$ {"\BIBentryALTinterwordspacing" write$ newline$} if$} + { skip$ } + if$ +} + +FUNCTION {if.url.std.interword.spacing} +{ is.use.alt.interword.spacing + {url empty$ 'skip$ {"\BIBentrySTDinterwordspacing" write$ newline$} if$} + { skip$ } + if$ +} + + + + +%%%%%%%%%%%%%%%%%%%%%%%% +%% LONGEST LABEL PASS %% +%%%%%%%%%%%%%%%%%%%%%%%% + +FUNCTION {initialize.longest.label} +{ "" 'longest.label := + #1 'number.label := + #0 'longest.label.width := +} + +FUNCTION {longest.label.pass} +{ type$ "ieeetranbstctl" = + { skip$ } + { number.label int.to.str$ 'label := + number.label #1 + 'number.label := + label width$ longest.label.width > + { label 'longest.label := + label width$ 'longest.label.width := + } + { skip$ } + if$ + } + if$ +} + + + + +%%%%%%%%%%%%%%%%%%%%% +%% FORMAT HANDLERS %% +%%%%%%%%%%%%%%%%%%%%% + +%% Lower Level Formats (used by higher level formats) + +FUNCTION {format.address.org.or.pub.date} +{ 't := + "" + year empty$ + { "empty year in " cite$ * warning$ } + { skip$ } + if$ + address empty$ t empty$ and + year empty$ and month empty$ and + { skip$ } + { this.to.prev.status + this.status.std + cap.status.std + address "address" bibinfo.check * + t empty$ + { skip$ } + { punct.period 'prev.status.punct := + space.large 'prev.status.space := + address empty$ + { skip$ } + { ": " * } + if$ + t * + } + if$ + year empty$ month empty$ and + { skip$ } + { t empty$ address empty$ and + { skip$ } + { ", " * } + if$ + month empty$ + { year empty$ + { skip$ } + { year "year" bibinfo.check * } + if$ + } + { month "month" bibinfo.check * + year empty$ + { skip$ } + { " " * year "year" bibinfo.check * } + if$ + } + if$ + } + if$ + } + if$ +} + + +FUNCTION {format.names} +{ 'bibinfo := + duplicate$ empty$ 'skip$ { + this.to.prev.status + this.status.std + 's := + "" 't := + #1 'nameptr := + s num.names$ 'numnames := + numnames 'namesleft := + { namesleft #0 > } + { s nameptr + name.format.string + format.name$ + bibinfo bibinfo.check + 't := + nameptr #1 > + { nameptr num.names.shown.with.forced.et.al #1 + = + numnames max.num.names.before.forced.et.al > + is.forced.et.al and and + { "others" 't := + #1 'namesleft := + } + { skip$ } + if$ + namesleft #1 > + { ", " * t do.name.latex.cmd * } + { s nameptr "{ll}" format.name$ duplicate$ "others" = + { 't := } + { pop$ } + if$ + t "others" = + { " " * bbl.etal emphasize * } + { numnames #2 > + { "," * } + { skip$ } + if$ + bbl.and + space.word * t do.name.latex.cmd * + } + if$ + } + if$ + } + { t do.name.latex.cmd } + if$ + nameptr #1 + 'nameptr := + namesleft #1 - 'namesleft := + } + while$ + cap.status.std + } if$ +} + + + + +%% Higher Level Formats + +%% addresses/locations + +FUNCTION {format.address} +{ address duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + } + if$ +} + + + +%% author/editor names + +FUNCTION {format.authors}{ author "author" format.names } + +FUNCTION {format.editors} +{ editor "editor" format.names duplicate$ empty$ 'skip$ + { ", " * + get.bbl.editor + capitalize + * + } + if$ +} + + + +%% date + +FUNCTION {format.date} +{ + month "month" bibinfo.check duplicate$ empty$ + year "year" bibinfo.check duplicate$ empty$ + { swap$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + "there's a month but no year in " cite$ * warning$ } + if$ + * + } + { this.to.prev.status + this.status.std + cap.status.std + swap$ 'skip$ + { + swap$ + " " * swap$ + } + if$ + * + } + if$ +} + +FUNCTION {format.date.electronic} +{ month "month" bibinfo.check duplicate$ empty$ + year "year" bibinfo.check duplicate$ empty$ + { swap$ + { pop$ } + { "there's a month but no year in " cite$ * warning$ + pop$ ")" * "(" swap$ * + this.to.prev.status + punct.no 'this.status.punct := + space.normal 'this.status.space := + quote.no 'this.status.quote := + cap.yes 'status.cap := + } + if$ + } + { swap$ + { swap$ pop$ ")" * "(" swap$ * } + { "(" swap$ * ", " * swap$ * ")" * } + if$ + this.to.prev.status + punct.no 'this.status.punct := + space.normal 'this.status.space := + quote.no 'this.status.quote := + cap.yes 'status.cap := + } + if$ +} + + + +%% edition/title + +% Note: IEEE considers the edition to be closely associated with +% the title of a book. So, in IEEEtran.bst the edition is normally handled +% within the formatting of the title. The format.edition function is +% retained here for possible future use. +FUNCTION {format.edition} +{ edition duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + convert.edition + status.cap + { "t" } + { "l" } + if$ change.case$ + "edition" bibinfo.check + "~" * bbl.edition * + cap.status.std + } + if$ +} + +% This is used to format the booktitle of a conference proceedings. +% Here we use the "intype" field to provide the user a way to +% override the word "in" (e.g., with things like "presented at") +% Use of intype stops the emphasis of the booktitle to indicate that +% we no longer mean the written conference proceedings, but the +% conference itself. +FUNCTION {format.in.booktitle} +{ booktitle "booktitle" bibinfo.check duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + select.language + intype missing$ + { emphasize + bbl.in " " * + } + { intype " " * } + if$ + swap$ * + cap.status.std + } + if$ +} + +% This is used to format the booktitle of collection. +% Here the "intype" field is not supported, but "edition" is. +FUNCTION {format.in.booktitle.edition} +{ booktitle "booktitle" bibinfo.check duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + select.language + emphasize + edition empty$ 'skip$ + { ", " * + edition + convert.edition + "l" change.case$ + * "~" * bbl.edition * + } + if$ + bbl.in " " * swap$ * + cap.status.std + } + if$ +} + +FUNCTION {format.article.title} +{ title duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + "t" change.case$ + } + if$ + "title" bibinfo.check + duplicate$ empty$ 'skip$ + { quote.close 'this.status.quote := + is.last.char.not.punct + { punct.std 'this.status.punct := } + { punct.no 'this.status.punct := } + if$ + select.language + "``" swap$ * + cap.status.std + } + if$ +} + +FUNCTION {format.article.title.electronic} +{ title duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + "t" change.case$ + } + if$ + "title" bibinfo.check + duplicate$ empty$ + { skip$ } + { select.language } + if$ +} + +FUNCTION {format.book.title.edition} +{ title "title" bibinfo.check + duplicate$ empty$ + { "empty title in " cite$ * warning$ } + { this.to.prev.status + this.status.std + select.language + emphasize + edition empty$ 'skip$ + { ", " * + edition + convert.edition + status.cap + { "t" } + { "l" } + if$ + change.case$ + * "~" * bbl.edition * + } + if$ + cap.status.std + } + if$ +} + +FUNCTION {format.book.title} +{ title "title" bibinfo.check + duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + select.language + emphasize + } + if$ +} + + + +%% journal + +FUNCTION {format.journal} +{ journal duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + select.language + emphasize + } + if$ +} + + + +%% how published + +FUNCTION {format.howpublished} +{ howpublished duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + } + if$ +} + + + +%% institutions/organization/publishers/school + +FUNCTION {format.institution} +{ institution duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + } + if$ +} + +FUNCTION {format.organization} +{ organization duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + } + if$ +} + +FUNCTION {format.address.publisher.date} +{ publisher "publisher" bibinfo.warn format.address.org.or.pub.date } + +FUNCTION {format.address.publisher.date.nowarn} +{ publisher "publisher" bibinfo.check format.address.org.or.pub.date } + +FUNCTION {format.address.organization.date} +{ organization "organization" bibinfo.check format.address.org.or.pub.date } + +FUNCTION {format.school} +{ school duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + } + if$ +} + + + +%% volume/number/series/chapter/pages + +FUNCTION {format.volume} +{ volume empty.field.to.null.string + duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + bbl.volume + status.cap + { capitalize } + { skip$ } + if$ + swap$ tie.or.space.prefix + "volume" bibinfo.check + * * + cap.status.std + } + if$ +} + +FUNCTION {format.number} +{ number empty.field.to.null.string + duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + status.cap + { bbl.number capitalize } + { bbl.number } + if$ + swap$ tie.or.space.prefix + "number" bibinfo.check + * * + cap.status.std + } + if$ +} + +FUNCTION {format.number.if.use.for.article} +{ is.use.number.for.article + { format.number } + { "" } + if$ +} + +% IEEE does not seem to tie the series so closely with the volume +% and number as is done in other bibliography styles. Instead the +% series is treated somewhat like an extension of the title. +FUNCTION {format.series} +{ series empty$ + { "" } + { this.to.prev.status + this.status.std + bbl.series " " * + series "series" bibinfo.check * + cap.status.std + } + if$ +} + + +FUNCTION {format.chapter} +{ chapter empty$ + { "" } + { this.to.prev.status + this.status.std + type empty$ + { bbl.chapter } + { type "l" change.case$ + "type" bibinfo.check + } + if$ + chapter tie.or.space.prefix + "chapter" bibinfo.check + * * + cap.status.std + } + if$ +} + + +% The intended use of format.paper is for paper numbers of inproceedings. +% The paper type can be overridden via the type field. +% We allow the type to be displayed even if the paper number is absent +% for things like "postdeadline paper" +FUNCTION {format.paper} +{ is.use.paper + { paper empty$ + { type empty$ + { "" } + { this.to.prev.status + this.status.std + type "type" bibinfo.check + cap.status.std + } + if$ + } + { this.to.prev.status + this.status.std + type empty$ + { bbl.paper } + { type "type" bibinfo.check } + if$ + " " * paper + "paper" bibinfo.check + * + cap.status.std + } + if$ + } + { "" } + if$ +} + + +FUNCTION {format.pages} +{ pages duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + duplicate$ is.multiple.pages + { + bbl.pages swap$ + n.dashify + } + { + bbl.page swap$ + } + if$ + tie.or.space.prefix + "pages" bibinfo.check + * * + cap.status.std + } + if$ +} + + + +%% technical report number + +FUNCTION {format.tech.report.number} +{ number "number" bibinfo.check + this.to.prev.status + this.status.std + cap.status.std + type duplicate$ empty$ + { pop$ + bbl.techrep + } + { skip$ } + if$ + "type" bibinfo.check + swap$ duplicate$ empty$ + { pop$ } + { tie.or.space.prefix * * } + if$ +} + + + +%% note + +FUNCTION {format.note} +{ note empty$ + { "" } + { this.to.prev.status + this.status.std + punct.period 'this.status.punct := + note #1 #1 substring$ + duplicate$ "{" = + { skip$ } + { status.cap + { "u" } + { "l" } + if$ + change.case$ + } + if$ + note #2 global.max$ substring$ * "note" bibinfo.check + cap.yes 'status.cap := + } + if$ +} + + + +%% patent + +FUNCTION {format.patent.date} +{ this.to.prev.status + this.status.std + year empty$ + { monthfiled duplicate$ empty$ + { "monthfiled" bibinfo.check pop$ "" } + { "monthfiled" bibinfo.check } + if$ + dayfiled duplicate$ empty$ + { "dayfiled" bibinfo.check pop$ "" * } + { "dayfiled" bibinfo.check + monthfiled empty$ + { "dayfiled without a monthfiled in " cite$ * warning$ + * + } + { " " swap$ * * } + if$ + } + if$ + yearfiled empty$ + { "no year or yearfiled in " cite$ * warning$ } + { yearfiled "yearfiled" bibinfo.check + swap$ + duplicate$ empty$ + { pop$ } + { ", " * swap$ * } + if$ + } + if$ + } + { month duplicate$ empty$ + { "month" bibinfo.check pop$ "" } + { "month" bibinfo.check } + if$ + day duplicate$ empty$ + { "day" bibinfo.check pop$ "" * } + { "day" bibinfo.check + month empty$ + { "day without a month in " cite$ * warning$ + * + } + { " " swap$ * * } + if$ + } + if$ + year "year" bibinfo.check + swap$ + duplicate$ empty$ + { pop$ } + { ", " * swap$ * } + if$ + } + if$ + cap.status.std +} + +FUNCTION {format.patent.nationality.type.number} +{ this.to.prev.status + this.status.std + nationality duplicate$ empty$ + { "nationality" bibinfo.warn pop$ "" } + { "nationality" bibinfo.check + duplicate$ "l" change.case$ "united states" = + { pop$ bbl.patentUS } + { skip$ } + if$ + " " * + } + if$ + type empty$ + { bbl.patent "type" bibinfo.check } + { type "type" bibinfo.check } + if$ + * + number duplicate$ empty$ + { "number" bibinfo.warn pop$ } + { "number" bibinfo.check + large.number.separate + swap$ " " * swap$ * + } + if$ + cap.status.std +} + + + +%% standard + +FUNCTION {format.organization.institution.standard.type.number} +{ this.to.prev.status + this.status.std + organization duplicate$ empty$ + { pop$ + institution duplicate$ empty$ + { "institution" bibinfo.warn } + { "institution" bibinfo.warn " " * } + if$ + } + { "organization" bibinfo.warn " " * } + if$ + type empty$ + { bbl.standard "type" bibinfo.check } + { type "type" bibinfo.check } + if$ + * + number duplicate$ empty$ + { "number" bibinfo.check pop$ } + { "number" bibinfo.check + large.number.separate + swap$ " " * swap$ * + } + if$ + cap.status.std +} + +FUNCTION {format.revision} +{ revision empty$ + { "" } + { this.to.prev.status + this.status.std + bbl.revision + revision tie.or.space.prefix + "revision" bibinfo.check + * * + cap.status.std + } + if$ +} + + +%% thesis + +FUNCTION {format.master.thesis.type} +{ this.to.prev.status + this.status.std + type empty$ + { + bbl.mthesis + } + { + type "type" bibinfo.check + } + if$ +cap.status.std +} + +FUNCTION {format.phd.thesis.type} +{ this.to.prev.status + this.status.std + type empty$ + { + bbl.phdthesis + } + { + type "type" bibinfo.check + } + if$ +cap.status.std +} + + + +%% URL + +FUNCTION {format.url} +{ url empty$ + { "" } + { this.to.prev.status + this.status.std + cap.yes 'status.cap := + name.url.prefix " " * + "\url{" * url * "}" * + punct.no 'this.status.punct := + punct.period 'prev.status.punct := + space.normal 'this.status.space := + space.normal 'prev.status.space := + quote.no 'this.status.quote := + } + if$ +} + + + + +%%%%%%%%%%%%%%%%%%%% +%% ENTRY HANDLERS %% +%%%%%%%%%%%%%%%%%%%% + + +% Note: In many journals, IEEE (or the authors) tend not to show the number +% for articles, so the display of the number is controlled here by the +% switch "is.use.number.for.article" +FUNCTION {article} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.journal "journal" bibinfo.check "journal" output.warn + format.volume output + format.number.if.use.for.article output + format.pages output + format.date "year" output.warn + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {book} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + author empty$ + { format.editors "author and editor" output.warn } + { format.authors output.nonnull } + if$ + name.or.dash + format.book.title.edition output + format.series output + author empty$ + { skip$ } + { format.editors output } + if$ + format.address.publisher.date output + format.volume output + format.number output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {booklet} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.article.title "title" output.warn + format.howpublished "howpublished" bibinfo.check output + format.organization "organization" bibinfo.check output + format.address "address" bibinfo.check output + format.date output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {electronic} +{ std.status.using.period + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.date.electronic output + format.article.title.electronic output + format.howpublished "howpublished" bibinfo.check output + format.organization "organization" bibinfo.check output + format.address "address" bibinfo.check output + format.note output + format.url output + fin.entry + empty.entry.warn + if.url.std.interword.spacing +} + +FUNCTION {inbook} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + author empty$ + { format.editors "author and editor" output.warn } + { format.authors output.nonnull } + if$ + name.or.dash + format.book.title.edition output + format.series output + format.address.publisher.date output + format.volume output + format.number output + format.chapter output + format.pages output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {incollection} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.in.booktitle.edition "booktitle" output.warn + format.series output + format.editors output + format.address.publisher.date.nowarn output + format.volume output + format.number output + format.chapter output + format.pages output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {inproceedings} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.in.booktitle "booktitle" output.warn + format.series output + format.editors output + format.volume output + format.number output + publisher empty$ + { format.address.organization.date output } + { format.organization "organization" bibinfo.check output + format.address.publisher.date output + } + if$ + format.paper output + format.pages output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {manual} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.book.title.edition "title" output.warn + format.howpublished "howpublished" bibinfo.check output + format.organization "organization" bibinfo.check output + format.address "address" bibinfo.check output + format.date output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {mastersthesis} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.master.thesis.type output.nonnull + format.school "school" bibinfo.warn output + format.address "address" bibinfo.check output + format.date "year" output.warn + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {misc} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.article.title output + format.howpublished "howpublished" bibinfo.check output + format.organization "organization" bibinfo.check output + format.address "address" bibinfo.check output + format.pages output + format.date output + format.note output + format.url output + fin.entry + empty.entry.warn + if.url.std.interword.spacing +} + +FUNCTION {patent} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.article.title output + format.patent.nationality.type.number output + format.patent.date output + format.note output + format.url output + fin.entry + empty.entry.warn + if.url.std.interword.spacing +} + +FUNCTION {periodical} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.editors output + name.or.dash + format.book.title "title" output.warn + format.series output + format.volume output + format.number output + format.organization "organization" bibinfo.check output + format.date "year" output.warn + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {phdthesis} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.phd.thesis.type output.nonnull + format.school "school" bibinfo.warn output + format.address "address" bibinfo.check output + format.date "year" output.warn + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {proceedings} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.editors output + name.or.dash + format.book.title "title" output.warn + format.series output + format.volume output + format.number output + publisher empty$ + { format.address.organization.date output } + { format.organization "organization" bibinfo.check output + format.address.publisher.date output + } + if$ + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {standard} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.book.title "title" output.warn + format.howpublished "howpublished" bibinfo.check output + format.organization.institution.standard.type.number output + format.revision output + format.date output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {techreport} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.howpublished "howpublished" bibinfo.check output + format.institution "institution" bibinfo.warn output + format.address "address" bibinfo.check output + format.tech.report.number output.nonnull + format.date "year" output.warn + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {unpublished} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.date output + format.note "note" output.warn + format.url output + fin.entry + if.url.std.interword.spacing +} + + +% The special entry type which provides the user interface to the +% BST controls +FUNCTION {IEEEtranBSTCTL} +{ is.print.banners.to.terminal + { "** IEEEtran BST control entry " quote$ * cite$ * quote$ * " detected." * + top$ + } + { skip$ } + if$ + CTLuse_article_number + empty$ + { skip$ } + { CTLuse_article_number + yes.no.to.int + 'is.use.number.for.article := + } + if$ + CTLuse_paper + empty$ + { skip$ } + { CTLuse_paper + yes.no.to.int + 'is.use.paper := + } + if$ + CTLuse_forced_etal + empty$ + { skip$ } + { CTLuse_forced_etal + yes.no.to.int + 'is.forced.et.al := + } + if$ + CTLmax_names_forced_etal + empty$ + { skip$ } + { CTLmax_names_forced_etal + string.to.integer + 'max.num.names.before.forced.et.al := + } + if$ + CTLnames_show_etal + empty$ + { skip$ } + { CTLnames_show_etal + string.to.integer + 'num.names.shown.with.forced.et.al := + } + if$ + CTLuse_alt_spacing + empty$ + { skip$ } + { CTLuse_alt_spacing + yes.no.to.int + 'is.use.alt.interword.spacing := + } + if$ + CTLalt_stretch_factor + empty$ + { skip$ } + { CTLalt_stretch_factor + 'ALTinterwordstretchfactor := + "\renewcommand{\BIBentryALTinterwordstretchfactor}{" + ALTinterwordstretchfactor * "}" * + write$ newline$ + } + if$ + CTLdash_repeated_names + empty$ + { skip$ } + { CTLdash_repeated_names + yes.no.to.int + 'is.dash.repeated.names := + } + if$ + CTLname_format_string + empty$ + { skip$ } + { CTLname_format_string + 'name.format.string := + } + if$ + CTLname_latex_cmd + empty$ + { skip$ } + { CTLname_latex_cmd + 'name.latex.cmd := + } + if$ + CTLname_url_prefix + missing$ + { skip$ } + { CTLname_url_prefix + 'name.url.prefix := + } + if$ + + + num.names.shown.with.forced.et.al max.num.names.before.forced.et.al > + { "CTLnames_show_etal cannot be greater than CTLmax_names_forced_etal in " cite$ * warning$ + max.num.names.before.forced.et.al 'num.names.shown.with.forced.et.al := + } + { skip$ } + if$ +} + + +%%%%%%%%%%%%%%%%%%% +%% ENTRY ALIASES %% +%%%%%%%%%%%%%%%%%%% +FUNCTION {conference}{inproceedings} +FUNCTION {online}{electronic} +FUNCTION {internet}{electronic} +FUNCTION {webpage}{electronic} +FUNCTION {www}{electronic} +FUNCTION {default.type}{misc} + + + +%%%%%%%%%%%%%%%%%% +%% MAIN PROGRAM %% +%%%%%%%%%%%%%%%%%% + +READ + +EXECUTE {initialize.controls} +EXECUTE {initialize.status.constants} +EXECUTE {banner.message} + +EXECUTE {initialize.longest.label} +ITERATE {longest.label.pass} + +EXECUTE {begin.bib} +ITERATE {call.type$} +EXECUTE {end.bib} + +EXECUTE{completed.message} + + +%% That's all folks, mds. diff --git a/spec/consensus/consensus-paper/IEEEtran.cls b/spec/consensus/consensus-paper/IEEEtran.cls new file mode 100644 index 000000000..9c967d555 --- /dev/null +++ b/spec/consensus/consensus-paper/IEEEtran.cls @@ -0,0 +1,4733 @@ +%% +%% IEEEtran.cls 2011/11/03 version V1.8 based on +%% IEEEtran.cls 2007/03/05 version V1.7a +%% The changes in V1.8 are made with a single goal in mind: +%% to change the look of the output using the [conference] option +%% and the default font size (10pt) to match the Word template more closely. +%% These changes may well have undesired side effects when other options +%% are in force! +%% +%% +%% This is the official IEEE LaTeX class for authors of the Institute of +%% Electrical and Electronics Engineers (IEEE) Transactions journals and +%% conferences. +%% +%% Support sites: +%% http://www.michaelshell.org/tex/ieeetran/ +%% http://www.ctan.org/tex-archive/macros/latex/contrib/IEEEtran/ +%% and +%% http://www.ieee.org/ +%% +%% Based on the original 1993 IEEEtran.cls, but with many bug fixes +%% and enhancements (from both JVH and MDS) over the 1996/7 version. +%% +%% +%% Contributors: +%% Gerry Murray (1993), Silvano Balemi (1993), +%% Jon Dixon (1996), Peter N"uchter (1996), +%% Juergen von Hagen (2000), and Michael Shell (2001-2007) +%% +%% +%% Copyright (c) 1993-2000 by Gerry Murray, Silvano Balemi, +%% Jon Dixon, Peter N"uchter, +%% Juergen von Hagen +%% and +%% Copyright (c) 2001-2007 by Michael Shell +%% +%% Current maintainer (V1.3 to V1.7): Michael Shell +%% See: +%% http://www.michaelshell.org/ +%% for current contact information. +%% +%% Special thanks to Peter Wilson (CUA) and Donald Arseneau +%% for allowing the inclusion of the \@ifmtarg command +%% from their ifmtarg LaTeX package. +%% +%%************************************************************************* +%% Legal Notice: +%% This code is offered as-is without any warranty either expressed or +%% implied; without even the implied warranty of MERCHANTABILITY or +%% FITNESS FOR A PARTICULAR PURPOSE! +%% User assumes all risk. +%% In no event shall IEEE or any contributor to this code be liable for +%% any damages or losses, including, but not limited to, incidental, +%% consequential, or any other damages, resulting from the use or misuse +%% of any information contained here. +%% +%% All comments are the opinions of their respective authors and are not +%% necessarily endorsed by the IEEE. +%% +%% This work is distributed under the LaTeX Project Public License (LPPL) +%% ( http://www.latex-project.org/ ) version 1.3, and may be freely used, +%% distributed and modified. A copy of the LPPL, version 1.3, is included +%% in the base LaTeX documentation of all distributions of LaTeX released +%% 2003/12/01 or later. +%% Retain all contribution notices and credits. +%% ** Modified files should be clearly indicated as such, including ** +%% ** renaming them and changing author support contact information. ** +%% +%% File list of work: IEEEtran.cls, IEEEtran_HOWTO.pdf, bare_adv.tex, +%% bare_conf.tex, bare_jrnl.tex, bare_jrnl_compsoc.tex +%% +%% Major changes to the user interface should be indicated by an +%% increase in the version numbers. If a version is a beta, it will +%% be indicated with a BETA suffix, i.e., 1.4 BETA. +%% Small changes can be indicated by appending letters to the version +%% such as "IEEEtran_v14a.cls". +%% In all cases, \Providesclass, any \typeout messages to the user, +%% \IEEEtransversionmajor and \IEEEtransversionminor must reflect the +%% correct version information. +%% The changes should also be documented via source comments. +%%************************************************************************* +%% +% +% Available class options +% e.g., \documentclass[10pt,conference]{IEEEtran} +% +% *** choose only one from each category *** +% +% 9pt, 10pt, 11pt, 12pt +% Sets normal font size. The default is 10pt. +% +% conference, journal, technote, peerreview, peerreviewca +% determines format mode - conference papers, journal papers, +% correspondence papers (technotes), or peer review papers. The user +% should also select 9pt when using technote. peerreview is like +% journal mode, but provides for a single-column "cover" title page for +% anonymous peer review. The paper title (without the author names) is +% repeated at the top of the page after the cover page. For peer review +% papers, the \IEEEpeerreviewmaketitle command must be executed (will +% automatically be ignored for non-peerreview modes) at the place the +% cover page is to end, usually just after the abstract (keywords are +% not normally used with peer review papers). peerreviewca is like +% peerreview, but allows the author names to be entered and formatted +% as with conference mode so that author affiliation and contact +% information can be easily seen on the cover page. +% The default is journal. +% +% draft, draftcls, draftclsnofoot, final +% determines if paper is formatted as a widely spaced draft (for +% handwritten editor comments) or as a properly typeset final version. +% draftcls restricts draft mode to the class file while all other LaTeX +% packages (i.e., \usepackage{graphicx}) will behave as final - allows +% for a draft paper with visible figures, etc. draftclsnofoot is like +% draftcls, but does not display the date and the word "DRAFT" at the foot +% of the pages. If using one of the draft modes, the user will probably +% also want to select onecolumn. +% The default is final. +% +% letterpaper, a4paper +% determines paper size: 8.5in X 11in or 210mm X 297mm. CHANGING THE PAPER +% SIZE WILL NOT ALTER THE TYPESETTING OF THE DOCUMENT - ONLY THE MARGINS +% WILL BE AFFECTED. In particular, documents using the a4paper option will +% have reduced side margins (A4 is narrower than US letter) and a longer +% bottom margin (A4 is longer than US letter). For both cases, the top +% margins will be the same and the text will be horizontally centered. +% For final submission to IEEE, authors should use US letter (8.5 X 11in) +% paper. Note that authors should ensure that all post-processing +% (ps, pdf, etc.) uses the same paper specificiation as the .tex document. +% Problems here are by far the number one reason for incorrect margins. +% IEEEtran will automatically set the default paper size under pdflatex +% (without requiring a change to pdftex.cfg), so this issue is more +% important to dvips users. Fix config.ps, config.pdf, or ~/.dvipsrc for +% dvips, or use the dvips -t papersize option instead as needed. See the +% testflow documentation +% http://www.ctan.org/tex-archive/macros/latex/contrib/IEEEtran/testflow +% for more details on dvips paper size configuration. +% The default is letterpaper. +% +% oneside, twoside +% determines if layout follows single sided or two sided (duplex) +% printing. The only notable change is with the headings at the top of +% the pages. +% The default is oneside. +% +% onecolumn, twocolumn +% determines if text is organized into one or two columns per page. One +% column mode is usually used only with draft papers. +% The default is twocolumn. +% +% compsoc +% Use the format of the IEEE Computer Society. +% +% romanappendices +% Use the "Appendix I" convention when numbering appendices. IEEEtran.cls +% now defaults to Alpha "Appendix A" convention - the opposite of what +% v1.6b and earlier did. +% +% captionsoff +% disables the display of the figure/table captions. Some IEEE journals +% request that captions be removed and figures/tables be put on pages +% of their own at the end of an initial paper submission. The endfloat +% package can be used with this class option to achieve this format. +% +% nofonttune +% turns off tuning of the font interword spacing. Maybe useful to those +% not using the standard Times fonts or for those who have already "tuned" +% their fonts. +% The default is to enable IEEEtran to tune font parameters. +% +% +%---------- +% Available CLASSINPUTs provided (all are macros unless otherwise noted): +% \CLASSINPUTbaselinestretch +% \CLASSINPUTinnersidemargin +% \CLASSINPUToutersidemargin +% \CLASSINPUTtoptextmargin +% \CLASSINPUTbottomtextmargin +% +% Available CLASSINFOs provided: +% \ifCLASSINFOpdf (TeX if conditional) +% \CLASSINFOpaperwidth (macro) +% \CLASSINFOpaperheight (macro) +% \CLASSINFOnormalsizebaselineskip (length) +% \CLASSINFOnormalsizeunitybaselineskip (length) +% +% Available CLASSOPTIONs provided: +% all class option flags (TeX if conditionals) unless otherwise noted, +% e.g., \ifCLASSOPTIONcaptionsoff +% point size options provided as a single macro: +% \CLASSOPTIONpt +% which will be defined as 9, 10, 11, or 12 depending on the document's +% normalsize point size. +% also, class option peerreviewca implies the use of class option peerreview +% and classoption draft implies the use of class option draftcls + + + + + +\ProvidesClass{IEEEtran}[2012/11/21 V1.8c by Harald Hanche-Olsen and Anders Christensen] +\typeout{-- Based on V1.7a by Michael Shell} +\typeout{-- See the "IEEEtran_HOWTO" manual for usage information.} +\typeout{-- http://www.michaelshell.org/tex/ieeetran/} +\NeedsTeXFormat{LaTeX2e} + +% IEEEtran.cls version numbers, provided as of V1.3 +% These values serve as a way a .tex file can +% determine if the new features are provided. +% The version number of this IEEEtrans.cls can be obtained from +% these values. i.e., V1.4 +% KEEP THESE AS INTEGERS! i.e., NO {4a} or anything like that- +% (no need to enumerate "a" minor changes here) +\def\IEEEtransversionmajor{1} +\def\IEEEtransversionminor{7} + +% These do nothing, but provide them like in article.cls +\newif\if@restonecol +\newif\if@titlepage + + +% class option conditionals +\newif\ifCLASSOPTIONonecolumn \CLASSOPTIONonecolumnfalse +\newif\ifCLASSOPTIONtwocolumn \CLASSOPTIONtwocolumntrue + +\newif\ifCLASSOPTIONoneside \CLASSOPTIONonesidetrue +\newif\ifCLASSOPTIONtwoside \CLASSOPTIONtwosidefalse + +\newif\ifCLASSOPTIONfinal \CLASSOPTIONfinaltrue +\newif\ifCLASSOPTIONdraft \CLASSOPTIONdraftfalse +\newif\ifCLASSOPTIONdraftcls \CLASSOPTIONdraftclsfalse +\newif\ifCLASSOPTIONdraftclsnofoot \CLASSOPTIONdraftclsnofootfalse + +\newif\ifCLASSOPTIONpeerreview \CLASSOPTIONpeerreviewfalse +\newif\ifCLASSOPTIONpeerreviewca \CLASSOPTIONpeerreviewcafalse + +\newif\ifCLASSOPTIONjournal \CLASSOPTIONjournaltrue +\newif\ifCLASSOPTIONconference \CLASSOPTIONconferencefalse +\newif\ifCLASSOPTIONtechnote \CLASSOPTIONtechnotefalse + +\newif\ifCLASSOPTIONnofonttune \CLASSOPTIONnofonttunefalse + +\newif\ifCLASSOPTIONcaptionsoff \CLASSOPTIONcaptionsofffalse + +\newif\ifCLASSOPTIONcompsoc \CLASSOPTIONcompsocfalse + +\newif\ifCLASSOPTIONromanappendices \CLASSOPTIONromanappendicesfalse + + +% class info conditionals + +% indicates if pdf (via pdflatex) output +\newif\ifCLASSINFOpdf \CLASSINFOpdffalse + + +% V1.6b internal flag to show if using a4paper +\newif\if@IEEEusingAfourpaper \@IEEEusingAfourpaperfalse + + + +% IEEEtran class scratch pad registers +% dimen +\newdimen\@IEEEtrantmpdimenA +\newdimen\@IEEEtrantmpdimenB +% count +\newcount\@IEEEtrantmpcountA +\newcount\@IEEEtrantmpcountB +% token list +\newtoks\@IEEEtrantmptoksA + +% we use \CLASSOPTIONpt so that we can ID the point size (even for 9pt docs) +% as well as LaTeX's \@ptsize to retain some compatability with some +% external packages +\def\@ptsize{0} +% LaTeX does not support 9pt, so we set \@ptsize to 0 - same as that of 10pt +\DeclareOption{9pt}{\def\CLASSOPTIONpt{9}\def\@ptsize{0}} +\DeclareOption{10pt}{\def\CLASSOPTIONpt{10}\def\@ptsize{0}} +\DeclareOption{11pt}{\def\CLASSOPTIONpt{11}\def\@ptsize{1}} +\DeclareOption{12pt}{\def\CLASSOPTIONpt{12}\def\@ptsize{2}} + + + +\DeclareOption{letterpaper}{\setlength{\paperheight}{11in}% + \setlength{\paperwidth}{8.5in}% + \@IEEEusingAfourpaperfalse + \def\CLASSOPTIONpaper{letter}% + \def\CLASSINFOpaperwidth{8.5in}% + \def\CLASSINFOpaperheight{11in}} + + +\DeclareOption{a4paper}{\setlength{\paperheight}{297mm}% + \setlength{\paperwidth}{210mm}% + \@IEEEusingAfourpapertrue + \def\CLASSOPTIONpaper{a4}% + \def\CLASSINFOpaperwidth{210mm}% + \def\CLASSINFOpaperheight{297mm}} + +\DeclareOption{oneside}{\@twosidefalse\@mparswitchfalse + \CLASSOPTIONonesidetrue\CLASSOPTIONtwosidefalse} +\DeclareOption{twoside}{\@twosidetrue\@mparswitchtrue + \CLASSOPTIONtwosidetrue\CLASSOPTIONonesidefalse} + +\DeclareOption{onecolumn}{\CLASSOPTIONonecolumntrue\CLASSOPTIONtwocolumnfalse} +\DeclareOption{twocolumn}{\CLASSOPTIONtwocolumntrue\CLASSOPTIONonecolumnfalse} + +% If the user selects draft, then this class AND any packages +% will go into draft mode. +\DeclareOption{draft}{\CLASSOPTIONdrafttrue\CLASSOPTIONdraftclstrue + \CLASSOPTIONdraftclsnofootfalse} +% draftcls is for a draft mode which will not affect any packages +% used by the document. +\DeclareOption{draftcls}{\CLASSOPTIONdraftfalse\CLASSOPTIONdraftclstrue + \CLASSOPTIONdraftclsnofootfalse} +% draftclsnofoot is like draftcls, but without the footer. +\DeclareOption{draftclsnofoot}{\CLASSOPTIONdraftfalse\CLASSOPTIONdraftclstrue + \CLASSOPTIONdraftclsnofoottrue} +\DeclareOption{final}{\CLASSOPTIONdraftfalse\CLASSOPTIONdraftclsfalse + \CLASSOPTIONdraftclsnofootfalse} + +\DeclareOption{journal}{\CLASSOPTIONpeerreviewfalse\CLASSOPTIONpeerreviewcafalse + \CLASSOPTIONjournaltrue\CLASSOPTIONconferencefalse\CLASSOPTIONtechnotefalse} + +\DeclareOption{conference}{\CLASSOPTIONpeerreviewfalse\CLASSOPTIONpeerreviewcafalse + \CLASSOPTIONjournalfalse\CLASSOPTIONconferencetrue\CLASSOPTIONtechnotefalse} + +\DeclareOption{technote}{\CLASSOPTIONpeerreviewfalse\CLASSOPTIONpeerreviewcafalse + \CLASSOPTIONjournalfalse\CLASSOPTIONconferencefalse\CLASSOPTIONtechnotetrue} + +\DeclareOption{peerreview}{\CLASSOPTIONpeerreviewtrue\CLASSOPTIONpeerreviewcafalse + \CLASSOPTIONjournalfalse\CLASSOPTIONconferencefalse\CLASSOPTIONtechnotefalse} + +\DeclareOption{peerreviewca}{\CLASSOPTIONpeerreviewtrue\CLASSOPTIONpeerreviewcatrue + \CLASSOPTIONjournalfalse\CLASSOPTIONconferencefalse\CLASSOPTIONtechnotefalse} + +\DeclareOption{nofonttune}{\CLASSOPTIONnofonttunetrue} + +\DeclareOption{captionsoff}{\CLASSOPTIONcaptionsofftrue} + +\DeclareOption{compsoc}{\CLASSOPTIONcompsoctrue} + +\DeclareOption{romanappendices}{\CLASSOPTIONromanappendicestrue} + + +% default to US letter paper, 10pt, twocolumn, one sided, final, journal +\ExecuteOptions{letterpaper,10pt,twocolumn,oneside,final,journal} +% overrride these defaults per user requests +\ProcessOptions + + + +% Computer Society conditional execution command +\long\def\@IEEEcompsoconly#1{\relax\ifCLASSOPTIONcompsoc\relax#1\relax\fi\relax} +% inverse +\long\def\@IEEEnotcompsoconly#1{\relax\ifCLASSOPTIONcompsoc\else\relax#1\relax\fi\relax} +% compsoc conference +\long\def\@IEEEcompsocconfonly#1{\relax\ifCLASSOPTIONcompsoc\ifCLASSOPTIONconference\relax#1\relax\fi\fi\relax} +% compsoc not conference +\long\def\@IEEEcompsocnotconfonly#1{\relax\ifCLASSOPTIONcompsoc\ifCLASSOPTIONconference\else\relax#1\relax\fi\fi\relax} + + +% IEEE uses Times Roman font, so we'll default to Times. +% These three commands make up the entire times.sty package. +\renewcommand{\sfdefault}{phv} +\renewcommand{\rmdefault}{ptm} +\renewcommand{\ttdefault}{pcr} + +\@IEEEcompsoconly{\typeout{-- Using IEEE Computer Society mode.}} + +% V1.7 compsoc nonconference papers, use Palatino/Palladio as the main text font, +% not Times Roman. +\@IEEEcompsocnotconfonly{\renewcommand{\rmdefault}{ppl}} + +% enable Times/Palatino main text font +\normalfont\selectfont + + + + + +% V1.7 conference notice message hook +\def\@IEEEconsolenoticeconference{\typeout{}% +\typeout{** Conference Paper **}% +\typeout{Before submitting the final camera ready copy, remember to:}% +\typeout{}% +\typeout{ 1. Manually equalize the lengths of two columns on the last page}% +\typeout{ of your paper;}% +\typeout{}% +\typeout{ 2. Ensure that any PostScript and/or PDF output post-processing}% +\typeout{ uses only Type 1 fonts and that every step in the generation}% +\typeout{ process uses the appropriate paper size.}% +\typeout{}} + + +% we can send console reminder messages to the user here +\AtEndDocument{\ifCLASSOPTIONconference\@IEEEconsolenoticeconference\fi} + + +% warn about the use of single column other than for draft mode +\ifCLASSOPTIONtwocolumn\else% + \ifCLASSOPTIONdraftcls\else% + \typeout{** ATTENTION: Single column mode is not typically used with IEEE publications.}% + \fi% +\fi + + +% V1.7 improved paper size setting code. +% Set pdfpage and dvips paper sizes. Conditional tests are similar to that +% of ifpdf.sty. Retain within {} to ensure tested macros are never altered, +% even if only effect is to set them to \relax. +% if \pdfoutput is undefined or equal to relax, output a dvips special +{\@ifundefined{pdfoutput}{\AtBeginDvi{\special{papersize=\CLASSINFOpaperwidth,\CLASSINFOpaperheight}}}{% +% pdfoutput is defined and not equal to \relax +% check for pdfpageheight existence just in case someone sets pdfoutput +% under non-pdflatex. If exists, set them regardless of value of \pdfoutput. +\@ifundefined{pdfpageheight}{\relax}{\global\pdfpagewidth\paperwidth +\global\pdfpageheight\paperheight}% +% if using \pdfoutput=0 under pdflatex, send dvips papersize special +\ifcase\pdfoutput +\AtBeginDvi{\special{papersize=\CLASSINFOpaperwidth,\CLASSINFOpaperheight}}% +\else +% we are using pdf output, set CLASSINFOpdf flag +\global\CLASSINFOpdftrue +\fi}} + +% let the user know the selected papersize +\typeout{-- Using \CLASSINFOpaperwidth\space x \CLASSINFOpaperheight\space +(\CLASSOPTIONpaper)\space paper.} + +\ifCLASSINFOpdf +\typeout{-- Using PDF output.} +\else +\typeout{-- Using DVI output.} +\fi + + +% The idea hinted here is for LaTeX to generate markleft{} and markright{} +% automatically for you after you enter \author{}, \journal{}, +% \journaldate{}, journalvol{}, \journalnum{}, etc. +% However, there may be some backward compatibility issues here as +% well as some special applications for IEEEtran.cls and special issues +% that may require the flexible \markleft{}, \markright{} and/or \markboth{}. +% We'll leave this as an open future suggestion. +%\newcommand{\journal}[1]{\def\@journal{#1}} +%\def\@journal{} + + + +% pointsize values +% used with ifx to determine the document's normal size +\def\@IEEEptsizenine{9} +\def\@IEEEptsizeten{10} +\def\@IEEEptsizeeleven{11} +\def\@IEEEptsizetwelve{12} + + + +% FONT DEFINITIONS (No sizexx.clo file needed) +% V1.6 revised font sizes, displayskip values and +% revised normalsize baselineskip to reduce underfull vbox problems +% on the 58pc = 696pt = 9.5in text height we want +% normalsize #lines/column baselineskip (aka leading) +% 9pt 63 11.0476pt (truncated down) +% 10pt 58 12pt (exact) +% 11pt 52 13.3846pt (truncated down) +% 12pt 50 13.92pt (exact) +% + +% we need to store the nominal baselineskip for the given font size +% in case baselinestretch ever changes. +% this is a dimen, so it will not hold stretch or shrink +\newdimen\@IEEEnormalsizeunitybaselineskip +\@IEEEnormalsizeunitybaselineskip\baselineskip + +\ifx\CLASSOPTIONpt\@IEEEptsizenine +\typeout{-- This is a 9 point document.} +\def\normalsize{\@setfontsize{\normalsize}{9}{11.0476pt}}% +\setlength{\@IEEEnormalsizeunitybaselineskip}{11.0476pt}% +\normalsize +\abovedisplayskip 1.5ex plus3pt minus1pt% +\belowdisplayskip \abovedisplayskip% +\abovedisplayshortskip 0pt plus3pt% +\belowdisplayshortskip 1.5ex plus3pt minus1pt +\def\small{\@setfontsize{\small}{8.5}{10pt}} +\def\footnotesize{\@setfontsize{\footnotesize}{8}{9pt}} +\def\scriptsize{\@setfontsize{\scriptsize}{7}{8pt}} +\def\tiny{\@setfontsize{\tiny}{5}{6pt}} +% sublargesize is the same as large - 10pt +\def\sublargesize{\@setfontsize{\sublargesize}{10}{12pt}} +\def\large{\@setfontsize{\large}{10}{12pt}} +\def\Large{\@setfontsize{\Large}{12}{14pt}} +\def\LARGE{\@setfontsize{\LARGE}{14}{17pt}} +\def\huge{\@setfontsize{\huge}{17}{20pt}} +\def\Huge{\@setfontsize{\Huge}{20}{24pt}} +\fi + + +% Check if we have selected 10 points +\ifx\CLASSOPTIONpt\@IEEEptsizeten +\typeout{-- This is a 10 point document.} +\def\normalsize{\@setfontsize{\normalsize}{10}{11}}% +\setlength{\@IEEEnormalsizeunitybaselineskip}{11pt}% +\normalsize +\abovedisplayskip 1.5ex plus4pt minus2pt% +\belowdisplayskip \abovedisplayskip% +\abovedisplayshortskip 0pt plus4pt% +\belowdisplayshortskip 1.5ex plus4pt minus2pt +\def\small{\@setfontsize{\small}{9}{10pt}} +\def\footnotesize{\@setfontsize{\footnotesize}{8}{9pt}} +\def\scriptsize{\@setfontsize{\scriptsize}{7}{8pt}} +\def\tiny{\@setfontsize{\tiny}{5}{6pt}} +% sublargesize is a tad smaller than large - 11pt +\def\sublargesize{\@setfontsize{\sublargesize}{11}{13.4pt}} +\def\large{\@setfontsize{\large}{12}{14pt}} +\def\Large{\@setfontsize{\Large}{14}{17pt}} +\def\LARGE{\@setfontsize{\LARGE}{17}{20pt}} +\def\huge{\@setfontsize{\huge}{20}{24pt}} +\def\Huge{\@setfontsize{\Huge}{24}{28pt}} +\fi + + +% Check if we have selected 11 points +\ifx\CLASSOPTIONpt\@IEEEptsizeeleven +\typeout{-- This is an 11 point document.} +\def\normalsize{\@setfontsize{\normalsize}{11}{13.3846pt}}% +\setlength{\@IEEEnormalsizeunitybaselineskip}{13.3846pt}% +\normalsize +\abovedisplayskip 1.5ex plus5pt minus3pt% +\belowdisplayskip \abovedisplayskip% +\abovedisplayshortskip 0pt plus5pt% +\belowdisplayshortskip 1.5ex plus5pt minus3pt +\def\small{\@setfontsize{\small}{10}{12pt}} +\def\footnotesize{\@setfontsize{\footnotesize}{9}{10.5pt}} +\def\scriptsize{\@setfontsize{\scriptsize}{8}{9pt}} +\def\tiny{\@setfontsize{\tiny}{6}{7pt}} +% sublargesize is the same as large - 12pt +\def\sublargesize{\@setfontsize{\sublargesize}{12}{14pt}} +\def\large{\@setfontsize{\large}{12}{14pt}} +\def\Large{\@setfontsize{\Large}{14}{17pt}} +\def\LARGE{\@setfontsize{\LARGE}{17}{20pt}} +\def\huge{\@setfontsize{\huge}{20}{24pt}} +\def\Huge{\@setfontsize{\Huge}{24}{28pt}} +\fi + + +% Check if we have selected 12 points +\ifx\CLASSOPTIONpt\@IEEEptsizetwelve +\typeout{-- This is a 12 point document.} +\def\normalsize{\@setfontsize{\normalsize}{12}{13.92pt}}% +\setlength{\@IEEEnormalsizeunitybaselineskip}{13.92pt}% +\normalsize +\abovedisplayskip 1.5ex plus6pt minus4pt% +\belowdisplayskip \abovedisplayskip% +\abovedisplayshortskip 0pt plus6pt% +\belowdisplayshortskip 1.5ex plus6pt minus4pt +\def\small{\@setfontsize{\small}{10}{12pt}} +\def\footnotesize{\@setfontsize{\footnotesize}{9}{10.5pt}} +\def\scriptsize{\@setfontsize{\scriptsize}{8}{9pt}} +\def\tiny{\@setfontsize{\tiny}{6}{7pt}} +% sublargesize is the same as large - 14pt +\def\sublargesize{\@setfontsize{\sublargesize}{14}{17pt}} +\def\large{\@setfontsize{\large}{14}{17pt}} +\def\Large{\@setfontsize{\Large}{17}{20pt}} +\def\LARGE{\@setfontsize{\LARGE}{20}{24pt}} +\def\huge{\@setfontsize{\huge}{22}{26pt}} +\def\Huge{\@setfontsize{\Huge}{24}{28pt}} +\fi + + +% V1.6 The Computer Modern Fonts will issue a substitution warning for +% 24pt titles (24.88pt is used instead) increase the substitution +% tolerance to turn off this warning +\def\fontsubfuzz{.9pt} +% However, the default (and correct) Times font will scale exactly as needed. + + +% warn the user in case they forget to use the 9pt option with +% technote +\ifCLASSOPTIONtechnote% + \ifx\CLASSOPTIONpt\@IEEEptsizenine\else% + \typeout{** ATTENTION: Technotes are normally 9pt documents.}% + \fi% +\fi + + +% V1.7 +% Improved \textunderscore to provide a much better fake _ when used with +% OT1 encoding. Under OT1, detect use of pcr or cmtt \ttfamily and use +% available true _ glyph for those two typewriter fonts. +\def\@IEEEstringptm{ptm} % Times Roman family +\def\@IEEEstringppl{ppl} % Palatino Roman family +\def\@IEEEstringphv{phv} % Helvetica Sans Serif family +\def\@IEEEstringpcr{pcr} % Courier typewriter family +\def\@IEEEstringcmtt{cmtt} % Computer Modern typewriter family +\DeclareTextCommandDefault{\textunderscore}{\leavevmode +\ifx\f@family\@IEEEstringpcr\string_\else +\ifx\f@family\@IEEEstringcmtt\string_\else +\ifx\f@family\@IEEEstringptm\kern 0em\vbox{\hrule\@width 0.5em\@height 0.5pt\kern -0.3ex}\else +\ifx\f@family\@IEEEstringppl\kern 0em\vbox{\hrule\@width 0.5em\@height 0.5pt\kern -0.3ex}\else +\ifx\f@family\@IEEEstringphv\kern -0.03em\vbox{\hrule\@width 0.62em\@height 0.52pt\kern -0.33ex}\kern -0.03em\else +\kern 0.09em\vbox{\hrule\@width 0.6em\@height 0.44pt\kern -0.63pt\kern -0.42ex}\kern 0.09em\fi\fi\fi\fi\fi\relax} + + + + +% set the default \baselinestretch +\def\baselinestretch{1} +\ifCLASSOPTIONdraftcls + \def\baselinestretch{1.5}% default baselinestretch for draft modes +\fi + + +% process CLASSINPUT baselinestretch +\ifx\CLASSINPUTbaselinestretch\@IEEEundefined +\else + \edef\baselinestretch{\CLASSINPUTbaselinestretch} % user CLASSINPUT override + \typeout{** ATTENTION: Overriding \string\baselinestretch\space to + \baselinestretch\space via \string\CLASSINPUT.} +\fi + +\normalsize % make \baselinestretch take affect + + + + +% store the normalsize baselineskip +\newdimen\CLASSINFOnormalsizebaselineskip +\CLASSINFOnormalsizebaselineskip=\baselineskip\relax +% and the normalsize unity (baselinestretch=1) baselineskip +% we could save a register by giving the user access to +% \@IEEEnormalsizeunitybaselineskip. However, let's protect +% its read only internal status +\newdimen\CLASSINFOnormalsizeunitybaselineskip +\CLASSINFOnormalsizeunitybaselineskip=\@IEEEnormalsizeunitybaselineskip\relax +% store the nominal value of jot +\newdimen\IEEEnormaljot +\IEEEnormaljot=0.25\baselineskip\relax + +% set \jot +\jot=\IEEEnormaljot\relax + + + + +% V1.6, we are now going to fine tune the interword spacing +% The default interword glue for Times under TeX appears to use a +% nominal interword spacing of 25% (relative to the font size, i.e., 1em) +% a maximum of 40% and a minimum of 19%. +% For example, 10pt text uses an interword glue of: +% +% 2.5pt plus 1.49998pt minus 0.59998pt +% +% However, IEEE allows for a more generous range which reduces the need +% for hyphenation, especially for two column text. Furthermore, IEEE +% tends to use a little bit more nominal space between the words. +% IEEE's interword spacing percentages appear to be: +% 35% nominal +% 23% minimum +% 50% maximum +% (They may even be using a tad more for the largest fonts such as 24pt.) +% +% for bold text, IEEE increases the spacing a little more: +% 37.5% nominal +% 23% minimum +% 55% maximum + +% here are the interword spacing ratios we'll use +% for medium (normal weight) +\def\@IEEEinterspaceratioM{0.35} +\def\@IEEEinterspaceMINratioM{0.23} +\def\@IEEEinterspaceMAXratioM{0.50} + +% for bold +\def\@IEEEinterspaceratioB{0.375} +\def\@IEEEinterspaceMINratioB{0.23} +\def\@IEEEinterspaceMAXratioB{0.55} + + +% command to revise the interword spacing for the current font under TeX: +% \fontdimen2 = nominal interword space +% \fontdimen3 = interword stretch +% \fontdimen4 = interword shrink +% since all changes to the \fontdimen are global, we can enclose these commands +% in braces to confine any font attribute or length changes +\def\@@@IEEEsetfontdimens#1#2#3{{% +\setlength{\@IEEEtrantmpdimenB}{\f@size pt}% grab the font size in pt, could use 1em instead. +\setlength{\@IEEEtrantmpdimenA}{#1\@IEEEtrantmpdimenB}% +\fontdimen2\font=\@IEEEtrantmpdimenA\relax +\addtolength{\@IEEEtrantmpdimenA}{-#2\@IEEEtrantmpdimenB}% +\fontdimen3\font=-\@IEEEtrantmpdimenA\relax +\setlength{\@IEEEtrantmpdimenA}{#1\@IEEEtrantmpdimenB}% +\addtolength{\@IEEEtrantmpdimenA}{-#3\@IEEEtrantmpdimenB}% +\fontdimen4\font=\@IEEEtrantmpdimenA\relax}} + +% revise the interword spacing for each font weight +\def\@@IEEEsetfontdimens{{% +\mdseries +\@@@IEEEsetfontdimens{\@IEEEinterspaceratioM}{\@IEEEinterspaceMAXratioM}{\@IEEEinterspaceMINratioM}% +\bfseries +\@@@IEEEsetfontdimens{\@IEEEinterspaceratioB}{\@IEEEinterspaceMAXratioB}{\@IEEEinterspaceMINratioB}% +}} + +% revise the interword spacing for each font shape +% \slshape is not often used for IEEE work and is not altered here. The \scshape caps are +% already a tad too large in the free LaTeX fonts (as compared to what IEEE uses) so we +% won't alter these either. +\def\@IEEEsetfontdimens{{% +\normalfont +\@@IEEEsetfontdimens +\normalfont\itshape +\@@IEEEsetfontdimens +}} + +% command to revise the interword spacing for each font size (and shape +% and weight). Only the \rmfamily is done here as \ttfamily uses a +% fixed spacing and \sffamily is not used as the main text of IEEE papers. +\def\@IEEEtunefonts{{\selectfont\rmfamily +\tiny\@IEEEsetfontdimens +\scriptsize\@IEEEsetfontdimens +\footnotesize\@IEEEsetfontdimens +\small\@IEEEsetfontdimens +\normalsize\@IEEEsetfontdimens +\sublargesize\@IEEEsetfontdimens +\large\@IEEEsetfontdimens +\LARGE\@IEEEsetfontdimens +\huge\@IEEEsetfontdimens +\Huge\@IEEEsetfontdimens}} + +% if the nofonttune class option is not given, revise the interword spacing +% now - in case IEEEtran makes any default length measurements, and make +% sure all the default fonts are loaded +\ifCLASSOPTIONnofonttune\else +\@IEEEtunefonts +\fi + +% and again at the start of the document in case the user loaded different fonts +\AtBeginDocument{\ifCLASSOPTIONnofonttune\else\@IEEEtunefonts\fi} + + + +% V1.6 +% LaTeX is a little to quick to use hyphenations +% So, we increase the penalty for their use and raise +% the badness level that triggers an underfull hbox +% warning. The author may still have to tweak things, +% but the appearance will be much better "right out +% of the box" than that under V1.5 and prior. +% TeX default is 50 +\hyphenpenalty=750 +% If we didn't adjust the interword spacing, 2200 might be better. +% The TeX default is 1000 +\hbadness=1350 +% IEEE does not use extra spacing after punctuation +\frenchspacing + +% V1.7 increase this a tad to discourage equation breaks +\binoppenalty=1000 % default 700 +\relpenalty=800 % default 500 + + +% margin note stuff +\marginparsep 10pt +\marginparwidth 20pt +\marginparpush 25pt + + +% if things get too close, go ahead and let them touch +\lineskip 0pt +\normallineskip 0pt +\lineskiplimit 0pt +\normallineskiplimit 0pt + +% The distance from the lower edge of the text body to the +% footline +\footskip 0.4in + +% normally zero, should be relative to font height. +% put in a little rubber to help stop some bad breaks (underfull vboxes) +\parskip 0ex plus 0.2ex minus 0.1ex +\ifCLASSOPTIONconference +\parskip 6pt plus 2pt minus 1pt +\fi + +\parindent 1.0em +\ifCLASSOPTIONconference +\parindent 14.45pt +\fi + +\topmargin -49.0pt +\headheight 12pt +\headsep 0.25in + +% use the normal font baselineskip +% so that \topskip is unaffected by changes in \baselinestretch +\topskip=\@IEEEnormalsizeunitybaselineskip +\textheight 58pc % 9.63in, 696pt +% Tweak textheight to a perfect integer number of lines/page. +% The normal baselineskip for each document point size is used +% to determine these values. +\ifx\CLASSOPTIONpt\@IEEEptsizenine\textheight=63\@IEEEnormalsizeunitybaselineskip\fi % 63 lines/page +\ifx\CLASSOPTIONpt\@IEEEptsizeten\textheight=58\@IEEEnormalsizeunitybaselineskip\fi % 58 lines/page +\ifx\CLASSOPTIONpt\@IEEEptsizeeleven\textheight=52\@IEEEnormalsizeunitybaselineskip\fi % 52 lines/page +\ifx\CLASSOPTIONpt\@IEEEptsizetwelve\textheight=50\@IEEEnormalsizeunitybaselineskip\fi % 50 lines/page + + +\columnsep 1.5pc +\textwidth 184.2mm + + +% the default side margins are equal +\if@IEEEusingAfourpaper +\oddsidemargin 14.32mm +\evensidemargin 14.32mm +\else +\oddsidemargin 0.680in +\evensidemargin 0.680in +\fi +% compensate for LaTeX's 1in offset +\addtolength{\oddsidemargin}{-1in} +\addtolength{\evensidemargin}{-1in} + + + +% adjust margins for conference mode +\ifCLASSOPTIONconference + \topmargin -0.25in + % we retain the reserved, but unused space for headers + \addtolength{\topmargin}{-\headheight} + \addtolength{\topmargin}{-\headsep} + \textheight 9.25in % The standard for conferences (668.4975pt) + % Tweak textheight to a perfect integer number of lines/page. + \ifx\CLASSOPTIONpt\@IEEEptsizenine\textheight=61\@IEEEnormalsizeunitybaselineskip\fi % 61 lines/page + \ifx\CLASSOPTIONpt\@IEEEptsizeten\textheight=62\@IEEEnormalsizeunitybaselineskip\fi % 62 lines/page + \ifx\CLASSOPTIONpt\@IEEEptsizeeleven\textheight=50\@IEEEnormalsizeunitybaselineskip\fi % 50 lines/page + \ifx\CLASSOPTIONpt\@IEEEptsizetwelve\textheight=48\@IEEEnormalsizeunitybaselineskip\fi % 48 lines/page +\fi + + +% compsoc conference +\ifCLASSOPTIONcompsoc +\ifCLASSOPTIONconference + % compsoc conference use a larger value for columnsep + \columnsep 0.375in + % compsoc conferences want 1in top margin, 1.125in bottom margin + \topmargin 0in + \addtolength{\topmargin}{-6pt}% we tweak this a tad to better comply with top of line stuff + % we retain the reserved, but unused space for headers + \addtolength{\topmargin}{-\headheight} + \addtolength{\topmargin}{-\headsep} + \textheight 8.875in % (641.39625pt) + % Tweak textheight to a perfect integer number of lines/page. + \ifx\CLASSOPTIONpt\@IEEEptsizenine\textheight=58\@IEEEnormalsizeunitybaselineskip\fi % 58 lines/page + \ifx\CLASSOPTIONpt\@IEEEptsizeten\textheight=53\@IEEEnormalsizeunitybaselineskip\fi % 53 lines/page + \ifx\CLASSOPTIONpt\@IEEEptsizeeleven\textheight=48\@IEEEnormalsizeunitybaselineskip\fi % 48 lines/page + \ifx\CLASSOPTIONpt\@IEEEptsizetwelve\textheight=46\@IEEEnormalsizeunitybaselineskip\fi % 46 lines/page + \textwidth 6.5in + % the default side margins are equal + \if@IEEEusingAfourpaper + \oddsidemargin 22.45mm + \evensidemargin 22.45mm + \else + \oddsidemargin 1in + \evensidemargin 1in + \fi + % compensate for LaTeX's 1in offset + \addtolength{\oddsidemargin}{-1in} + \addtolength{\evensidemargin}{-1in} +\fi\fi + + + +% draft mode settings override that of all other modes +% provides a nice 1in margin all around the paper and extra +% space between the lines for editor's comments +\ifCLASSOPTIONdraftcls + % want 1in from top of paper to text + \setlength{\topmargin}{-\headsep}% + \addtolength{\topmargin}{-\headheight}% + % we want 1in side margins regardless of paper type + \oddsidemargin 0in + \evensidemargin 0in + % set the text width + \setlength{\textwidth}{\paperwidth}% + \addtolength{\textwidth}{-2.0in}% + \setlength{\textheight}{\paperheight}% + \addtolength{\textheight}{-2.0in}% + % digitize textheight to be an integer number of lines. + % this may cause the bottom margin to be off a tad + \addtolength{\textheight}{-1\topskip}% + \divide\textheight by \baselineskip% + \multiply\textheight by \baselineskip% + \addtolength{\textheight}{\topskip}% +\fi + + + +% process CLASSINPUT inner/outer margin +% if inner margin defined, but outer margin not, set outer to inner. +\ifx\CLASSINPUTinnersidemargin\@IEEEundefined +\else + \ifx\CLASSINPUToutersidemargin\@IEEEundefined + \edef\CLASSINPUToutersidemargin{\CLASSINPUTinnersidemargin} + \fi +\fi + +\ifx\CLASSINPUToutersidemargin\@IEEEundefined +\else + % if outer margin defined, but inner margin not, set inner to outer. + \ifx\CLASSINPUTinnersidemargin\@IEEEundefined + \edef\CLASSINPUTinnersidemargin{\CLASSINPUToutersidemargin} + \fi + \setlength{\oddsidemargin}{\CLASSINPUTinnersidemargin} + \ifCLASSOPTIONtwoside + \setlength{\evensidemargin}{\CLASSINPUToutersidemargin} + \else + \setlength{\evensidemargin}{\CLASSINPUTinnersidemargin} + \fi + \addtolength{\oddsidemargin}{-1in} + \addtolength{\evensidemargin}{-1in} + \setlength{\textwidth}{\paperwidth} + \addtolength{\textwidth}{-\CLASSINPUTinnersidemargin} + \addtolength{\textwidth}{-\CLASSINPUToutersidemargin} + \typeout{** ATTENTION: Overriding inner side margin to \CLASSINPUTinnersidemargin\space and + outer side margin to \CLASSINPUToutersidemargin\space via \string\CLASSINPUT.} +\fi + + + +% process CLASSINPUT top/bottom text margin +% if toptext margin defined, but bottomtext margin not, set bottomtext to toptext margin +\ifx\CLASSINPUTtoptextmargin\@IEEEundefined +\else + \ifx\CLASSINPUTbottomtextmargin\@IEEEundefined + \edef\CLASSINPUTbottomtextmargin{\CLASSINPUTtoptextmargin} + \fi +\fi + +\ifx\CLASSINPUTbottomtextmargin\@IEEEundefined +\else + % if bottomtext margin defined, but toptext margin not, set toptext to bottomtext margin + \ifx\CLASSINPUTtoptextmargin\@IEEEundefined + \edef\CLASSINPUTtoptextmargin{\CLASSINPUTbottomtextmargin} + \fi + \setlength{\topmargin}{\CLASSINPUTtoptextmargin} + \addtolength{\topmargin}{-1in} + \addtolength{\topmargin}{-\headheight} + \addtolength{\topmargin}{-\headsep} + \setlength{\textheight}{\paperheight} + \addtolength{\textheight}{-\CLASSINPUTtoptextmargin} + \addtolength{\textheight}{-\CLASSINPUTbottomtextmargin} + % in the default format we use the normal baselineskip as topskip + % we only need 0.7 of this to clear typical top text and we need + % an extra 0.3 spacing at the bottom for descenders. This will + % correct for both. + \addtolength{\topmargin}{-0.3\@IEEEnormalsizeunitybaselineskip} + \typeout{** ATTENTION: Overriding top text margin to \CLASSINPUTtoptextmargin\space and + bottom text margin to \CLASSINPUTbottomtextmargin\space via \string\CLASSINPUT.} +\fi + + + + + + + +% LIST SPACING CONTROLS + +% Controls the amount of EXTRA spacing +% above and below \trivlist +% Both \list and IED lists override this. +% However, \trivlist will use this as will most +% things built from \trivlist like the \center +% environment. +\topsep 0.5\baselineskip + +% Controls the additional spacing around lists preceded +% or followed by blank lines. IEEE does not increase +% spacing before or after paragraphs so it is set to zero. +% \z@ is the same as zero, but faster. +\partopsep \z@ + +% Controls the spacing between paragraphs in lists. +% IEEE does not increase spacing before or after paragraphs +% so this is also zero. +% With IEEEtran.cls, global changes to +% this value DO affect lists (but not IED lists). +\parsep \z@ + +% Controls the extra spacing between list items. +% IEEE does not put extra spacing between items. +% With IEEEtran.cls, global changes to this value DO affect +% lists (but not IED lists). +\itemsep \z@ + +% \itemindent is the amount to indent the FIRST line of a list +% item. It is auto set to zero within the \list environment. To alter +% it, you have to do so when you call the \list. +% However, IEEE uses this for the theorem environment +% There is an alternative value for this near \leftmargini below +\itemindent -1em + +% \leftmargin, the spacing from the left margin of the main text to +% the left of the main body of a list item is set by \list. +% Hence this statement does nothing for lists. +% But, quote and verse do use it for indention. +\leftmargin 2em + +% we retain this stuff from the older IEEEtran.cls so that \list +% will work the same way as before. However, itemize, enumerate and +% description (IED) could care less about what these are as they +% all are overridden. +\leftmargini 2em +%\itemindent 2em % Alternative values: sometimes used. +%\leftmargini 0em +\leftmarginii 1em +\leftmarginiii 1.5em +\leftmarginiv 1.5em +\leftmarginv 1.0em +\leftmarginvi 1.0em +\labelsep 0.5em +\labelwidth \z@ + + +% The old IEEEtran.cls behavior of \list is retained. +% However, the new V1.3 IED list environments override all the +% @list stuff (\@listX is called within \list for the +% appropriate level just before the user's list_decl is called). +% \topsep is now 2pt as IEEE puts a little extra space around +% lists - used by those non-IED macros that depend on \list. +% Note that \parsep and \itemsep are not redefined as in +% the sizexx.clo \@listX (which article.cls uses) so global changes +% of these values DO affect \list +% +\def\@listi{\leftmargin\leftmargini \topsep 2pt plus 1pt minus 1pt} +\let\@listI\@listi +\def\@listii{\leftmargin\leftmarginii\labelwidth\leftmarginii% + \advance\labelwidth-\labelsep \topsep 2pt} +\def\@listiii{\leftmargin\leftmarginiii\labelwidth\leftmarginiii% + \advance\labelwidth-\labelsep \topsep 2pt} +\def\@listiv{\leftmargin\leftmarginiv\labelwidth\leftmarginiv% + \advance\labelwidth-\labelsep \topsep 2pt} +\def\@listv{\leftmargin\leftmarginv\labelwidth\leftmarginv% + \advance\labelwidth-\labelsep \topsep 2pt} +\def\@listvi{\leftmargin\leftmarginvi\labelwidth\leftmarginvi% + \advance\labelwidth-\labelsep \topsep 2pt} + + +% IEEE uses 5) not 5. +\def\labelenumi{\theenumi)} \def\theenumi{\arabic{enumi}} + +% IEEE uses a) not (a) +\def\labelenumii{\theenumii)} \def\theenumii{\alph{enumii}} + +% IEEE uses iii) not iii. +\def\labelenumiii{\theenumiii)} \def\theenumiii{\roman{enumiii}} + +% IEEE uses A) not A. +\def\labelenumiv{\theenumiv)} \def\theenumiv{\Alph{enumiv}} + +% exactly the same as in article.cls +\def\p@enumii{\theenumi} +\def\p@enumiii{\theenumi(\theenumii)} +\def\p@enumiv{\p@enumiii\theenumiii} + +% itemized list label styles +\def\labelitemi{$\bullet$} +\def\labelitemii{$\circ$} +\def\labelitemiii{\vrule height 0.8ex depth -0.2ex width 0.6ex} +\def\labelitemiv{$\ast$} + + + +% **** V1.3 ENHANCEMENTS **** +% Itemize, Enumerate and Description (IED) List Controls +% *************************** +% +% +% IEEE seems to use at least two different values by +% which ITEMIZED list labels are indented to the right +% For The Journal of Lightwave Technology (JLT) and The Journal +% on Selected Areas in Communications (JSAC), they tend to use +% an indention equal to \parindent. For Transactions on Communications +% they tend to indent ITEMIZED lists a little more--- 1.3\parindent. +% We'll provide both values here for you so that you can choose +% which one you like in your document using a command such as: +% setlength{\IEEEilabelindent}{\IEEEilabelindentB} +\newdimen\IEEEilabelindentA +\IEEEilabelindentA \parindent + +\newdimen\IEEEilabelindentB +\IEEEilabelindentB 1.3\parindent +% However, we'll default to using \parindent +% which makes more sense to me +\newdimen\IEEEilabelindent +\IEEEilabelindent \IEEEilabelindentA + + +% This controls the default amount the enumerated list labels +% are indented to the right. +% Normally, this is the same as the paragraph indention +\newdimen\IEEEelabelindent +\IEEEelabelindent \parindent + +% This controls the default amount the description list labels +% are indented to the right. +% Normally, this is the same as the paragraph indention +\newdimen\IEEEdlabelindent +\IEEEdlabelindent \parindent + +% This is the value actually used within the IED lists. +% The IED environments automatically set its value to +% one of the three values above, so global changes do +% not have any effect +\newdimen\IEEElabelindent +\IEEElabelindent \parindent + +% The actual amount labels will be indented is +% \IEEElabelindent multiplied by the factor below +% corresponding to the level of nesting depth +% This provides a means by which the user can +% alter the effective \IEEElabelindent for deeper +% levels +% There may not be such a thing as correct "standard IEEE" +% values. What IEEE actually does may depend on the specific +% circumstances. +% The first list level almost always has full indention. +% The second levels I've seen have only 75% of the normal indentation +% Three level or greater nestings are very rare. I am guessing +% that they don't use any indentation. +\def\IEEElabelindentfactori{1.0} % almost always one +\def\IEEElabelindentfactorii{0.75} % 0.0 or 1.0 may be used in some cases +\def\IEEElabelindentfactoriii{0.0} % 0.75? 0.5? 0.0? +\def\IEEElabelindentfactoriv{0.0} +\def\IEEElabelindentfactorv{0.0} +\def\IEEElabelindentfactorvi{0.0} + +% value actually used within IED lists, it is auto +% set to one of the 6 values above +% global changes here have no effect +\def\IEEElabelindentfactor{1.0} + +% This controls the default spacing between the end of the IED +% list labels and the list text, when normal text is used for +% the labels. +\newdimen\IEEEiednormlabelsep +\IEEEiednormlabelsep \parindent + +% This controls the default spacing between the end of the IED +% list labels and the list text, when math symbols are used for +% the labels (nomenclature lists). IEEE usually increases the +% spacing in these cases +\newdimen\IEEEiedmathlabelsep +\IEEEiedmathlabelsep 1.2em + +% This controls the extra vertical separation put above and +% below each IED list. IEEE usually puts a little extra spacing +% around each list. However, this spacing is barely noticeable. +\newskip\IEEEiedtopsep +\IEEEiedtopsep 2pt plus 1pt minus 1pt + + +% This command is executed within each IED list environment +% at the beginning of the list. You can use this to set the +% parameters for some/all your IED list(s) without disturbing +% global parameters that affect things other than lists. +% i.e., renewcommand{\IEEEiedlistdecl}{\setlength{\labelsep}{5em}} +% will alter the \labelsep for the next list(s) until +% \IEEEiedlistdecl is redefined. +\def\IEEEiedlistdecl{\relax} + +% This command provides an easy way to set \leftmargin based +% on the \labelwidth, \labelsep and the argument \IEEElabelindent +% Usage: \IEEEcalcleftmargin{width-to-indent-the-label} +% output is in the \leftmargin variable, i.e., effectively: +% \leftmargin = argument + \labelwidth + \labelsep +% Note controlled spacing here, shield end of lines with % +\def\IEEEcalcleftmargin#1{\setlength{\leftmargin}{#1}% +\addtolength{\leftmargin}{\labelwidth}% +\addtolength{\leftmargin}{\labelsep}} + +% This command provides an easy way to set \labelwidth to the +% width of the given text. It is the same as +% \settowidth{\labelwidth}{label-text} +% and useful as a shorter alternative. +% Typically used to set \labelwidth to be the width +% of the longest label in the list +\def\IEEEsetlabelwidth#1{\settowidth{\labelwidth}{#1}} + +% When this command is executed, IED lists will use the +% IEEEiedmathlabelsep label separation rather than the normal +% spacing. To have an effect, this command must be executed via +% the \IEEEiedlistdecl or within the option of the IED list +% environments. +\def\IEEEusemathlabelsep{\setlength{\labelsep}{\IEEEiedmathlabelsep}} + +% A flag which controls whether the IED lists automatically +% calculate \leftmargin from \IEEElabelindent, \labelwidth and \labelsep +% Useful if you want to specify your own \leftmargin +% This flag must be set (\IEEEnocalcleftmargintrue or \IEEEnocalcleftmarginfalse) +% via the \IEEEiedlistdecl or within the option of the IED list +% environments to have an effect. +\newif\ifIEEEnocalcleftmargin +\IEEEnocalcleftmarginfalse + +% A flag which controls whether \IEEElabelindent is multiplied by +% the \IEEElabelindentfactor for each list level. +% This flag must be set via the \IEEEiedlistdecl or within the option +% of the IED list environments to have an effect. +\newif\ifIEEEnolabelindentfactor +\IEEEnolabelindentfactorfalse + + +% internal variable to indicate type of IED label +% justification +% 0 - left; 1 - center; 2 - right +\def\@IEEEiedjustify{0} + + +% commands to allow the user to control IED +% label justifications. Use these commands within +% the IED environment option or in the \IEEEiedlistdecl +% Note that changing the normal list justifications +% is nonstandard and IEEE may not like it if you do so! +% I include these commands as they may be helpful to +% those who are using these enhanced list controls for +% other non-IEEE related LaTeX work. +% itemize and enumerate automatically default to right +% justification, description defaults to left. +\def\IEEEiedlabeljustifyl{\def\@IEEEiedjustify{0}}%left +\def\IEEEiedlabeljustifyc{\def\@IEEEiedjustify{1}}%center +\def\IEEEiedlabeljustifyr{\def\@IEEEiedjustify{2}}%right + + + + +% commands to save to and restore from the list parameter copies +% this allows us to set all the list parameters within +% the list_decl and prevent \list (and its \@list) +% from overriding any of our parameters +% V1.6 use \edefs instead of dimen's to conserve dimen registers +% Note controlled spacing here, shield end of lines with % +\def\@IEEEsavelistparams{\edef\@IEEEiedtopsep{\the\topsep}% +\edef\@IEEEiedlabelwidth{\the\labelwidth}% +\edef\@IEEEiedlabelsep{\the\labelsep}% +\edef\@IEEEiedleftmargin{\the\leftmargin}% +\edef\@IEEEiedpartopsep{\the\partopsep}% +\edef\@IEEEiedparsep{\the\parsep}% +\edef\@IEEEieditemsep{\the\itemsep}% +\edef\@IEEEiedrightmargin{\the\rightmargin}% +\edef\@IEEEiedlistparindent{\the\listparindent}% +\edef\@IEEEieditemindent{\the\itemindent}} + +% Note controlled spacing here +\def\@IEEErestorelistparams{\topsep\@IEEEiedtopsep\relax% +\labelwidth\@IEEEiedlabelwidth\relax% +\labelsep\@IEEEiedlabelsep\relax% +\leftmargin\@IEEEiedleftmargin\relax% +\partopsep\@IEEEiedpartopsep\relax% +\parsep\@IEEEiedparsep\relax% +\itemsep\@IEEEieditemsep\relax% +\rightmargin\@IEEEiedrightmargin\relax% +\listparindent\@IEEEiedlistparindent\relax% +\itemindent\@IEEEieditemindent\relax} + + +% v1.6b provide original LaTeX IED list environments +% note that latex.ltx defines \itemize and \enumerate, but not \description +% which must be created by the base classes +% save original LaTeX itemize and enumerate +\let\LaTeXitemize\itemize +\let\endLaTeXitemize\enditemize +\let\LaTeXenumerate\enumerate +\let\endLaTeXenumerate\endenumerate + +% provide original LaTeX description environment from article.cls +\newenvironment{LaTeXdescription} + {\list{}{\labelwidth\z@ \itemindent-\leftmargin + \let\makelabel\descriptionlabel}} + {\endlist} +\newcommand*\descriptionlabel[1]{\hspace\labelsep + \normalfont\bfseries #1} + + +% override LaTeX's default IED lists +\def\itemize{\@IEEEitemize} +\def\enditemize{\@endIEEEitemize} +\def\enumerate{\@IEEEenumerate} +\def\endenumerate{\@endIEEEenumerate} +\def\description{\@IEEEdescription} +\def\enddescription{\@endIEEEdescription} + +% provide the user with aliases - may help those using packages that +% override itemize, enumerate, or description +\def\IEEEitemize{\@IEEEitemize} +\def\endIEEEitemize{\@endIEEEitemize} +\def\IEEEenumerate{\@IEEEenumerate} +\def\endIEEEenumerate{\@endIEEEenumerate} +\def\IEEEdescription{\@IEEEdescription} +\def\endIEEEdescription{\@endIEEEdescription} + + +% V1.6 we want to keep the IEEEtran IED list definitions as our own internal +% commands so they are protected against redefinition +\def\@IEEEitemize{\@ifnextchar[{\@@IEEEitemize}{\@@IEEEitemize[\relax]}} +\def\@IEEEenumerate{\@ifnextchar[{\@@IEEEenumerate}{\@@IEEEenumerate[\relax]}} +\def\@IEEEdescription{\@ifnextchar[{\@@IEEEdescription}{\@@IEEEdescription[\relax]}} +\def\@endIEEEitemize{\endlist} +\def\@endIEEEenumerate{\endlist} +\def\@endIEEEdescription{\endlist} + + +% DO NOT ALLOW BLANK LINES TO BE IN THESE IED ENVIRONMENTS +% AS THIS WILL FORCE NEW PARAGRAPHS AFTER THE IED LISTS +% IEEEtran itemized list MDS 1/2001 +% Note controlled spacing here, shield end of lines with % +\def\@@IEEEitemize[#1]{% + \ifnum\@itemdepth>3\relax\@toodeep\else% + \ifnum\@listdepth>5\relax\@toodeep\else% + \advance\@itemdepth\@ne% + \edef\@itemitem{labelitem\romannumeral\the\@itemdepth}% + % get the labelindentfactor for this level + \advance\@listdepth\@ne% we need to know what the level WILL be + \edef\IEEElabelindentfactor{\csname IEEElabelindentfactor\romannumeral\the\@listdepth\endcsname}% + \advance\@listdepth-\@ne% undo our increment + \def\@IEEEiedjustify{2}% right justified labels are default + % set other defaults + \IEEEnocalcleftmarginfalse% + \IEEEnolabelindentfactorfalse% + \topsep\IEEEiedtopsep% + \IEEElabelindent\IEEEilabelindent% + \labelsep\IEEEiednormlabelsep% + \partopsep 0ex% + \parsep 0ex% + \itemsep \parskip% + \rightmargin 0em% + \listparindent 0em% + \itemindent 0em% + % calculate the label width + % the user can override this later if + % they specified a \labelwidth + \settowidth{\labelwidth}{\csname labelitem\romannumeral\the\@itemdepth\endcsname}% + \@IEEEsavelistparams% save our list parameters + \list{\csname\@itemitem\endcsname}{% + \@IEEErestorelistparams% override any list{} changes + % to our globals + \let\makelabel\@IEEEiedmakelabel% v1.6b setup \makelabel + \IEEEiedlistdecl% let user alter parameters + #1\relax% + % If the user has requested not to use the + % labelindent factor, don't revise \labelindent + \ifIEEEnolabelindentfactor\relax% + \else\IEEElabelindent=\IEEElabelindentfactor\labelindent% + \fi% + % Unless the user has requested otherwise, + % calculate our left margin based + % on \IEEElabelindent, \labelwidth and + % \labelsep + \ifIEEEnocalcleftmargin\relax% + \else\IEEEcalcleftmargin{\IEEElabelindent}% + \fi}\fi\fi}% + + +% DO NOT ALLOW BLANK LINES TO BE IN THESE IED ENVIRONMENTS +% AS THIS WILL FORCE NEW PARAGRAPHS AFTER THE IED LISTS +% IEEEtran enumerate list MDS 1/2001 +% Note controlled spacing here, shield end of lines with % +\def\@@IEEEenumerate[#1]{% + \ifnum\@enumdepth>3\relax\@toodeep\else% + \ifnum\@listdepth>5\relax\@toodeep\else% + \advance\@enumdepth\@ne% + \edef\@enumctr{enum\romannumeral\the\@enumdepth}% + % get the labelindentfactor for this level + \advance\@listdepth\@ne% we need to know what the level WILL be + \edef\IEEElabelindentfactor{\csname IEEElabelindentfactor\romannumeral\the\@listdepth\endcsname}% + \advance\@listdepth-\@ne% undo our increment + \def\@IEEEiedjustify{2}% right justified labels are default + % set other defaults + \IEEEnocalcleftmarginfalse% + \IEEEnolabelindentfactorfalse% + \topsep\IEEEiedtopsep% + \IEEElabelindent\IEEEelabelindent% + \labelsep\IEEEiednormlabelsep% + \partopsep 0ex% + \parsep 0ex% + \itemsep 0ex% + \rightmargin 0em% + \listparindent 0em% + \itemindent 0em% + % calculate the label width + % We'll set it to the width suitable for all labels using + % normalfont 1) to 9) + % The user can override this later + \settowidth{\labelwidth}{9)}% + \@IEEEsavelistparams% save our list parameters + \list{\csname label\@enumctr\endcsname}{\usecounter{\@enumctr}% + \@IEEErestorelistparams% override any list{} changes + % to our globals + \let\makelabel\@IEEEiedmakelabel% v1.6b setup \makelabel + \IEEEiedlistdecl% let user alter parameters + #1\relax% + % If the user has requested not to use the + % IEEElabelindent factor, don't revise \IEEElabelindent + \ifIEEEnolabelindentfactor\relax% + \else\IEEElabelindent=\IEEElabelindentfactor\IEEElabelindent% + \fi% + % Unless the user has requested otherwise, + % calculate our left margin based + % on \IEEElabelindent, \labelwidth and + % \labelsep + \ifIEEEnocalcleftmargin\relax% + \else\IEEEcalcleftmargin{\IEEElabelindent}% + \fi}\fi\fi}% + + +% DO NOT ALLOW BLANK LINES TO BE IN THESE IED ENVIRONMENTS +% AS THIS WILL FORCE NEW PARAGRAPHS AFTER THE IED LISTS +% IEEEtran description list MDS 1/2001 +% Note controlled spacing here, shield end of lines with % +\def\@@IEEEdescription[#1]{% + \ifnum\@listdepth>5\relax\@toodeep\else% + % get the labelindentfactor for this level + \advance\@listdepth\@ne% we need to know what the level WILL be + \edef\IEEElabelindentfactor{\csname IEEElabelindentfactor\romannumeral\the\@listdepth\endcsname}% + \advance\@listdepth-\@ne% undo our increment + \def\@IEEEiedjustify{0}% left justified labels are default + % set other defaults + \IEEEnocalcleftmarginfalse% + \IEEEnolabelindentfactorfalse% + \topsep\IEEEiedtopsep% + \IEEElabelindent\IEEEdlabelindent% + % assume normal labelsep + \labelsep\IEEEiednormlabelsep% + \partopsep 0ex% + \parsep 0ex% + \itemsep 0ex% + \rightmargin 0em% + \listparindent 0em% + \itemindent 0em% + % Bogus label width in case the user forgets + % to set it. + % TIP: If you want to see what a variable's width is you + % can use the TeX command \showthe\width-variable to + % display it on the screen during compilation + % (This might be helpful to know when you need to find out + % which label is the widest) + \settowidth{\labelwidth}{Hello}% + \@IEEEsavelistparams% save our list parameters + \list{}{\@IEEErestorelistparams% override any list{} changes + % to our globals + \let\makelabel\@IEEEiedmakelabel% v1.6b setup \makelabel + \IEEEiedlistdecl% let user alter parameters + #1\relax% + % If the user has requested not to use the + % labelindent factor, don't revise \IEEElabelindent + \ifIEEEnolabelindentfactor\relax% + \else\IEEElabelindent=\IEEElabelindentfactor\IEEElabelindent% + \fi% + % Unless the user has requested otherwise, + % calculate our left margin based + % on \IEEElabelindent, \labelwidth and + % \labelsep + \ifIEEEnocalcleftmargin\relax% + \else\IEEEcalcleftmargin{\IEEElabelindent}\relax% + \fi}\fi} + +% v1.6b we use one makelabel that does justification as needed. +\def\@IEEEiedmakelabel#1{\relax\if\@IEEEiedjustify 0\relax +\makebox[\labelwidth][l]{\normalfont #1}\else +\if\@IEEEiedjustify 1\relax +\makebox[\labelwidth][c]{\normalfont #1}\else +\makebox[\labelwidth][r]{\normalfont #1}\fi\fi} + + +% VERSE and QUOTE +% V1.7 define environments with newenvironment +\newenvironment{verse}{\let\\=\@centercr + \list{}{\itemsep\z@ \itemindent -1.5em \listparindent \itemindent + \rightmargin\leftmargin\advance\leftmargin 1.5em}\item\relax} + {\endlist} +\newenvironment{quotation}{\list{}{\listparindent 1.5em \itemindent\listparindent + \rightmargin\leftmargin \parsep 0pt plus 1pt}\item\relax} + {\endlist} +\newenvironment{quote}{\list{}{\rightmargin\leftmargin}\item\relax} + {\endlist} + + +% \titlepage +% provided only for backward compatibility. \maketitle is the correct +% way to create the title page. +\newif\if@restonecol +\def\titlepage{\@restonecolfalse\if@twocolumn\@restonecoltrue\onecolumn + \else \newpage \fi \thispagestyle{empty}\c@page\z@} +\def\endtitlepage{\if@restonecol\twocolumn \else \newpage \fi} + +% standard values from article.cls +\arraycolsep 5pt +\arrayrulewidth .4pt +\doublerulesep 2pt + +\tabcolsep 6pt +\tabbingsep 0.5em + + +%% FOOTNOTES +% +%\skip\footins 10pt plus 4pt minus 2pt +% V1.6 respond to changes in font size +% space added above the footnotes (if present) +\skip\footins 0.9\baselineskip plus 0.4\baselineskip minus 0.2\baselineskip + +% V1.6, we need to make \footnotesep responsive to changes +% in \baselineskip or strange spacings will result when in +% draft mode. Here is a little LaTeX secret - \footnotesep +% determines the height of an invisible strut that is placed +% *above* the baseline of footnotes after the first. Since +% LaTeX considers the space for characters to be 0.7/baselineskip +% above the baseline and 0.3/baselineskip below it, we need to +% use 0.7/baselineskip as a \footnotesep to maintain equal spacing +% between all the lines of the footnotes. IEEE often uses a tad +% more, so use 0.8\baselineskip. This slightly larger value also helps +% the text to clear the footnote marks. Note that \thanks in IEEEtran +% uses its own value of \footnotesep which is set in \maketitle. +{\footnotesize +\global\footnotesep 0.8\baselineskip} + +\def\unnumberedfootnote{\gdef\@thefnmark{\quad}\@footnotetext} + +\skip\@mpfootins 0.3\baselineskip +\fboxsep = 3pt +\fboxrule = .4pt +% V1.6 use 1em, then use LaTeX2e's \@makefnmark +% Note that IEEE normally *left* aligns the footnote marks, so we don't need +% box resizing tricks here. +%\long\def\@makefnmark{\scriptsize\normalfont\@thefnmark} +\long\def\@makefntext#1{\parindent 1em\indent\hbox{\@makefnmark}#1}% V1.6 use 1em +\long\def\@maketablefntext#1{\raggedleft\leavevmode\hbox{\@makefnmark}#1} +% V1.7 compsoc does not use superscipts for footnote marks +\ifCLASSOPTIONcompsoc +\def\@IEEEcompsocmakefnmark{\hbox{\normalfont\@thefnmark.\ }} +\long\def\@makefntext#1{\parindent 1em\indent\hbox{\@IEEEcompsocmakefnmark}#1} +\fi + +% IEEE does not use footnote rules. Or do they? +\def\footnoterule{\vskip-2pt \hrule height 0.6pt depth \z@ \vskip1.6pt\relax} +\toks@\expandafter{\@setminipage\let\footnoterule\relax\footnotesep\z@} +\edef\@setminipage{\the\toks@} + +% V1.7 for compsoc, IEEE uses a footnote rule only for \thanks. We devise a "one-shot" +% system to implement this. +\newif\if@IEEEenableoneshotfootnoterule +\@IEEEenableoneshotfootnoterulefalse +\ifCLASSOPTIONcompsoc +\def\footnoterule{\relax\if@IEEEenableoneshotfootnoterule +\kern-5pt +\hbox to \columnwidth{\hfill\vrule width 0.5\columnwidth height 0.4pt\hfill} +\kern4.6pt +\global\@IEEEenableoneshotfootnoterulefalse +\else +\relax +\fi} +\fi + +% V1.6 do not allow LaTeX to break a footnote across multiple pages +\interfootnotelinepenalty=10000 + +% V1.6 discourage breaks within equations +% Note that amsmath normally sets this to 10000, +% but LaTeX2e normally uses 100. +\interdisplaylinepenalty=2500 + +% default allows section depth up to /paragraph +\setcounter{secnumdepth}{4} + +% technotes do not allow /paragraph +\ifCLASSOPTIONtechnote + \setcounter{secnumdepth}{3} +\fi +% neither do compsoc conferences +\@IEEEcompsocconfonly{\setcounter{secnumdepth}{3}} + + +\newcounter{section} +\newcounter{subsection}[section] +\newcounter{subsubsection}[subsection] +\newcounter{paragraph}[subsubsection] + +% used only by IEEEtran's IEEEeqnarray as other packages may +% have their own, different, implementations +\newcounter{IEEEsubequation}[equation] + +% as shown when called by user from \ref, \label and in table of contents +\def\theequation{\arabic{equation}} % 1 +\def\theIEEEsubequation{\theequation\alph{IEEEsubequation}} % 1a (used only by IEEEtran's IEEEeqnarray) +\ifCLASSOPTIONcompsoc +% compsoc is all arabic +\def\thesection{\arabic{section}} +\def\thesubsection{\thesection.\arabic{subsection}} +\def\thesubsubsection{\thesubsection.\arabic{subsubsection}} +\def\theparagraph{\thesubsubsection.\arabic{paragraph}} +\else +\def\thesection{\Roman{section}} % I +% V1.7, \mbox prevents breaks around - +\def\thesubsection{\mbox{\thesection-\Alph{subsection}}} % I-A +% V1.7 use I-A1 format used by IEEE rather than I-A.1 +\def\thesubsubsection{\thesubsection\arabic{subsubsection}} % I-A1 +\def\theparagraph{\thesubsubsection\alph{paragraph}} % I-A1a +\fi + +% From Heiko Oberdiek. Because of the \mbox in \thesubsection, we need to +% tell hyperref to disable the \mbox command when making PDF bookmarks. +% This done already with hyperref.sty version 6.74o and later, but +% it will not hurt to do it here again for users of older versions. +\@ifundefined{pdfstringdefPreHook}{\let\pdfstringdefPreHook\@empty}{}% +\g@addto@macro\pdfstringdefPreHook{\let\mbox\relax} + + +% Main text forms (how shown in main text headings) +% V1.6, using \thesection in \thesectiondis allows changes +% in the former to automatically appear in the latter +\ifCLASSOPTIONcompsoc + \ifCLASSOPTIONconference% compsoc conference + \def\thesectiondis{\thesection.} + \def\thesubsectiondis{\thesectiondis\arabic{subsection}.} + \def\thesubsubsectiondis{\thesubsectiondis\arabic{subsubsection}.} + \def\theparagraphdis{\thesubsubsectiondis\arabic{paragraph}.} + \else% compsoc not conferencs + \def\thesectiondis{\thesection} + \def\thesubsectiondis{\thesectiondis.\arabic{subsection}} + \def\thesubsubsectiondis{\thesubsectiondis.\arabic{subsubsection}} + \def\theparagraphdis{\thesubsubsectiondis.\arabic{paragraph}} + \fi +\else% not compsoc + \def\thesectiondis{\thesection.} % I. + \def\thesubsectiondis{\Alph{subsection}.} % B. + \def\thesubsubsectiondis{\arabic{subsubsection})} % 3) + \def\theparagraphdis{\alph{paragraph})} % d) +\fi + +% just like LaTeX2e's \@eqnnum +\def\theequationdis{{\normalfont \normalcolor (\theequation)}}% (1) +% IEEEsubequation used only by IEEEtran's IEEEeqnarray +\def\theIEEEsubequationdis{{\normalfont \normalcolor (\theIEEEsubequation)}}% (1a) +% redirect LaTeX2e's equation number display and all that depend on +% it, through IEEEtran's \theequationdis +\def\@eqnnum{\theequationdis} + + + +% V1.7 provide string macros as article.cls does +\def\contentsname{Contents} +\def\listfigurename{List of Figures} +\def\listtablename{List of Tables} +\def\refname{References} +\def\indexname{Index} +\def\figurename{Fig.} +\def\tablename{TABLE} +\@IEEEcompsocconfonly{\def\figurename{Figure}\def\tablename{Table}} +\def\partname{Part} +\def\appendixname{Appendix} +\def\abstractname{Abstract} +% IEEE specific names +\def\IEEEkeywordsname{Keywords} +\def\IEEEproofname{Proof} + + +% LIST OF FIGURES AND TABLES AND TABLE OF CONTENTS +% +\def\@pnumwidth{1.55em} +\def\@tocrmarg{2.55em} +\def\@dotsep{4.5} +\setcounter{tocdepth}{3} + +% adjusted some spacings here so that section numbers will not easily +% collide with the section titles. +% VIII; VIII-A; and VIII-A.1 are usually the worst offenders. +% MDS 1/2001 +\def\tableofcontents{\section*{\contentsname}\@starttoc{toc}} +\def\l@section#1#2{\addpenalty{\@secpenalty}\addvspace{1.0em plus 1pt}% + \@tempdima 2.75em \begingroup \parindent \z@ \rightskip \@pnumwidth% + \parfillskip-\@pnumwidth {\bfseries\leavevmode #1}\hfil\hbox to\@pnumwidth{\hss #2}\par% + \endgroup} +% argument format #1:level, #2:labelindent,#3:labelsep +\def\l@subsection{\@dottedtocline{2}{2.75em}{3.75em}} +\def\l@subsubsection{\@dottedtocline{3}{6.5em}{4.5em}} +% must provide \l@ defs for ALL sublevels EVEN if tocdepth +% is such as they will not appear in the table of contents +% these defs are how TOC knows what level these things are! +\def\l@paragraph{\@dottedtocline{4}{6.5em}{5.5em}} +\def\l@subparagraph{\@dottedtocline{5}{6.5em}{6.5em}} +\def\listoffigures{\section*{\listfigurename}\@starttoc{lof}} +\def\l@figure{\@dottedtocline{1}{0em}{2.75em}} +\def\listoftables{\section*{\listtablename}\@starttoc{lot}} +\let\l@table\l@figure + + +%% Definitions for floats +%% +%% Normal Floats +\floatsep 1\baselineskip plus 0.2\baselineskip minus 0.2\baselineskip +\textfloatsep 1.7\baselineskip plus 0.2\baselineskip minus 0.4\baselineskip +\@fptop 0pt plus 1fil +\@fpsep 0.75\baselineskip plus 2fil +\@fpbot 0pt plus 1fil +\def\topfraction{0.9} +\def\bottomfraction{0.4} +\def\floatpagefraction{0.8} +% V1.7, let top floats approach 90% of page +\def\textfraction{0.1} + +%% Double Column Floats +\dblfloatsep 1\baselineskip plus 0.2\baselineskip minus 0.2\baselineskip + +\dbltextfloatsep 1.7\baselineskip plus 0.2\baselineskip minus 0.4\baselineskip +% Note that it would be nice if the rubber here actually worked in LaTeX2e. +% There is a long standing limitation in LaTeX, first discovered (to the best +% of my knowledge) by Alan Jeffrey in 1992. LaTeX ignores the stretchable +% portion of \dbltextfloatsep, and as a result, double column figures can and +% do result in an non-integer number of lines in the main text columns with +% underfull vbox errors as a consequence. A post to comp.text.tex +% by Donald Arseneau confirms that this had not yet been fixed in 1998. +% IEEEtran V1.6 will fix this problem for you in the titles, but it doesn't +% protect you from other double floats. Happy vspace'ing. + +\@dblfptop 0pt plus 1fil +\@dblfpsep 0.75\baselineskip plus 2fil +\@dblfpbot 0pt plus 1fil +\def\dbltopfraction{0.8} +\def\dblfloatpagefraction{0.8} +\setcounter{dbltopnumber}{4} + +\intextsep 1\baselineskip plus 0.2\baselineskip minus 0.2\baselineskip +\setcounter{topnumber}{2} +\setcounter{bottomnumber}{2} +\setcounter{totalnumber}{4} + + + +% article class provides these, we should too. +\newlength\abovecaptionskip +\newlength\belowcaptionskip +% but only \abovecaptionskip is used above figure captions and *below* table +% captions +\setlength\abovecaptionskip{0.65\baselineskip} +\setlength\belowcaptionskip{0.75\baselineskip} +% V1.6 create hooks in case the caption spacing ever needs to be +% overridden by a user +\def\@IEEEfigurecaptionsepspace{\vskip\abovecaptionskip\relax}% +\def\@IEEEtablecaptionsepspace{\vskip\belowcaptionskip\relax}% + + +% 1.6b revise caption system so that \@makecaption uses two arguments +% as with LaTeX2e. Otherwise, there will be problems when using hyperref. +\def\@IEEEtablestring{table} + +\ifCLASSOPTIONcompsoc +% V1.7 compsoc \@makecaption +\ifCLASSOPTIONconference% compsoc conference +\long\def\@makecaption#1#2{% +% test if is a for a figure or table +\ifx\@captype\@IEEEtablestring% +% if a table, do table caption +\normalsize\begin{center}{\normalfont\sffamily\normalsize {#1.}~ #2}\end{center}% +\@IEEEtablecaptionsepspace +% if not a table, format it as a figure +\else +\@IEEEfigurecaptionsepspace +\setbox\@tempboxa\hbox{\normalfont\sffamily\normalsize {#1.}~ #2}% +\ifdim \wd\@tempboxa >\hsize% +% if caption is longer than a line, let it wrap around +\setbox\@tempboxa\hbox{\normalfont\sffamily\normalsize {#1.}~ }% +\parbox[t]{\hsize}{\normalfont\sffamily\normalsize \noindent\unhbox\@tempboxa#2}% +% if caption is shorter than a line, center +\else% +\hbox to\hsize{\normalfont\sffamily\normalsize\hfil\box\@tempboxa\hfil}% +\fi\fi} +\else% nonconference compsoc +\long\def\@makecaption#1#2{% +% test if is a for a figure or table +\ifx\@captype\@IEEEtablestring% +% if a table, do table caption +\normalsize\begin{center}{\normalfont\sffamily\normalsize #1}\\{\normalfont\sffamily\normalsize #2}\end{center}% +\@IEEEtablecaptionsepspace +% if not a table, format it as a figure +\else +\@IEEEfigurecaptionsepspace +\setbox\@tempboxa\hbox{\normalfont\sffamily\normalsize {#1.}~ #2}% +\ifdim \wd\@tempboxa >\hsize% +% if caption is longer than a line, let it wrap around +\setbox\@tempboxa\hbox{\normalfont\sffamily\normalsize {#1.}~ }% +\parbox[t]{\hsize}{\normalfont\sffamily\normalsize \noindent\unhbox\@tempboxa#2}% +% if caption is shorter than a line, left justify +\else% +\hbox to\hsize{\normalfont\sffamily\normalsize\box\@tempboxa\hfil}% +\fi\fi} +\fi + +\else% traditional noncompsoc \@makecaption +\long\def\@makecaption#1#2{% +% test if is a for a figure or table +\ifx\@captype\@IEEEtablestring% +% if a table, do table caption +\footnotesize{\centering\normalfont\footnotesize#1.\qquad\scshape #2\par}% +\@IEEEtablecaptionsepspace +% if not a table, format it as a figure +\else +\@IEEEfigurecaptionsepspace +% 3/2001 use footnotesize, not small; use two nonbreaking spaces, not one +\setbox\@tempboxa\hbox{\normalfont\footnotesize {#1.}~~ #2}% +\ifdim \wd\@tempboxa >\hsize% +% if caption is longer than a line, let it wrap around +\setbox\@tempboxa\hbox{\normalfont\footnotesize {#1.}~~ }% +\parbox[t]{\hsize}{\normalfont\footnotesize\noindent\unhbox\@tempboxa#2}% +% if caption is shorter than a line, center if conference, left justify otherwise +\else% +\ifCLASSOPTIONconference \hbox to\hsize{\normalfont\footnotesize\box\@tempboxa\hfil}% +\else \hbox to\hsize{\normalfont\footnotesize\box\@tempboxa\hfil}% +\fi\fi\fi} +\fi + + + +% V1.7 disable captions class option, do so in a way that retains operation of \label +% within \caption +\ifCLASSOPTIONcaptionsoff +\long\def\@makecaption#1#2{\vspace*{2em}\footnotesize\begin{center}{\footnotesize #1}\end{center}% +\let\@IEEEtemporiglabeldefsave\label +\let\@IEEEtemplabelargsave\relax +\def\label##1{\gdef\@IEEEtemplabelargsave{##1}}% +\setbox\@tempboxa\hbox{#2}% +\let\label\@IEEEtemporiglabeldefsave +\ifx\@IEEEtemplabelargsave\relax\else\label{\@IEEEtemplabelargsave}\fi} +\fi + + +% V1.7 define end environments with \def not \let so as to work OK with +% preview-latex +\newcounter{figure} +\def\thefigure{\@arabic\c@figure} +\def\fps@figure{tbp} +\def\ftype@figure{1} +\def\ext@figure{lof} +\def\fnum@figure{\figurename~\thefigure} +\def\figure{\@float{figure}} +\def\endfigure{\end@float} +\@namedef{figure*}{\@dblfloat{figure}} +\@namedef{endfigure*}{\end@dblfloat} +\newcounter{table} +\ifCLASSOPTIONcompsoc +\def\thetable{\arabic{table}} +\else +\def\thetable{\@Roman\c@table} +\fi +\def\fps@table{tbp} +\def\ftype@table{2} +\def\ext@table{lot} +\def\fnum@table{\tablename~\thetable} +% V1.6 IEEE uses 8pt text for tables +% to default to footnotesize, we hack into LaTeX2e's \@floatboxreset and pray +\def\table{\def\@floatboxreset{\reset@font\scriptsize\@setminipage}% + \let\@makefntext\@maketablefntext + \@float{table}} +\def\endtable{\end@float} +% v1.6b double column tables need to default to footnotesize as well. +\@namedef{table*}{\def\@floatboxreset{\reset@font\scriptsize\@setminipage}\@dblfloat{table}} +\@namedef{endtable*}{\end@dblfloat} + + + + +%% +%% START OF IEEEeqnarry DEFINITIONS +%% +%% Inspired by the concepts, examples, and previous works of LaTeX +%% coders and developers such as Donald Arseneau, Fred Bartlett, +%% David Carlisle, Tony Liu, Frank Mittelbach, Piet van Oostrum, +%% Roland Winkler and Mark Wooding. +%% I don't make the claim that my work here is even near their calibre. ;) + + +% hook to allow easy changeover to IEEEtran.cls/tools.sty error reporting +\def\@IEEEclspkgerror{\ClassError{IEEEtran}} + +\newif\if@IEEEeqnarraystarform% flag to indicate if the environment was called as the star form +\@IEEEeqnarraystarformfalse + +\newif\if@advanceIEEEeqncolcnt% tracks if the environment should advance the col counter +% allows a way to make an \IEEEeqnarraybox that can be used within an \IEEEeqnarray +% used by IEEEeqnarraymulticol so that it can work properly in both +\@advanceIEEEeqncolcnttrue + +\newcount\@IEEEeqnnumcols % tracks how many IEEEeqnarray cols are defined +\newcount\@IEEEeqncolcnt % tracks how many IEEEeqnarray cols the user actually used + + +% The default math style used by the columns +\def\IEEEeqnarraymathstyle{\displaystyle} +% The default text style used by the columns +% default to using the current font +\def\IEEEeqnarraytextstyle{\relax} + +% like the iedlistdecl but for \IEEEeqnarray +\def\IEEEeqnarraydecl{\relax} +\def\IEEEeqnarrayboxdecl{\relax} + +% \yesnumber is the opposite of \nonumber +% a novel concept with the same def as the equationarray package +% However, we give IEEE versions too since some LaTeX packages such as +% the MDWtools mathenv.sty redefine \nonumber to something else. +\providecommand{\yesnumber}{\global\@eqnswtrue} +\def\IEEEyesnumber{\global\@eqnswtrue} +\def\IEEEnonumber{\global\@eqnswfalse} + + +\def\IEEEyessubnumber{\global\@IEEEissubequationtrue\global\@eqnswtrue% +\if@IEEEeqnarrayISinner% only do something inside an IEEEeqnarray +\if@IEEElastlinewassubequation\addtocounter{equation}{-1}\else\setcounter{IEEEsubequation}{1}\fi% +\def\@currentlabel{\p@IEEEsubequation\theIEEEsubequation}\fi} + +% flag to indicate that an equation is a sub equation +\newif\if@IEEEissubequation% +\@IEEEissubequationfalse + +% allows users to "push away" equations that get too close to the equation numbers +\def\IEEEeqnarraynumspace{\hphantom{\if@IEEEissubequation\theIEEEsubequationdis\else\theequationdis\fi}} + +% provides a way to span multiple columns within IEEEeqnarray environments +% will consider \if@advanceIEEEeqncolcnt before globally advancing the +% column counter - so as to work within \IEEEeqnarraybox +% usage: \IEEEeqnarraymulticol{number cols. to span}{col type}{cell text} +\long\def\IEEEeqnarraymulticol#1#2#3{\multispan{#1}% +% check if column is defined +\relax\expandafter\ifx\csname @IEEEeqnarraycolDEF#2\endcsname\@IEEEeqnarraycolisdefined% +\csname @IEEEeqnarraycolPRE#2\endcsname#3\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\csname @IEEEeqnarraycolPOST#2\endcsname% +\else% if not, error and use default type +\@IEEEclspkgerror{Invalid column type "#2" in \string\IEEEeqnarraymulticol.\MessageBreak +Using a default centering column instead}% +{You must define IEEEeqnarray column types before use.}% +\csname @IEEEeqnarraycolPRE@IEEEdefault\endcsname#3\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\csname @IEEEeqnarraycolPOST@IEEEdefault\endcsname% +\fi% +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by #1\relax\fi} + +% like \omit, but maintains track of the column counter for \IEEEeqnarray +\def\IEEEeqnarrayomit{\omit\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by 1\relax\fi} + + +% provides a way to define a letter referenced column type +% usage: \IEEEeqnarraydefcol{col. type letter/name}{pre insertion text}{post insertion text} +\def\IEEEeqnarraydefcol#1#2#3{\expandafter\def\csname @IEEEeqnarraycolPRE#1\endcsname{#2}% +\expandafter\def\csname @IEEEeqnarraycolPOST#1\endcsname{#3}% +\expandafter\def\csname @IEEEeqnarraycolDEF#1\endcsname{1}} + + +% provides a way to define a numerically referenced inter-column glue types +% usage: \IEEEeqnarraydefcolsep{col. glue number}{glue definition} +\def\IEEEeqnarraydefcolsep#1#2{\expandafter\def\csname @IEEEeqnarraycolSEP\romannumeral #1\endcsname{#2}% +\expandafter\def\csname @IEEEeqnarraycolSEPDEF\romannumeral #1\endcsname{1}} + + +\def\@IEEEeqnarraycolisdefined{1}% just a macro for 1, used for checking undefined column types + + +% expands and appends the given argument to the \@IEEEtrantmptoksA token list +% used to build up the \halign preamble +\def\@IEEEappendtoksA#1{\edef\@@IEEEappendtoksA{\@IEEEtrantmptoksA={\the\@IEEEtrantmptoksA #1}}% +\@@IEEEappendtoksA} + +% also appends to \@IEEEtrantmptoksA, but does not expand the argument +% uses \toks8 as a scratchpad register +\def\@IEEEappendNOEXPANDtoksA#1{\toks8={#1}% +\edef\@@IEEEappendNOEXPANDtoksA{\@IEEEtrantmptoksA={\the\@IEEEtrantmptoksA\the\toks8}}% +\@@IEEEappendNOEXPANDtoksA} + +% define some common column types for the user +% math +\IEEEeqnarraydefcol{l}{$\IEEEeqnarraymathstyle}{$\hfil} +\IEEEeqnarraydefcol{c}{\hfil$\IEEEeqnarraymathstyle}{$\hfil} +\IEEEeqnarraydefcol{r}{\hfil$\IEEEeqnarraymathstyle}{$} +\IEEEeqnarraydefcol{L}{$\IEEEeqnarraymathstyle{}}{{}$\hfil} +\IEEEeqnarraydefcol{C}{\hfil$\IEEEeqnarraymathstyle{}}{{}$\hfil} +\IEEEeqnarraydefcol{R}{\hfil$\IEEEeqnarraymathstyle{}}{{}$} +% text +\IEEEeqnarraydefcol{s}{\IEEEeqnarraytextstyle}{\hfil} +\IEEEeqnarraydefcol{t}{\hfil\IEEEeqnarraytextstyle}{\hfil} +\IEEEeqnarraydefcol{u}{\hfil\IEEEeqnarraytextstyle}{} + +% vertical rules +\IEEEeqnarraydefcol{v}{}{\vrule width\arrayrulewidth} +\IEEEeqnarraydefcol{vv}{\vrule width\arrayrulewidth\hfil}{\hfil\vrule width\arrayrulewidth} +\IEEEeqnarraydefcol{V}{}{\vrule width\arrayrulewidth\hskip\doublerulesep\vrule width\arrayrulewidth} +\IEEEeqnarraydefcol{VV}{\vrule width\arrayrulewidth\hskip\doublerulesep\vrule width\arrayrulewidth\hfil}% +{\hfil\vrule width\arrayrulewidth\hskip\doublerulesep\vrule width\arrayrulewidth} + +% horizontal rules +\IEEEeqnarraydefcol{h}{}{\leaders\hrule height\arrayrulewidth\hfil} +\IEEEeqnarraydefcol{H}{}{\leaders\vbox{\hrule width\arrayrulewidth\vskip\doublerulesep\hrule width\arrayrulewidth}\hfil} + +% plain +\IEEEeqnarraydefcol{x}{}{} +\IEEEeqnarraydefcol{X}{$}{$} + +% the default column type to use in the event a column type is not defined +\IEEEeqnarraydefcol{@IEEEdefault}{\hfil$\IEEEeqnarraymathstyle}{$\hfil} + + +% a zero tabskip (used for "-" col types) +\def\@IEEEeqnarraycolSEPzero{0pt plus 0pt minus 0pt} +% a centering tabskip (used for "+" col types) +\def\@IEEEeqnarraycolSEPcenter{1000pt plus 0pt minus 1000pt} + +% top level default tabskip glues for the start, end, and inter-column +% may be reset within environments not always at the top level, e.g., \IEEEeqnarraybox +\edef\@IEEEeqnarraycolSEPdefaultstart{\@IEEEeqnarraycolSEPcenter}% default start glue +\edef\@IEEEeqnarraycolSEPdefaultend{\@IEEEeqnarraycolSEPcenter}% default end glue +\edef\@IEEEeqnarraycolSEPdefaultmid{\@IEEEeqnarraycolSEPzero}% default inter-column glue + + + +% creates a vertical rule that extends from the bottom to the top a a cell +% Provided in case other packages redefine \vline some other way. +% usage: \IEEEeqnarrayvrule[rule thickness] +% If no argument is provided, \arrayrulewidth will be used for the rule thickness. +\newcommand\IEEEeqnarrayvrule[1][\arrayrulewidth]{\vrule\@width#1\relax} + +% creates a blank separator row +% usage: \IEEEeqnarrayseprow[separation length][font size commands] +% default is \IEEEeqnarrayseprow[0.25\normalbaselineskip][\relax] +% blank arguments inherit the default values +% uses \skip5 as a scratch register - calls \@IEEEeqnarraystrutsize which uses more scratch registers +\def\IEEEeqnarrayseprow{\relax\@ifnextchar[{\@IEEEeqnarrayseprow}{\@IEEEeqnarrayseprow[0.25\normalbaselineskip]}} +\def\@IEEEeqnarrayseprow[#1]{\relax\@ifnextchar[{\@@IEEEeqnarrayseprow[#1]}{\@@IEEEeqnarrayseprow[#1][\relax]}} +\def\@@IEEEeqnarrayseprow[#1][#2]{\def\@IEEEeqnarrayseprowARGONE{#1}% +\ifx\@IEEEeqnarrayseprowARGONE\@empty% +% get the skip value, based on the font commands +% use skip5 because \IEEEeqnarraystrutsize uses \skip0, \skip2, \skip3 +% assign within a bogus box to confine the font changes +{\setbox0=\hbox{#2\relax\global\skip5=0.25\normalbaselineskip}}% +\else% +{\setbox0=\hbox{#2\relax\global\skip5=#1}}% +\fi% +\@IEEEeqnarrayhoptolastcolumn\IEEEeqnarraystrutsize{\skip5}{0pt}[\relax]\relax} + +% creates a blank separator row, but omits all the column templates +% usage: \IEEEeqnarrayseprowcut[separation length][font size commands] +% default is \IEEEeqnarrayseprowcut[0.25\normalbaselineskip][\relax] +% blank arguments inherit the default values +% uses \skip5 as a scratch register - calls \@IEEEeqnarraystrutsize which uses more scratch registers +\def\IEEEeqnarrayseprowcut{\multispan{\@IEEEeqnnumcols}\relax% span all the cols +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\@ifnextchar[{\@IEEEeqnarrayseprowcut}{\@IEEEeqnarrayseprowcut[0.25\normalbaselineskip]}} +\def\@IEEEeqnarrayseprowcut[#1]{\relax\@ifnextchar[{\@@IEEEeqnarrayseprowcut[#1]}{\@@IEEEeqnarrayseprowcut[#1][\relax]}} +\def\@@IEEEeqnarrayseprowcut[#1][#2]{\def\@IEEEeqnarrayseprowARGONE{#1}% +\ifx\@IEEEeqnarrayseprowARGONE\@empty% +% get the skip value, based on the font commands +% use skip5 because \IEEEeqnarraystrutsize uses \skip0, \skip2, \skip3 +% assign within a bogus box to confine the font changes +{\setbox0=\hbox{#2\relax\global\skip5=0.25\normalbaselineskip}}% +\else% +{\setbox0=\hbox{#2\relax\global\skip5=#1}}% +\fi% +\IEEEeqnarraystrutsize{\skip5}{0pt}[\relax]\relax} + + + +% draws a single rule across all the columns optional +% argument determines the rule width, \arrayrulewidth is the default +% updates column counter as needed and turns off struts +% usage: \IEEEeqnarrayrulerow[rule line thickness] +\def\IEEEeqnarrayrulerow{\multispan{\@IEEEeqnnumcols}\relax% span all the cols +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\@ifnextchar[{\@IEEEeqnarrayrulerow}{\@IEEEeqnarrayrulerow[\arrayrulewidth]}} +\def\@IEEEeqnarrayrulerow[#1]{\leaders\hrule height#1\hfil\relax% put in our rule +% turn off any struts +\IEEEeqnarraystrutsize{0pt}{0pt}[\relax]\relax} + + +% draws a double rule by using a single rule row, a separator row, and then +% another single rule row +% first optional argument determines the rule thicknesses, \arrayrulewidth is the default +% second optional argument determines the rule spacing, \doublerulesep is the default +% usage: \IEEEeqnarraydblrulerow[rule line thickness][rule spacing] +\def\IEEEeqnarraydblrulerow{\multispan{\@IEEEeqnnumcols}\relax% span all the cols +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\@ifnextchar[{\@IEEEeqnarraydblrulerow}{\@IEEEeqnarraydblrulerow[\arrayrulewidth]}} +\def\@IEEEeqnarraydblrulerow[#1]{\relax\@ifnextchar[{\@@IEEEeqnarraydblrulerow[#1]}% +{\@@IEEEeqnarraydblrulerow[#1][\doublerulesep]}} +\def\@@IEEEeqnarraydblrulerow[#1][#2]{\def\@IEEEeqnarraydblrulerowARG{#1}% +% we allow the user to say \IEEEeqnarraydblrulerow[][] +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\@IEEEeqnarrayrulerow[\arrayrulewidth]% +\else% +\@IEEEeqnarrayrulerow[#1]\relax% +\fi% +\def\@IEEEeqnarraydblrulerowARG{#2}% +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\\\IEEEeqnarrayseprow[\doublerulesep][\relax]% +\else% +\\\IEEEeqnarrayseprow[#2][\relax]% +\fi% +\\\multispan{\@IEEEeqnnumcols}% +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\def\@IEEEeqnarraydblrulerowARG{#1}% +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\@IEEEeqnarrayrulerow[\arrayrulewidth]% +\else% +\@IEEEeqnarrayrulerow[#1]% +\fi% +} + +% draws a double rule by using a single rule row, a separator (cutting) row, and then +% another single rule row +% first optional argument determines the rule thicknesses, \arrayrulewidth is the default +% second optional argument determines the rule spacing, \doublerulesep is the default +% usage: \IEEEeqnarraydblrulerow[rule line thickness][rule spacing] +\def\IEEEeqnarraydblrulerowcut{\multispan{\@IEEEeqnnumcols}\relax% span all the cols +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\@ifnextchar[{\@IEEEeqnarraydblrulerowcut}{\@IEEEeqnarraydblrulerowcut[\arrayrulewidth]}} +\def\@IEEEeqnarraydblrulerowcut[#1]{\relax\@ifnextchar[{\@@IEEEeqnarraydblrulerowcut[#1]}% +{\@@IEEEeqnarraydblrulerowcut[#1][\doublerulesep]}} +\def\@@IEEEeqnarraydblrulerowcut[#1][#2]{\def\@IEEEeqnarraydblrulerowARG{#1}% +% we allow the user to say \IEEEeqnarraydblrulerow[][] +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\@IEEEeqnarrayrulerow[\arrayrulewidth]% +\else% +\@IEEEeqnarrayrulerow[#1]% +\fi% +\def\@IEEEeqnarraydblrulerowARG{#2}% +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\\\IEEEeqnarrayseprowcut[\doublerulesep][\relax]% +\else% +\\\IEEEeqnarrayseprowcut[#2][\relax]% +\fi% +\\\multispan{\@IEEEeqnnumcols}% +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\def\@IEEEeqnarraydblrulerowARG{#1}% +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\@IEEEeqnarrayrulerow[\arrayrulewidth]% +\else% +\@IEEEeqnarrayrulerow[#1]% +\fi% +} + + + +% inserts a full row's worth of &'s +% relies on \@IEEEeqnnumcols to provide the correct number of columns +% uses \@IEEEtrantmptoksA, \count0 as scratch registers +\def\@IEEEeqnarrayhoptolastcolumn{\@IEEEtrantmptoksA={}\count0=1\relax% +\loop% add cols if the user did not use them all +\ifnum\count0<\@IEEEeqnnumcols\relax% +\@IEEEappendtoksA{&}% +\advance\count0 by 1\relax% update the col count +\repeat% +\the\@IEEEtrantmptoksA%execute the &'s +} + + + +\newif\if@IEEEeqnarrayISinner % flag to indicate if we are within the lines +\@IEEEeqnarrayISinnerfalse % of an IEEEeqnarray - after the IEEEeqnarraydecl + +\edef\@IEEEeqnarrayTHEstrutheight{0pt} % height and depth of IEEEeqnarray struts +\edef\@IEEEeqnarrayTHEstrutdepth{0pt} + +\edef\@IEEEeqnarrayTHEmasterstrutheight{0pt} % default height and depth of +\edef\@IEEEeqnarrayTHEmasterstrutdepth{0pt} % struts within an IEEEeqnarray + +\edef\@IEEEeqnarrayTHEmasterstrutHSAVE{0pt} % saved master strut height +\edef\@IEEEeqnarrayTHEmasterstrutDSAVE{0pt} % and depth + +\newif\if@IEEEeqnarrayusemasterstrut % flag to indicate that the master strut value +\@IEEEeqnarrayusemasterstruttrue % is to be used + + + +% saves the strut height and depth of the master strut +\def\@IEEEeqnarraymasterstrutsave{\relax% +\expandafter\skip0=\@IEEEeqnarrayTHEmasterstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEmasterstrutdepth\relax% +% remove stretchability +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% save values +\edef\@IEEEeqnarrayTHEmasterstrutHSAVE{\the\dimen0}% +\edef\@IEEEeqnarrayTHEmasterstrutDSAVE{\the\dimen2}} + +% restores the strut height and depth of the master strut +\def\@IEEEeqnarraymasterstrutrestore{\relax% +\expandafter\skip0=\@IEEEeqnarrayTHEmasterstrutHSAVE\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEmasterstrutDSAVE\relax% +% remove stretchability +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% restore values +\edef\@IEEEeqnarrayTHEmasterstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEmasterstrutdepth{\the\dimen2}} + + +% globally restores the strut height and depth to the +% master values and sets the master strut flag to true +\def\@IEEEeqnarraystrutreset{\relax% +\expandafter\skip0=\@IEEEeqnarrayTHEmasterstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEmasterstrutdepth\relax% +% remove stretchability +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% restore values +\xdef\@IEEEeqnarrayTHEstrutheight{\the\dimen0}% +\xdef\@IEEEeqnarrayTHEstrutdepth{\the\dimen2}% +\global\@IEEEeqnarrayusemasterstruttrue} + + +% if the master strut is not to be used, make the current +% values of \@IEEEeqnarrayTHEstrutheight, \@IEEEeqnarrayTHEstrutdepth +% and the use master strut flag, global +% this allows user strut commands issued in the last column to be carried +% into the isolation/strut column +\def\@IEEEeqnarrayglobalizestrutstatus{\relax% +\if@IEEEeqnarrayusemasterstrut\else% +\xdef\@IEEEeqnarrayTHEstrutheight{\@IEEEeqnarrayTHEstrutheight}% +\xdef\@IEEEeqnarrayTHEstrutdepth{\@IEEEeqnarrayTHEstrutdepth}% +\global\@IEEEeqnarrayusemasterstrutfalse% +\fi} + + + +% usage: \IEEEeqnarraystrutsize{height}{depth}[font size commands] +% If called outside the lines of an IEEEeqnarray, sets the height +% and depth of both the master and local struts. If called inside +% an IEEEeqnarray line, sets the height and depth of the local strut +% only and sets the flag to indicate the use of the local strut +% values. If the height or depth is left blank, 0.7\normalbaselineskip +% and 0.3\normalbaselineskip will be used, respectively. +% The optional argument can be used to evaluate the lengths under +% a different font size and styles. If none is specified, the current +% font is used. +% uses scratch registers \skip0, \skip2, \skip3, \dimen0, \dimen2 +\def\IEEEeqnarraystrutsize#1#2{\relax\@ifnextchar[{\@IEEEeqnarraystrutsize{#1}{#2}}{\@IEEEeqnarraystrutsize{#1}{#2}[\relax]}} +\def\@IEEEeqnarraystrutsize#1#2[#3]{\def\@IEEEeqnarraystrutsizeARG{#1}% +\ifx\@IEEEeqnarraystrutsizeARG\@empty% +{\setbox0=\hbox{#3\relax\global\skip3=0.7\normalbaselineskip}}% +\skip0=\skip3\relax% +\else% arg one present +{\setbox0=\hbox{#3\relax\global\skip3=#1\relax}}% +\skip0=\skip3\relax% +\fi% if null arg +\def\@IEEEeqnarraystrutsizeARG{#2}% +\ifx\@IEEEeqnarraystrutsizeARG\@empty% +{\setbox0=\hbox{#3\relax\global\skip3=0.3\normalbaselineskip}}% +\skip2=\skip3\relax% +\else% arg two present +{\setbox0=\hbox{#3\relax\global\skip3=#2\relax}}% +\skip2=\skip3\relax% +\fi% if null arg +% remove stretchability, just to be safe +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% dimen0 = height, dimen2 = depth +\if@IEEEeqnarrayISinner% inner does not touch master strut size +\edef\@IEEEeqnarrayTHEstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEstrutdepth{\the\dimen2}% +\@IEEEeqnarrayusemasterstrutfalse% do not use master +\else% outer, have to set master strut too +\edef\@IEEEeqnarrayTHEmasterstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEmasterstrutdepth{\the\dimen2}% +\edef\@IEEEeqnarrayTHEstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEstrutdepth{\the\dimen2}% +\@IEEEeqnarrayusemasterstruttrue% use master strut +\fi} + + +% usage: \IEEEeqnarraystrutsizeadd{added height}{added depth}[font size commands] +% If called outside the lines of an IEEEeqnarray, adds the given height +% and depth to both the master and local struts. +% If called inside an IEEEeqnarray line, adds the given height and depth +% to the local strut only and sets the flag to indicate the use +% of the local strut values. +% In both cases, if a height or depth is left blank, 0pt is used instead. +% The optional argument can be used to evaluate the lengths under +% a different font size and styles. If none is specified, the current +% font is used. +% uses scratch registers \skip0, \skip2, \skip3, \dimen0, \dimen2 +\def\IEEEeqnarraystrutsizeadd#1#2{\relax\@ifnextchar[{\@IEEEeqnarraystrutsizeadd{#1}{#2}}{\@IEEEeqnarraystrutsizeadd{#1}{#2}[\relax]}} +\def\@IEEEeqnarraystrutsizeadd#1#2[#3]{\def\@IEEEeqnarraystrutsizearg{#1}% +\ifx\@IEEEeqnarraystrutsizearg\@empty% +\skip0=0pt\relax% +\else% arg one present +{\setbox0=\hbox{#3\relax\global\skip3=#1}}% +\skip0=\skip3\relax% +\fi% if null arg +\def\@IEEEeqnarraystrutsizearg{#2}% +\ifx\@IEEEeqnarraystrutsizearg\@empty% +\skip2=0pt\relax% +\else% arg two present +{\setbox0=\hbox{#3\relax\global\skip3=#2}}% +\skip2=\skip3\relax% +\fi% if null arg +% remove stretchability, just to be safe +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% dimen0 = height, dimen2 = depth +\if@IEEEeqnarrayISinner% inner does not touch master strut size +% get local strut size +\expandafter\skip0=\@IEEEeqnarrayTHEstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEstrutdepth\relax% +% add it to the user supplied values +\advance\dimen0 by \skip0\relax% +\advance\dimen2 by \skip2\relax% +% update the local strut size +\edef\@IEEEeqnarrayTHEstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEstrutdepth{\the\dimen2}% +\@IEEEeqnarrayusemasterstrutfalse% do not use master +\else% outer, have to set master strut too +% get master strut size +\expandafter\skip0=\@IEEEeqnarrayTHEmasterstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEmasterstrutdepth\relax% +% add it to the user supplied values +\advance\dimen0 by \skip0\relax% +\advance\dimen2 by \skip2\relax% +% update the local and master strut sizes +\edef\@IEEEeqnarrayTHEmasterstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEmasterstrutdepth{\the\dimen2}% +\edef\@IEEEeqnarrayTHEstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEstrutdepth{\the\dimen2}% +\@IEEEeqnarrayusemasterstruttrue% use master strut +\fi} + + +% allow user a way to see the struts +\newif\ifIEEEvisiblestruts +\IEEEvisiblestrutsfalse + +% inserts an invisible strut using the master or local strut values +% uses scratch registers \skip0, \skip2, \dimen0, \dimen2 +\def\@IEEEeqnarrayinsertstrut{\relax% +\if@IEEEeqnarrayusemasterstrut +% get master strut size +\expandafter\skip0=\@IEEEeqnarrayTHEmasterstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEmasterstrutdepth\relax% +\else% +% get local strut size +\expandafter\skip0=\@IEEEeqnarrayTHEstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEstrutdepth\relax% +\fi% +% remove stretchability, probably not needed +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% dimen0 = height, dimen2 = depth +% allow user to see struts if desired +\ifIEEEvisiblestruts% +\vrule width0.2pt height\dimen0 depth\dimen2\relax% +\else% +\vrule width0pt height\dimen0 depth\dimen2\relax\fi} + + +% creates an invisible strut, useable even outside \IEEEeqnarray +% if \IEEEvisiblestrutstrue, the strut will be visible and 0.2pt wide. +% usage: \IEEEstrut[height][depth][font size commands] +% default is \IEEEstrut[0.7\normalbaselineskip][0.3\normalbaselineskip][\relax] +% blank arguments inherit the default values +% uses \dimen0, \dimen2, \skip0, \skip2 +\def\IEEEstrut{\relax\@ifnextchar[{\@IEEEstrut}{\@IEEEstrut[0.7\normalbaselineskip]}} +\def\@IEEEstrut[#1]{\relax\@ifnextchar[{\@@IEEEstrut[#1]}{\@@IEEEstrut[#1][0.3\normalbaselineskip]}} +\def\@@IEEEstrut[#1][#2]{\relax\@ifnextchar[{\@@@IEEEstrut[#1][#2]}{\@@@IEEEstrut[#1][#2][\relax]}} +\def\@@@IEEEstrut[#1][#2][#3]{\mbox{#3\relax% +\def\@IEEEstrutARG{#1}% +\ifx\@IEEEstrutARG\@empty% +\skip0=0.7\normalbaselineskip\relax% +\else% +\skip0=#1\relax% +\fi% +\def\@IEEEstrutARG{#2}% +\ifx\@IEEEstrutARG\@empty% +\skip2=0.3\normalbaselineskip\relax% +\else% +\skip2=#2\relax% +\fi% +% remove stretchability, probably not needed +\dimen0\skip0\relax% +\dimen2\skip2\relax% +\ifIEEEvisiblestruts% +\vrule width0.2pt height\dimen0 depth\dimen2\relax% +\else% +\vrule width0.0pt height\dimen0 depth\dimen2\relax\fi}} + + +% enables strut mode by setting a default strut size and then zeroing the +% \baselineskip, \lineskip, \lineskiplimit and \jot +\def\IEEEeqnarraystrutmode{\IEEEeqnarraystrutsize{0.7\normalbaselineskip}{0.3\normalbaselineskip}[\relax]% +\baselineskip=0pt\lineskip=0pt\lineskiplimit=0pt\jot=0pt} + + + +\def\IEEEeqnarray{\@IEEEeqnarraystarformfalse\@IEEEeqnarray} +\def\endIEEEeqnarray{\end@IEEEeqnarray} + +\@namedef{IEEEeqnarray*}{\@IEEEeqnarraystarformtrue\@IEEEeqnarray} +\@namedef{endIEEEeqnarray*}{\end@IEEEeqnarray} + + +% \IEEEeqnarray is an enhanced \eqnarray. +% The star form defaults to not putting equation numbers at the end of each row. +% usage: \IEEEeqnarray[decl]{cols} +\def\@IEEEeqnarray{\relax\@ifnextchar[{\@@IEEEeqnarray}{\@@IEEEeqnarray[\relax]}} +\def\@@IEEEeqnarray[#1]#2{% + % default to showing the equation number or not based on whether or not + % the star form was involked + \if@IEEEeqnarraystarform\global\@eqnswfalse + \else% not the star form + \global\@eqnswtrue + \fi% if star form + \@IEEEissubequationfalse% default to no subequations + \@IEEElastlinewassubequationfalse% assume last line is not a sub equation + \@IEEEeqnarrayISinnerfalse% not yet within the lines of the halign + \@IEEEeqnarraystrutsize{0pt}{0pt}[\relax]% turn off struts by default + \@IEEEeqnarrayusemasterstruttrue% use master strut till user asks otherwise + \IEEEvisiblestrutsfalse% diagnostic mode defaults to off + % no extra space unless the user specifically requests it + \lineskip=0pt\relax + \lineskiplimit=0pt\relax + \baselineskip=\normalbaselineskip\relax% + \jot=\IEEEnormaljot\relax% + \mathsurround\z@\relax% no extra spacing around math + \@advanceIEEEeqncolcnttrue% advance the col counter for each col the user uses, + % used in \IEEEeqnarraymulticol and in the preamble build + \stepcounter{equation}% advance equation counter before first line + \setcounter{IEEEsubequation}{0}% no subequation yet + \def\@currentlabel{\p@equation\theequation}% redefine the ref label + \IEEEeqnarraydecl\relax% allow a way for the user to make global overrides + #1\relax% allow user to override defaults + \let\\\@IEEEeqnarraycr% replace newline with one that can put in eqn. numbers + \global\@IEEEeqncolcnt\z@% col. count = 0 for first line + \@IEEEbuildpreamble #2\end\relax% build the preamble and put it into \@IEEEtrantmptoksA + % put in the column for the equation number + \ifnum\@IEEEeqnnumcols>0\relax\@IEEEappendtoksA{&}\fi% col separator for those after the first + \toks0={##}% + % advance the \@IEEEeqncolcnt for the isolation col, this helps with error checking + \@IEEEappendtoksA{\global\advance\@IEEEeqncolcnt by 1\relax}% + % add the isolation column + \@IEEEappendtoksA{\tabskip\z@skip\bgroup\the\toks0\egroup}% + % advance the \@IEEEeqncolcnt for the equation number col, this helps with error checking + \@IEEEappendtoksA{&\global\advance\@IEEEeqncolcnt by 1\relax}% + % add the equation number col to the preamble + \@IEEEappendtoksA{\tabskip\z@skip\hb@xt@\z@\bgroup\hss\the\toks0\egroup}% + % note \@IEEEeqnnumcols does not count the equation col or isolation col + % set the starting tabskip glue as determined by the preamble build + \tabskip=\@IEEEBPstartglue\relax + % begin the display alignment + \@IEEEeqnarrayISinnertrue% commands are now within the lines + $$\everycr{}\halign to\displaywidth\bgroup + % "exspand" the preamble + \span\the\@IEEEtrantmptoksA\cr} + +% enter isolation/strut column (or the next column if the user did not use +% every column), record the strut status, complete the columns, do the strut if needed, +% restore counters to correct values and exit +\def\end@IEEEeqnarray{\@IEEEeqnarrayglobalizestrutstatus&\@@IEEEeqnarraycr\egroup% +\if@IEEElastlinewassubequation\global\advance\c@IEEEsubequation\m@ne\fi% +\global\advance\c@equation\m@ne% +$$\@ignoretrue} + +% need a way to remember if last line is a subequation +\newif\if@IEEElastlinewassubequation% +\@IEEElastlinewassubequationfalse + +% IEEEeqnarray uses a modifed \\ instead of the plain \cr to +% end rows. This allows for things like \\*[vskip amount] +% This "cr" macros are modified versions those for LaTeX2e's eqnarray +% the {\ifnum0=`} braces must be kept away from the last column to avoid +% altering spacing of its math, so we use & to advance to the next column +% as there is an isolation/strut column after the user's columns +\def\@IEEEeqnarraycr{\@IEEEeqnarrayglobalizestrutstatus&% save strut status and advance to next column + {\ifnum0=`}\fi + \@ifstar{% + \global\@eqpen\@M\@IEEEeqnarrayYCR + }{% + \global\@eqpen\interdisplaylinepenalty \@IEEEeqnarrayYCR + }% +} + +\def\@IEEEeqnarrayYCR{\@testopt\@IEEEeqnarrayXCR\z@skip} + +\def\@IEEEeqnarrayXCR[#1]{% + \ifnum0=`{\fi}% + \@@IEEEeqnarraycr + \noalign{\penalty\@eqpen\vskip\jot\vskip #1\relax}}% + +\def\@@IEEEeqnarraycr{\@IEEEtrantmptoksA={}% clear token register + \advance\@IEEEeqncolcnt by -1\relax% adjust col count because of the isolation column + \ifnum\@IEEEeqncolcnt>\@IEEEeqnnumcols\relax + \@IEEEclspkgerror{Too many columns within the IEEEeqnarray\MessageBreak + environment}% + {Use fewer \string &'s or put more columns in the IEEEeqnarry column\MessageBreak + specifications.}\relax% + \else + \loop% add cols if the user did not use them all + \ifnum\@IEEEeqncolcnt<\@IEEEeqnnumcols\relax + \@IEEEappendtoksA{&}% + \advance\@IEEEeqncolcnt by 1\relax% update the col count + \repeat + % this number of &'s will take us the the isolation column + \fi + % execute the &'s + \the\@IEEEtrantmptoksA% + % handle the strut/isolation column + \@IEEEeqnarrayinsertstrut% do the strut if needed + \@IEEEeqnarraystrutreset% reset the strut system for next line or IEEEeqnarray + &% and enter the equation number column + % is this line needs an equation number, display it and advance the + % (sub)equation counters, record what type this line was + \if@eqnsw% + \if@IEEEissubequation\theIEEEsubequationdis\addtocounter{equation}{1}\stepcounter{IEEEsubequation}% + \global\@IEEElastlinewassubequationtrue% + \else% display a standard equation number, initialize the IEEEsubequation counter + \theequationdis\stepcounter{equation}\setcounter{IEEEsubequation}{0}% + \global\@IEEElastlinewassubequationfalse\fi% + \fi% + % reset the eqnsw flag to indicate default preference of the display of equation numbers + \if@IEEEeqnarraystarform\global\@eqnswfalse\else\global\@eqnswtrue\fi + \global\@IEEEissubequationfalse% reset the subequation flag + % reset the number of columns the user actually used + \global\@IEEEeqncolcnt\z@\relax + % the real end of the line + \cr} + + + + + +% \IEEEeqnarraybox is like \IEEEeqnarray except the box form puts everything +% inside a vtop, vbox, or vcenter box depending on the letter in the second +% optional argument (t,b,c). Vbox is the default. Unlike \IEEEeqnarray, +% equation numbers are not displayed and \IEEEeqnarraybox can be nested. +% \IEEEeqnarrayboxm is for math mode (like \array) and does not put the vbox +% within an hbox. +% \IEEEeqnarrayboxt is for text mode (like \tabular) and puts the vbox within +% a \hbox{$ $} construct. +% \IEEEeqnarraybox will auto detect whether to use \IEEEeqnarrayboxm or +% \IEEEeqnarrayboxt depending on the math mode. +% The third optional argument specifies the width this box is to be set to - +% natural width is the default. +% The * forms do not add \jot line spacing +% usage: \IEEEeqnarraybox[decl][pos][width]{cols} +\def\IEEEeqnarrayboxm{\@IEEEeqnarraystarformfalse\@IEEEeqnarrayboxHBOXSWfalse\@IEEEeqnarraybox} +\def\endIEEEeqnarrayboxm{\end@IEEEeqnarraybox} +\@namedef{IEEEeqnarrayboxm*}{\@IEEEeqnarraystarformtrue\@IEEEeqnarrayboxHBOXSWfalse\@IEEEeqnarraybox} +\@namedef{endIEEEeqnarrayboxm*}{\end@IEEEeqnarraybox} + +\def\IEEEeqnarrayboxt{\@IEEEeqnarraystarformfalse\@IEEEeqnarrayboxHBOXSWtrue\@IEEEeqnarraybox} +\def\endIEEEeqnarrayboxt{\end@IEEEeqnarraybox} +\@namedef{IEEEeqnarrayboxt*}{\@IEEEeqnarraystarformtrue\@IEEEeqnarrayboxHBOXSWtrue\@IEEEeqnarraybox} +\@namedef{endIEEEeqnarrayboxt*}{\end@IEEEeqnarraybox} + +\def\IEEEeqnarraybox{\@IEEEeqnarraystarformfalse\ifmmode\@IEEEeqnarrayboxHBOXSWfalse\else\@IEEEeqnarrayboxHBOXSWtrue\fi% +\@IEEEeqnarraybox} +\def\endIEEEeqnarraybox{\end@IEEEeqnarraybox} + +\@namedef{IEEEeqnarraybox*}{\@IEEEeqnarraystarformtrue\ifmmode\@IEEEeqnarrayboxHBOXSWfalse\else\@IEEEeqnarrayboxHBOXSWtrue\fi% +\@IEEEeqnarraybox} +\@namedef{endIEEEeqnarraybox*}{\end@IEEEeqnarraybox} + +% flag to indicate if the \IEEEeqnarraybox needs to put things into an hbox{$ $} +% for \vcenter in non-math mode +\newif\if@IEEEeqnarrayboxHBOXSW% +\@IEEEeqnarrayboxHBOXSWfalse + +\def\@IEEEeqnarraybox{\relax\@ifnextchar[{\@@IEEEeqnarraybox}{\@@IEEEeqnarraybox[\relax]}} +\def\@@IEEEeqnarraybox[#1]{\relax\@ifnextchar[{\@@@IEEEeqnarraybox[#1]}{\@@@IEEEeqnarraybox[#1][b]}} +\def\@@@IEEEeqnarraybox[#1][#2]{\relax\@ifnextchar[{\@@@@IEEEeqnarraybox[#1][#2]}{\@@@@IEEEeqnarraybox[#1][#2][\relax]}} + +% #1 = decl; #2 = t,b,c; #3 = width, #4 = col specs +\def\@@@@IEEEeqnarraybox[#1][#2][#3]#4{\@IEEEeqnarrayISinnerfalse % not yet within the lines of the halign + \@IEEEeqnarraymasterstrutsave% save current master strut values + \@IEEEeqnarraystrutsize{0pt}{0pt}[\relax]% turn off struts by default + \@IEEEeqnarrayusemasterstruttrue% use master strut till user asks otherwise + \IEEEvisiblestrutsfalse% diagnostic mode defaults to off + % no extra space unless the user specifically requests it + \lineskip=0pt\relax% + \lineskiplimit=0pt\relax% + \baselineskip=\normalbaselineskip\relax% + \jot=\IEEEnormaljot\relax% + \mathsurround\z@\relax% no extra spacing around math + % the default end glues are zero for an \IEEEeqnarraybox + \edef\@IEEEeqnarraycolSEPdefaultstart{\@IEEEeqnarraycolSEPzero}% default start glue + \edef\@IEEEeqnarraycolSEPdefaultend{\@IEEEeqnarraycolSEPzero}% default end glue + \edef\@IEEEeqnarraycolSEPdefaultmid{\@IEEEeqnarraycolSEPzero}% default inter-column glue + \@advanceIEEEeqncolcntfalse% do not advance the col counter for each col the user uses, + % used in \IEEEeqnarraymulticol and in the preamble build + \IEEEeqnarrayboxdecl\relax% allow a way for the user to make global overrides + #1\relax% allow user to override defaults + \let\\\@IEEEeqnarrayboxcr% replace newline with one that allows optional spacing + \@IEEEbuildpreamble #4\end\relax% build the preamble and put it into \@IEEEtrantmptoksA + % add an isolation column to the preamble to stop \\'s {} from getting into the last col + \ifnum\@IEEEeqnnumcols>0\relax\@IEEEappendtoksA{&}\fi% col separator for those after the first + \toks0={##}% + % add the isolation column to the preamble + \@IEEEappendtoksA{\tabskip\z@skip\bgroup\the\toks0\egroup}% + % set the starting tabskip glue as determined by the preamble build + \tabskip=\@IEEEBPstartglue\relax + % begin the alignment + \everycr{}% + % use only the very first token to determine the positioning + % this stops some problems when the user uses more than one letter, + % but is probably not worth the effort + % \noindent is used as a delimiter + \def\@IEEEgrabfirstoken##1##2\noindent{\let\@IEEEgrabbedfirstoken=##1}% + \@IEEEgrabfirstoken#2\relax\relax\noindent + % \@IEEEgrabbedfirstoken has the first token, the rest are discarded + % if we need to put things into and hbox and go into math mode, do so now + \if@IEEEeqnarrayboxHBOXSW \leavevmode \hbox \bgroup $\fi% + % use the appropriate vbox type + \if\@IEEEgrabbedfirstoken t\relax\vtop\else\if\@IEEEgrabbedfirstoken c\relax% + \vcenter\else\vbox\fi\fi\bgroup% + \@IEEEeqnarrayISinnertrue% commands are now within the lines + \ifx#3\relax\halign\else\halign to #3\relax\fi% + \bgroup + % "exspand" the preamble + \span\the\@IEEEtrantmptoksA\cr} + +% carry strut status and enter the isolation/strut column, +% exit from math mode if needed, and exit +\def\end@IEEEeqnarraybox{\@IEEEeqnarrayglobalizestrutstatus% carry strut status +&% enter isolation/strut column +\@IEEEeqnarrayinsertstrut% do strut if needed +\@IEEEeqnarraymasterstrutrestore% restore the previous master strut values +% reset the strut system for next IEEEeqnarray +% (sets local strut values back to previous master strut values) +\@IEEEeqnarraystrutreset% +% ensure last line, exit from halign, close vbox +\crcr\egroup\egroup% +% exit from math mode and close hbox if needed +\if@IEEEeqnarrayboxHBOXSW $\egroup\fi} + + + +% IEEEeqnarraybox uses a modifed \\ instead of the plain \cr to +% end rows. This allows for things like \\[vskip amount] +% This "cr" macros are modified versions those for LaTeX2e's eqnarray +% For IEEEeqnarraybox, \\* is the same as \\ +% the {\ifnum0=`} braces must be kept away from the last column to avoid +% altering spacing of its math, so we use & to advance to the isolation/strut column +% carry strut status into isolation/strut column +\def\@IEEEeqnarrayboxcr{\@IEEEeqnarrayglobalizestrutstatus% carry strut status +&% enter isolation/strut column +\@IEEEeqnarrayinsertstrut% do strut if needed +% reset the strut system for next line or IEEEeqnarray +\@IEEEeqnarraystrutreset% +{\ifnum0=`}\fi% +\@ifstar{\@IEEEeqnarrayboxYCR}{\@IEEEeqnarrayboxYCR}} + +% test and setup the optional argument to \\[] +\def\@IEEEeqnarrayboxYCR{\@testopt\@IEEEeqnarrayboxXCR\z@skip} + +% IEEEeqnarraybox does not automatically increase line spacing by \jot +\def\@IEEEeqnarrayboxXCR[#1]{\ifnum0=`{\fi}% +\cr\noalign{\if@IEEEeqnarraystarform\else\vskip\jot\fi\vskip#1\relax}} + + + +% starts the halign preamble build +\def\@IEEEbuildpreamble{\@IEEEtrantmptoksA={}% clear token register +\let\@IEEEBPcurtype=u%current column type is not yet known +\let\@IEEEBPprevtype=s%the previous column type was the start +\let\@IEEEBPnexttype=u%next column type is not yet known +% ensure these are valid +\def\@IEEEBPcurglue={0pt plus 0pt minus 0pt}% +\def\@IEEEBPcurcolname{@IEEEdefault}% name of current column definition +% currently acquired numerically referenced glue +% use a name that is easier to remember +\let\@IEEEBPcurnum=\@IEEEtrantmpcountA% +\@IEEEBPcurnum=0% +% tracks number of columns in the preamble +\@IEEEeqnnumcols=0% +% record the default end glues +\edef\@IEEEBPstartglue{\@IEEEeqnarraycolSEPdefaultstart}% +\edef\@IEEEBPendglue{\@IEEEeqnarraycolSEPdefaultend}% +% now parse the user's column specifications +\@@IEEEbuildpreamble} + + +% parses and builds the halign preamble +\def\@@IEEEbuildpreamble#1#2{\let\@@nextIEEEbuildpreamble=\@@IEEEbuildpreamble% +% use only the very first token to check the end +% \noindent is used as a delimiter as \end can be present here +\def\@IEEEgrabfirstoken##1##2\noindent{\let\@IEEEgrabbedfirstoken=##1}% +\@IEEEgrabfirstoken#1\relax\relax\noindent +\ifx\@IEEEgrabbedfirstoken\end\let\@@nextIEEEbuildpreamble=\@@IEEEfinishpreamble\else% +% identify current and next token type +\@IEEEgetcoltype{#1}{\@IEEEBPcurtype}{1}% current, error on invalid +\@IEEEgetcoltype{#2}{\@IEEEBPnexttype}{0}% next, no error on invalid next +% if curtype is a glue, get the glue def +\if\@IEEEBPcurtype g\@IEEEgetcurglue{#1}{\@IEEEBPcurglue}\fi% +% if curtype is a column, get the column def and set the current column name +\if\@IEEEBPcurtype c\@IEEEgetcurcol{#1}\fi% +% if curtype is a numeral, acquire the user defined glue +\if\@IEEEBPcurtype n\@IEEEprocessNcol{#1}\fi% +% process the acquired glue +\if\@IEEEBPcurtype g\@IEEEprocessGcol\fi% +% process the acquired col +\if\@IEEEBPcurtype c\@IEEEprocessCcol\fi% +% ready prevtype for next col spec. +\let\@IEEEBPprevtype=\@IEEEBPcurtype% +% be sure and put back the future token(s) as a group +\fi\@@nextIEEEbuildpreamble{#2}} + + +% executed just after preamble build is completed +% warn about zero cols, and if prevtype type = u, put in end tabskip glue +\def\@@IEEEfinishpreamble#1{\ifnum\@IEEEeqnnumcols<1\relax +\@IEEEclspkgerror{No column specifiers declared for IEEEeqnarray}% +{At least one column type must be declared for each IEEEeqnarray.}% +\fi%num cols less than 1 +%if last type undefined, set default end tabskip glue +\if\@IEEEBPprevtype u\@IEEEappendtoksA{\tabskip=\@IEEEBPendglue}\fi} + + +% Identify and return the column specifier's type code +\def\@IEEEgetcoltype#1#2#3{% +% use only the very first token to determine the type +% \noindent is used as a delimiter as \end can be present here +\def\@IEEEgrabfirstoken##1##2\noindent{\let\@IEEEgrabbedfirstoken=##1}% +\@IEEEgrabfirstoken#1\relax\relax\noindent +% \@IEEEgrabfirstoken has the first token, the rest are discarded +% n = number +% g = glue (any other char in catagory 12) +% c = letter +% e = \end +% u = undefined +% third argument: 0 = no error message, 1 = error on invalid char +\let#2=u\relax% assume invalid until know otherwise +\ifx\@IEEEgrabbedfirstoken\end\let#2=e\else +\ifcat\@IEEEgrabbedfirstoken\relax\else% screen out control sequences +\if0\@IEEEgrabbedfirstoken\let#2=n\else +\if1\@IEEEgrabbedfirstoken\let#2=n\else +\if2\@IEEEgrabbedfirstoken\let#2=n\else +\if3\@IEEEgrabbedfirstoken\let#2=n\else +\if4\@IEEEgrabbedfirstoken\let#2=n\else +\if5\@IEEEgrabbedfirstoken\let#2=n\else +\if6\@IEEEgrabbedfirstoken\let#2=n\else +\if7\@IEEEgrabbedfirstoken\let#2=n\else +\if8\@IEEEgrabbedfirstoken\let#2=n\else +\if9\@IEEEgrabbedfirstoken\let#2=n\else +\ifcat,\@IEEEgrabbedfirstoken\let#2=g\relax +\else\ifcat a\@IEEEgrabbedfirstoken\let#2=c\relax\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi +\if#2u\relax +\if0\noexpand#3\relax\else\@IEEEclspkgerror{Invalid character in column specifications}% +{Only letters, numerals and certain other symbols are allowed \MessageBreak +as IEEEeqnarray column specifiers.}\fi\fi} + + +% identify the current letter referenced column +% if invalid, use a default column +\def\@IEEEgetcurcol#1{\expandafter\ifx\csname @IEEEeqnarraycolDEF#1\endcsname\@IEEEeqnarraycolisdefined% +\def\@IEEEBPcurcolname{#1}\else% invalid column name +\@IEEEclspkgerror{Invalid column type "#1" in column specifications.\MessageBreak +Using a default centering column instead}% +{You must define IEEEeqnarray column types before use.}% +\def\@IEEEBPcurcolname{@IEEEdefault}\fi} + + +% identify and return the predefined (punctuation) glue value +\def\@IEEEgetcurglue#1#2{% +% ! = \! (neg small) -0.16667em (-3/18 em) +% , = \, (small) 0.16667em ( 3/18 em) +% : = \: (med) 0.22222em ( 4/18 em) +% ; = \; (large) 0.27778em ( 5/18 em) +% ' = \quad 1em +% " = \qquad 2em +% . = 0.5\arraycolsep +% / = \arraycolsep +% ? = 2\arraycolsep +% * = 1fil +% + = \@IEEEeqnarraycolSEPcenter +% - = \@IEEEeqnarraycolSEPzero +% Note that all em values are referenced to the math font (textfont2) fontdimen6 +% value for 1em. +% +% use only the very first token to determine the type +% this prevents errant tokens from getting in the main text +% \noindent is used as a delimiter here +\def\@IEEEgrabfirstoken##1##2\noindent{\let\@IEEEgrabbedfirstoken=##1}% +\@IEEEgrabfirstoken#1\relax\relax\noindent +% get the math font 1em value +% LaTeX2e's NFSS2 does not preload the fonts, but \IEEEeqnarray needs +% to gain access to the math (\textfont2) font's spacing parameters. +% So we create a bogus box here that uses the math font to ensure +% that \textfont2 is loaded and ready. If this is not done, +% the \textfont2 stuff here may not work. +% Thanks to Bernd Raichle for his 1997 post on this topic. +{\setbox0=\hbox{$\displaystyle\relax$}}% +% fontdimen6 has the width of 1em (a quad). +\@IEEEtrantmpdimenA=\fontdimen6\textfont2\relax% +% identify the glue value based on the first token +% we discard anything after the first +\if!\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=-0.16667\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if,\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=0.16667\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if:\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=0.22222\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if;\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=0.27778\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if'\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=1\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if"\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=2\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if.\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=0.5\arraycolsep\edef#2{\the\@IEEEtrantmpdimenA}\else +\if/\@IEEEgrabbedfirstoken\edef#2{\the\arraycolsep}\else +\if?\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=2\arraycolsep\edef#2{\the\@IEEEtrantmpdimenA}\else +\if *\@IEEEgrabbedfirstoken\edef#2{0pt plus 1fil minus 0pt}\else +\if+\@IEEEgrabbedfirstoken\edef#2{\@IEEEeqnarraycolSEPcenter}\else +\if-\@IEEEgrabbedfirstoken\edef#2{\@IEEEeqnarraycolSEPzero}\else +\edef#2{\@IEEEeqnarraycolSEPzero}% +\@IEEEclspkgerror{Invalid predefined inter-column glue type "#1" in\MessageBreak +column specifications. Using a default value of\MessageBreak +0pt instead}% +{Only !,:;'"./?*+ and - are valid predefined glue types in the\MessageBreak +IEEEeqnarray column specifications.}\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi} + + + +% process a numerical digit from the column specification +% and look up the corresponding user defined glue value +% can transform current type from n to g or a as the user defined glue is acquired +\def\@IEEEprocessNcol#1{\if\@IEEEBPprevtype g% +\@IEEEclspkgerror{Back-to-back inter-column glue specifiers in column\MessageBreak +specifications. Ignoring consecutive glue specifiers\MessageBreak +after the first}% +{You cannot have two or more glue types next to each other\MessageBreak +in the IEEEeqnarray column specifications.}% +\let\@IEEEBPcurtype=a% abort this glue, future digits will be discarded +\@IEEEBPcurnum=0\relax% +\else% if we previously aborted a glue +\if\@IEEEBPprevtype a\@IEEEBPcurnum=0\let\@IEEEBPcurtype=a%maintain digit abortion +\else%acquire this number +% save the previous type before the numerical digits started +\if\@IEEEBPprevtype n\else\let\@IEEEBPprevsavedtype=\@IEEEBPprevtype\fi% +\multiply\@IEEEBPcurnum by 10\relax% +\advance\@IEEEBPcurnum by #1\relax% add in number, \relax is needed to stop TeX's number scan +\if\@IEEEBPnexttype n\else%close acquisition +\expandafter\ifx\csname @IEEEeqnarraycolSEPDEF\expandafter\romannumeral\number\@IEEEBPcurnum\endcsname\@IEEEeqnarraycolisdefined% +\edef\@IEEEBPcurglue{\csname @IEEEeqnarraycolSEP\expandafter\romannumeral\number\@IEEEBPcurnum\endcsname}% +\else%user glue not defined +\@IEEEclspkgerror{Invalid user defined inter-column glue type "\number\@IEEEBPcurnum" in\MessageBreak +column specifications. Using a default value of\MessageBreak +0pt instead}% +{You must define all IEEEeqnarray numerical inter-column glue types via\MessageBreak +\string\IEEEeqnarraydefcolsep \space before they are used in column specifications.}% +\edef\@IEEEBPcurglue{\@IEEEeqnarraycolSEPzero}% +\fi% glue defined or not +\let\@IEEEBPcurtype=g% change the type to reflect the acquired glue +\let\@IEEEBPprevtype=\@IEEEBPprevsavedtype% restore the prev type before this number glue +\@IEEEBPcurnum=0\relax%ready for next acquisition +\fi%close acquisition, get glue +\fi%discard or acquire number +\fi%prevtype glue or not +} + + +% process an acquired glue +% add any acquired column/glue pair to the preamble +\def\@IEEEprocessGcol{\if\@IEEEBPprevtype a\let\@IEEEBPcurtype=a%maintain previous glue abortions +\else +% if this is the start glue, save it, but do nothing else +% as this is not used in the preamble, but before +\if\@IEEEBPprevtype s\edef\@IEEEBPstartglue{\@IEEEBPcurglue}% +\else%not the start glue +\if\@IEEEBPprevtype g%ignore if back to back glues +\@IEEEclspkgerror{Back-to-back inter-column glue specifiers in column\MessageBreak +specifications. Ignoring consecutive glue specifiers\MessageBreak +after the first}% +{You cannot have two or more glue types next to each other\MessageBreak +in the IEEEeqnarray column specifications.}% +\let\@IEEEBPcurtype=a% abort this glue +\else% not a back to back glue +\if\@IEEEBPprevtype c\relax% if the previoustype was a col, add column/glue pair to preamble +\ifnum\@IEEEeqnnumcols>0\relax\@IEEEappendtoksA{&}\fi +\toks0={##}% +% make preamble advance col counter if this environment needs this +\if@advanceIEEEeqncolcnt\@IEEEappendtoksA{\global\advance\@IEEEeqncolcnt by 1\relax}\fi +% insert the column defintion into the preamble, being careful not to expand +% the column definition +\@IEEEappendtoksA{\tabskip=\@IEEEBPcurglue}% +\@IEEEappendNOEXPANDtoksA{\begingroup\csname @IEEEeqnarraycolPRE}% +\@IEEEappendtoksA{\@IEEEBPcurcolname}% +\@IEEEappendNOEXPANDtoksA{\endcsname}% +\@IEEEappendtoksA{\the\toks0}% +\@IEEEappendNOEXPANDtoksA{\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\csname @IEEEeqnarraycolPOST}% +\@IEEEappendtoksA{\@IEEEBPcurcolname}% +\@IEEEappendNOEXPANDtoksA{\endcsname\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\endgroup}% +\advance\@IEEEeqnnumcols by 1\relax%one more column in the preamble +\else% error: non-start glue with no pending column +\@IEEEclspkgerror{Inter-column glue specifier without a prior column\MessageBreak +type in the column specifications. Ignoring this glue\MessageBreak +specifier}% +{Except for the first and last positions, glue can be placed only\MessageBreak +between column types.}% +\let\@IEEEBPcurtype=a% abort this glue +\fi% previous was a column +\fi% back-to-back glues +\fi% is start column glue +\fi% prev type not a +} + + +% process an acquired letter referenced column and, if necessary, add it to the preamble +\def\@IEEEprocessCcol{\if\@IEEEBPnexttype g\else +\if\@IEEEBPnexttype n\else +% we have a column followed by something other than a glue (or numeral glue) +% so we must add this column to the preamble now +\ifnum\@IEEEeqnnumcols>0\relax\@IEEEappendtoksA{&}\fi%col separator for those after the first +\if\@IEEEBPnexttype e\@IEEEappendtoksA{\tabskip=\@IEEEBPendglue\relax}\else%put in end glue +\@IEEEappendtoksA{\tabskip=\@IEEEeqnarraycolSEPdefaultmid\relax}\fi% or default mid glue +\toks0={##}% +% make preamble advance col counter if this environment needs this +\if@advanceIEEEeqncolcnt\@IEEEappendtoksA{\global\advance\@IEEEeqncolcnt by 1\relax}\fi +% insert the column definition into the preamble, being careful not to expand +% the column definition +\@IEEEappendNOEXPANDtoksA{\begingroup\csname @IEEEeqnarraycolPRE}% +\@IEEEappendtoksA{\@IEEEBPcurcolname}% +\@IEEEappendNOEXPANDtoksA{\endcsname}% +\@IEEEappendtoksA{\the\toks0}% +\@IEEEappendNOEXPANDtoksA{\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\csname @IEEEeqnarraycolPOST}% +\@IEEEappendtoksA{\@IEEEBPcurcolname}% +\@IEEEappendNOEXPANDtoksA{\endcsname\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\endgroup}% +\advance\@IEEEeqnnumcols by 1\relax%one more column in the preamble +\fi%next type not numeral +\fi%next type not glue +} + + +%% +%% END OF IEEEeqnarry DEFINITIONS +%% + + + + +% set up the running headings, this complex because of all the different +% modes IEEEtran supports +\if@twoside + \ifCLASSOPTIONtechnote + \def\ps@headings{% + \def\@oddhead{\hbox{}\scriptsize\leftmark \hfil \thepage} + \def\@evenhead{\scriptsize\thepage \hfil \leftmark\hbox{}} + \ifCLASSOPTIONdraftcls + \ifCLASSOPTIONdraftclsnofoot + \def\@oddfoot{}\def\@evenfoot{}% + \else + \def\@oddfoot{\scriptsize\@date\hfil DRAFT} + \def\@evenfoot{\scriptsize DRAFT\hfil\@date} + \fi + \else + \def\@oddfoot{}\def\@evenfoot{} + \fi} + \else % not a technote + \def\ps@headings{% + \ifCLASSOPTIONconference + \def\@oddhead{} + \def\@evenhead{} + \else + \def\@oddhead{\hbox{}\scriptsize\rightmark \hfil \thepage} + \def\@evenhead{\scriptsize\thepage \hfil \leftmark\hbox{}} + \fi + \ifCLASSOPTIONdraftcls + \def\@oddhead{\hbox{}\scriptsize\rightmark \hfil \thepage} + \def\@evenhead{\scriptsize\thepage \hfil \leftmark\hbox{}} + \ifCLASSOPTIONdraftclsnofoot + \def\@oddfoot{}\def\@evenfoot{}% + \else + \def\@oddfoot{\scriptsize\@date\hfil DRAFT} + \def\@evenfoot{\scriptsize DRAFT\hfil\@date} + \fi + \else + \def\@oddfoot{}\def\@evenfoot{}% + \fi} + \fi +\else % single side +\def\ps@headings{% + \ifCLASSOPTIONconference + \def\@oddhead{} + \def\@evenhead{} + \else + \def\@oddhead{\hbox{}\scriptsize\leftmark \hfil \thepage} + \def\@evenhead{} + \fi + \ifCLASSOPTIONdraftcls + \def\@oddhead{\hbox{}\scriptsize\leftmark \hfil \thepage} + \def\@evenhead{} + \ifCLASSOPTIONdraftclsnofoot + \def\@oddfoot{} + \else + \def\@oddfoot{\scriptsize \@date \hfil DRAFT} + \fi + \else + \def\@oddfoot{} + \fi + \def\@evenfoot{}} +\fi + + +% title page style +\def\ps@IEEEtitlepagestyle{\def\@oddfoot{}\def\@evenfoot{}% +\ifCLASSOPTIONconference + \def\@oddhead{}% + \def\@evenhead{}% +\else + \def\@oddhead{\hbox{}\scriptsize\leftmark \hfil \thepage}% + \def\@evenhead{\scriptsize\thepage \hfil \leftmark\hbox{}}% +\fi +\ifCLASSOPTIONdraftcls + \def\@oddhead{\hbox{}\scriptsize\leftmark \hfil \thepage}% + \def\@evenhead{\scriptsize\thepage \hfil \leftmark\hbox{}}% + \ifCLASSOPTIONdraftclsnofoot\else + \def\@oddfoot{\scriptsize \@date\hfil DRAFT}% + \def\@evenfoot{\scriptsize DRAFT\hfil \@date}% + \fi +\else + % all non-draft mode footers + \if@IEEEusingpubid + % for title pages that are using a pubid + % do not repeat pubid if using peer review option + \ifCLASSOPTIONpeerreview + \else + \footskip 0pt% + \ifCLASSOPTIONcompsoc + \def\@oddfoot{\hss\normalfont\scriptsize\raisebox{-1.5\@IEEEnormalsizeunitybaselineskip}[0ex][0ex]{\@IEEEpubid}\hss}% + \def\@evenfoot{\hss\normalfont\scriptsize\raisebox{-1.5\@IEEEnormalsizeunitybaselineskip}[0ex][0ex]{\@IEEEpubid}\hss}% + \else + \def\@oddfoot{\hss\normalfont\footnotesize\raisebox{1.5ex}[1.5ex]{\@IEEEpubid}\hss}% + \def\@evenfoot{\hss\normalfont\footnotesize\raisebox{1.5ex}[1.5ex]{\@IEEEpubid}\hss}% + \fi + \fi + \fi +\fi} + + +% peer review cover page style +\def\ps@IEEEpeerreviewcoverpagestyle{% +\def\@oddhead{}\def\@evenhead{}% +\def\@oddfoot{}\def\@evenfoot{}% +\ifCLASSOPTIONdraftcls + \ifCLASSOPTIONdraftclsnofoot\else + \def\@oddfoot{\scriptsize \@date\hfil DRAFT}% + \def\@evenfoot{\scriptsize DRAFT\hfil \@date}% + \fi +\else + % non-draft mode footers + \if@IEEEusingpubid + \footskip 0pt% + \ifCLASSOPTIONcompsoc + \def\@oddfoot{\hss\normalfont\scriptsize\raisebox{-1.5\@IEEEnormalsizeunitybaselineskip}[0ex][0ex]{\@IEEEpubid}\hss}% + \def\@evenfoot{\hss\normalfont\scriptsize\raisebox{-1.5\@IEEEnormalsizeunitybaselineskip}[0ex][0ex]{\@IEEEpubid}\hss}% + \else + \def\@oddfoot{\hss\normalfont\footnotesize\raisebox{1.5ex}[1.5ex]{\@IEEEpubid}\hss}% + \def\@evenfoot{\hss\normalfont\footnotesize\raisebox{1.5ex}[1.5ex]{\@IEEEpubid}\hss}% + \fi + \fi +\fi} + + +% start with empty headings +\def\rightmark{}\def\leftmark{} + + +%% Defines the command for putting the header. \footernote{TEXT} is the same +%% as \markboth{TEXT}{TEXT}. +%% Note that all the text is forced into uppercase, if you have some text +%% that needs to be in lower case, for instance et. al., then either manually +%% set \leftmark and \rightmark or use \MakeLowercase{et. al.} within the +%% arguments to \markboth. +\def\markboth#1#2{\def\leftmark{\@IEEEcompsoconly{\sffamily}\MakeUppercase{#1}}% +\def\rightmark{\@IEEEcompsoconly{\sffamily}\MakeUppercase{#2}}} +\def\footernote#1{\markboth{#1}{#1}} + +\def\today{\ifcase\month\or + January\or February\or March\or April\or May\or June\or + July\or August\or September\or October\or November\or December\fi + \space\number\day, \number\year} + + + + +%% CITATION AND BIBLIOGRAPHY COMMANDS +%% +%% V1.6 no longer supports the older, nonstandard \shortcite and \citename setup stuff +% +% +% Modify Latex2e \@citex to separate citations with "], [" +\def\@citex[#1]#2{% + \let\@citea\@empty + \@cite{\@for\@citeb:=#2\do + {\@citea\def\@citea{], [}% + \edef\@citeb{\expandafter\@firstofone\@citeb\@empty}% + \if@filesw\immediate\write\@auxout{\string\citation{\@citeb}}\fi + \@ifundefined{b@\@citeb}{\mbox{\reset@font\bfseries ?}% + \G@refundefinedtrue + \@latex@warning + {Citation `\@citeb' on page \thepage \space undefined}}% + {\hbox{\csname b@\@citeb\endcsname}}}}{#1}} + +% V1.6 we create hooks for the optional use of Donald Arseneau's +% cite.sty package. cite.sty is "smart" and will notice that the +% following format controls are already defined and will not +% redefine them. The result will be the proper sorting of the +% citation numbers and auto detection of 3 or more entry "ranges" - +% all in IEEE style: [1], [2], [5]--[7], [12] +% This also allows for an optional note, i.e., \cite[mynote]{..}. +% If the \cite with note has more than one reference, the note will +% be applied to the last of the listed references. It is generally +% desired that if a note is given, only one reference is listed in +% that \cite. +% Thanks to Mr. Arseneau for providing the required format arguments +% to produce the IEEE style. +\def\citepunct{], [} +\def\citedash{]--[} + +% V1.7 default to using same font for urls made by url.sty +\AtBeginDocument{\csname url@samestyle\endcsname} + +% V1.6 class files should always provide these +\def\newblock{\hskip .11em\@plus.33em\@minus.07em} +\let\@openbib@code\@empty + + +% Provide support for the control entries of IEEEtran.bst V1.00 and later. +% V1.7 optional argument allows for a different aux file to be specified in +% order to handle multiple bibliographies. For example, with multibib.sty: +% \newcites{sec}{Secondary Literature} +% \bstctlcite[@auxoutsec]{BSTcontrolhak} +\def\bstctlcite{\@ifnextchar[{\@bstctlcite}{\@bstctlcite[@auxout]}} +\def\@bstctlcite[#1]#2{\@bsphack + \@for\@citeb:=#2\do{% + \edef\@citeb{\expandafter\@firstofone\@citeb}% + \if@filesw\immediate\write\csname #1\endcsname{\string\citation{\@citeb}}\fi}% + \@esphack} + +% V1.6 provide a way for a user to execute a command just before +% a given reference number - used to insert a \newpage to balance +% the columns on the last page +\edef\@IEEEtriggerrefnum{0} % the default of zero means that + % the command is not executed +\def\@IEEEtriggercmd{\newpage} + +% allow the user to alter the triggered command +\long\def\IEEEtriggercmd#1{\long\def\@IEEEtriggercmd{#1}} + +% allow user a way to specify the reference number just before the +% command is executed +\def\IEEEtriggeratref#1{\@IEEEtrantmpcountA=#1% +\edef\@IEEEtriggerrefnum{\the\@IEEEtrantmpcountA}}% + +% trigger command at the given reference +\def\@IEEEbibitemprefix{\@IEEEtrantmpcountA=\@IEEEtriggerrefnum\relax% +\advance\@IEEEtrantmpcountA by -1\relax% +\ifnum\c@enumiv=\@IEEEtrantmpcountA\relax\@IEEEtriggercmd\relax\fi} + + +\def\@biblabel#1{[#1]} + +% compsoc journals left align the reference numbers +\@IEEEcompsocnotconfonly{\def\@biblabel#1{[#1]\hfill}} + +% controls bib item spacing +\def\IEEEbibitemsep{2.5pt plus .5pt} + +\@IEEEcompsocconfonly{\def\IEEEbibitemsep{1\baselineskip plus 0.25\baselineskip minus 0.25\baselineskip}} + + +\def\thebibliography#1{\section*{\refname}% + \addcontentsline{toc}{section}{\refname}% + % V1.6 add some rubber space here and provide a command trigger + \footnotesize\@IEEEcompsocconfonly{\small}\vskip 0.3\baselineskip plus 0.1\baselineskip minus 0.1\baselineskip% + \list{\@biblabel{\@arabic\c@enumiv}}% + {\settowidth\labelwidth{\@biblabel{#1}}% + \leftmargin\labelwidth + \labelsep 1em + \advance\leftmargin\labelsep\relax + \itemsep \IEEEbibitemsep\relax + \usecounter{enumiv}% + \let\p@enumiv\@empty + \renewcommand\theenumiv{\@arabic\c@enumiv}}% + \let\@IEEElatexbibitem\bibitem% + \def\bibitem{\@IEEEbibitemprefix\@IEEElatexbibitem}% +\def\newblock{\hskip .11em plus .33em minus .07em}% +% originally: +% \sloppy\clubpenalty4000\widowpenalty4000% +% by adding the \interlinepenalty here, we make it more +% difficult, but not impossible, for LaTeX to break within a reference. +% IEEE almost never breaks a reference (but they do it more often with +% technotes). You may get an underfull vbox warning around the bibliography, +% but the final result will be much more like what IEEE will publish. +% MDS 11/2000 +\ifCLASSOPTIONtechnote\sloppy\clubpenalty4000\widowpenalty4000\interlinepenalty100% +\else\sloppy\clubpenalty4000\widowpenalty4000\interlinepenalty500\fi% + \sfcode`\.=1000\relax} +\let\endthebibliography=\endlist + + + + +% TITLE PAGE COMMANDS +% +% +% \IEEEmembership is used to produce the sublargesize italic font used to indicate author +% IEEE membership. compsoc uses a large size sans slant font +\def\IEEEmembership#1{{\@IEEEnotcompsoconly{\sublargesize}\normalfont\@IEEEcompsoconly{\sffamily}\textit{#1}}} + + +% \IEEEauthorrefmark{} produces a footnote type symbol to indicate author affiliation. +% When given an argument of 1 to 9, \IEEEauthorrefmark{} follows the standard LaTeX footnote +% symbol sequence convention. However, for arguments 10 and above, \IEEEauthorrefmark{} +% reverts to using lower case roman numerals, so it cannot overflow. Do note that you +% cannot use \footnotemark[] in place of \IEEEauthorrefmark{} within \author as the footnote +% symbols will have been turned off to prevent \thanks from creating footnote marks. +% \IEEEauthorrefmark{} produces a symbol that appears to LaTeX as having zero vertical +% height - this allows for a more compact line packing, but the user must ensure that +% the interline spacing is large enough to prevent \IEEEauthorrefmark{} from colliding +% with the text above. +% V1.7 make this a robust command +\DeclareRobustCommand*{\IEEEauthorrefmark}[1]{\raisebox{0pt}[0pt][0pt]{\textsuperscript{\footnotesize\ensuremath{\ifcase#1\or *\or \dagger\or \ddagger\or% + \mathsection\or \mathparagraph\or \|\or **\or \dagger\dagger% + \or \ddagger\ddagger \else\textsuperscript{\expandafter\romannumeral#1}\fi}}}} + + +% FONT CONTROLS AND SPACINGS FOR CONFERENCE MODE AUTHOR NAME AND AFFILIATION BLOCKS +% +% The default font styles for the author name and affiliation blocks (confmode) +\def\@IEEEauthorblockNstyle{\normalfont\@IEEEcompsocnotconfonly{\sffamily}\sublargesize\@IEEEcompsocconfonly{\large}} +\def\@IEEEauthorblockAstyle{\normalfont\@IEEEcompsocnotconfonly{\sffamily}\@IEEEcompsocconfonly{\itshape}\normalsize\@IEEEcompsocconfonly{\large}} +% The default if the user does not use an author block +\def\@IEEEauthordefaulttextstyle{\normalfont\@IEEEcompsocnotconfonly{\sffamily}\sublargesize} + +% spacing from title (or special paper notice) to author name blocks (confmode) +% can be negative +\def\@IEEEauthorblockconfadjspace{-0.25em} +% compsoc conferences need more space here +\@IEEEcompsocconfonly{\def\@IEEEauthorblockconfadjspace{0.75\@IEEEnormalsizeunitybaselineskip}} +\ifCLASSOPTIONconference\def\@IEEEauthorblockconfadjspace{20pt}\fi + +% spacing between name and affiliation blocks (confmode) +% This can be negative. +% IEEE doesn't want any added spacing here, but I will leave these +% controls in place in case they ever change their mind. +% Personally, I like 0.75ex. +%\def\@IEEEauthorblockNtopspace{0.75ex} +%\def\@IEEEauthorblockAtopspace{0.75ex} +\def\@IEEEauthorblockNtopspace{0.0ex} +\def\@IEEEauthorblockAtopspace{0.0ex} +% baseline spacing within name and affiliation blocks (confmode) +% must be positive, spacings below certain values will make +% the position of line of text sensitive to the contents of the +% line above it i.e., whether or not the prior line has descenders, +% subscripts, etc. For this reason it is a good idea to keep +% these above 2.6ex +\def\@IEEEauthorblockNinterlinespace{2.6ex} +\def\@IEEEauthorblockAinterlinespace{2.75ex} + +% This tracks the required strut size. +% See the \@IEEEauthorhalign command for the actual default value used. +\def\@IEEEauthorblockXinterlinespace{2.7ex} + +% variables to retain font size and style across groups +% values given here have no effect as they will be overwritten later +\gdef\@IEEESAVESTATEfontsize{10} +\gdef\@IEEESAVESTATEfontbaselineskip{12} +\gdef\@IEEESAVESTATEfontencoding{OT1} +\gdef\@IEEESAVESTATEfontfamily{ptm} +\gdef\@IEEESAVESTATEfontseries{m} +\gdef\@IEEESAVESTATEfontshape{n} + +% saves the current font attributes +\def\@IEEEcurfontSAVE{\global\let\@IEEESAVESTATEfontsize\f@size% +\global\let\@IEEESAVESTATEfontbaselineskip\f@baselineskip% +\global\let\@IEEESAVESTATEfontencoding\f@encoding% +\global\let\@IEEESAVESTATEfontfamily\f@family% +\global\let\@IEEESAVESTATEfontseries\f@series% +\global\let\@IEEESAVESTATEfontshape\f@shape} + +% restores the saved font attributes +\def\@IEEEcurfontRESTORE{\fontsize{\@IEEESAVESTATEfontsize}{\@IEEESAVESTATEfontbaselineskip}% +\fontencoding{\@IEEESAVESTATEfontencoding}% +\fontfamily{\@IEEESAVESTATEfontfamily}% +\fontseries{\@IEEESAVESTATEfontseries}% +\fontshape{\@IEEESAVESTATEfontshape}% +\selectfont} + + +% variable to indicate if the current block is the first block in the column +\newif\if@IEEEprevauthorblockincol \@IEEEprevauthorblockincolfalse + + +% the command places a strut with height and depth = \@IEEEauthorblockXinterlinespace +% we use this technique to have complete manual control over the spacing of the lines +% within the halign environment. +% We set the below baseline portion at 30%, the above +% baseline portion at 70% of the total length. +% Responds to changes in the document's \baselinestretch +\def\@IEEEauthorstrutrule{\@IEEEtrantmpdimenA\@IEEEauthorblockXinterlinespace% +\@IEEEtrantmpdimenA=\baselinestretch\@IEEEtrantmpdimenA% +\rule[-0.3\@IEEEtrantmpdimenA]{0pt}{\@IEEEtrantmpdimenA}} + + +% blocks to hold the authors' names and affilations. +% Makes formatting easy for conferences +% +% use real definitions in conference mode +% name block +\def\IEEEauthorblockN#1{\relax\@IEEEauthorblockNstyle% set the default text style +\gdef\@IEEEauthorblockXinterlinespace{0pt}% disable strut for spacer row +% the \expandafter hides the \cr in conditional tex, see the array.sty docs +% for details, probably not needed here as the \cr is in a macro +% do a spacer row if needed +\if@IEEEprevauthorblockincol\expandafter\@IEEEauthorblockNtopspaceline\fi +\global\@IEEEprevauthorblockincoltrue% we now have a block in this column +%restore the correct strut value +\gdef\@IEEEauthorblockXinterlinespace{\@IEEEauthorblockNinterlinespace}% +% input the author names +#1% +% end the row if the user did not already +\crcr} +% spacer row for names +\def\@IEEEauthorblockNtopspaceline{\cr\noalign{\vskip\@IEEEauthorblockNtopspace}} +% +% affiliation block +\def\IEEEauthorblockA#1{\relax\@IEEEauthorblockAstyle% set the default text style +\gdef\@IEEEauthorblockXinterlinespace{0pt}%disable strut for spacer row +% the \expandafter hides the \cr in conditional tex, see the array.sty docs +% for details, probably not needed here as the \cr is in a macro +% do a spacer row if needed +\if@IEEEprevauthorblockincol\expandafter\@IEEEauthorblockAtopspaceline\fi +\global\@IEEEprevauthorblockincoltrue% we now have a block in this column +%restore the correct strut value +\gdef\@IEEEauthorblockXinterlinespace{\@IEEEauthorblockAinterlinespace}% +% input the author affiliations +#1% +% end the row if the user did not already +\crcr} +% spacer row for affiliations +\def\@IEEEauthorblockAtopspaceline{\cr\noalign{\vskip\@IEEEauthorblockAtopspace}} + + +% allow papers to compile even if author blocks are used in modes other +% than conference or peerreviewca. For such cases, we provide dummy blocks. +\ifCLASSOPTIONconference +\else + \ifCLASSOPTIONpeerreviewca\else + % not conference or peerreviewca mode + \def\IEEEauthorblockN#1{#1}% + \def\IEEEauthorblockA#1{#1}% + \fi +\fi + + + +% we provide our own halign so as not to have to depend on tabular +\def\@IEEEauthorhalign{\@IEEEauthordefaulttextstyle% default text style + \lineskip=0pt\relax% disable line spacing + \lineskiplimit=0pt\relax% + \baselineskip=0pt\relax% + \@IEEEcurfontSAVE% save the current font + \mathsurround\z@\relax% no extra spacing around math + \let\\\@IEEEauthorhaligncr% replace newline with halign friendly one + \tabskip=0pt\relax% no column spacing + \everycr{}% ensure no problems here + \@IEEEprevauthorblockincolfalse% no author blocks yet + \def\@IEEEauthorblockXinterlinespace{2.7ex}% default interline space + \vtop\bgroup%vtop box + \halign\bgroup&\relax\hfil\@IEEEcurfontRESTORE\relax ##\relax + \hfil\@IEEEcurfontSAVE\@IEEEauthorstrutrule\cr} + +% ensure last line, exit from halign, close vbox +\def\end@IEEEauthorhalign{\crcr\egroup\egroup} + +% handle bogus star form +\def\@IEEEauthorhaligncr{{\ifnum0=`}\fi\@ifstar{\@@IEEEauthorhaligncr}{\@@IEEEauthorhaligncr}} + +% test and setup the optional argument to \\[] +\def\@@IEEEauthorhaligncr{\@testopt\@@@IEEEauthorhaligncr\z@skip} + +% end the line and do the optional spacer +\def\@@@IEEEauthorhaligncr[#1]{\ifnum0=`{\fi}\cr\noalign{\vskip#1\relax}} + + + +% flag to prevent multiple \and warning messages +\newif\if@IEEEWARNand +\@IEEEWARNandtrue + +% if in conference or peerreviewca modes, we support the use of \and as \author is a +% tabular environment, otherwise we warn the user that \and is invalid +% outside of conference or peerreviewca modes. +\def\and{\relax} % provide a bogus \and that we will then override + +\renewcommand{\and}[1][\relax]{\if@IEEEWARNand\typeout{** WARNING: \noexpand\and is valid only + when in conference or peerreviewca}\typeout{modes (line \the\inputlineno).}\fi\global\@IEEEWARNandfalse} + +\ifCLASSOPTIONconference% +\renewcommand{\and}[1][\hfill]{\end{@IEEEauthorhalign}#1\begin{@IEEEauthorhalign}}% +\fi +\ifCLASSOPTIONpeerreviewca +\renewcommand{\and}[1][\hfill]{\end{@IEEEauthorhalign}#1\begin{@IEEEauthorhalign}}% +\fi + + +% page clearing command +% based on LaTeX2e's \cleardoublepage, but allows different page styles +% for the inserted blank pages +\def\@IEEEcleardoublepage#1{\clearpage\if@twoside\ifodd\c@page\else +\hbox{}\thispagestyle{#1}\newpage\if@twocolumn\hbox{}\thispagestyle{#1}\newpage\fi\fi\fi} + + +% user command to invoke the title page +\def\maketitle{\par% + \begingroup% + \normalfont% + \def\thefootnote{}% the \thanks{} mark type is empty + \def\footnotemark{}% and kill space from \thanks within author + \let\@makefnmark\relax% V1.7, must *really* kill footnotemark to remove all \textsuperscript spacing as well. + \footnotesize% equal spacing between thanks lines + \footnotesep 0.7\baselineskip%see global setting of \footnotesep for more info + % V1.7 disable \thanks note indention for compsoc + \@IEEEcompsoconly{\long\def\@makefntext##1{\parindent 1em\noindent\hbox{\@makefnmark}##1}}% + \normalsize% + \ifCLASSOPTIONpeerreview + \newpage\global\@topnum\z@ \@maketitle\@IEEEstatictitlevskip\@IEEEaftertitletext% + \thispagestyle{IEEEpeerreviewcoverpagestyle}\@thanks% + \else + \if@twocolumn% + \ifCLASSOPTIONtechnote% + \newpage\global\@topnum\z@ \@maketitle\@IEEEstatictitlevskip\@IEEEaftertitletext% + \else + \twocolumn[\@maketitle\@IEEEstatictitlevskip\@IEEEaftertitletext]% + \fi + \else + \newpage\global\@topnum\z@ \@maketitle\@IEEEstatictitlevskip\@IEEEaftertitletext% + \fi + \thispagestyle{IEEEtitlepagestyle}\@thanks% + \fi + % pullup page for pubid if used. + \if@IEEEusingpubid + \enlargethispage{-\@IEEEpubidpullup}% + \fi + \endgroup + \setcounter{footnote}{0}\let\maketitle\relax\let\@maketitle\relax + \gdef\@thanks{}% + % v1.6b do not clear these as we will need the title again for peer review papers + % \gdef\@author{}\gdef\@title{}% + \let\thanks\relax} + + + +% V1.7 parbox to format \@IEEEcompsoctitleabstractindextext +\long\def\@IEEEcompsoctitleabstractindextextbox#1{\parbox{0.915\textwidth}{#1}} + +% formats the Title, authors names, affiliations and special paper notice +% THIS IS A CONTROLLED SPACING COMMAND! Do not allow blank lines or unintentional +% spaces to enter the definition - use % at the end of each line +\def\@maketitle{\newpage +\begingroup\centering +\ifCLASSOPTIONtechnote% technotes + {\bfseries\large\@IEEEcompsoconly{\sffamily}\@title\par}\vskip 1.3em{\lineskip .5em\@IEEEcompsoconly{\sffamily}\@author + \@IEEEspecialpapernotice\par{\@IEEEcompsoconly{\vskip 1.5em\relax + \@IEEEcompsoctitleabstractindextextbox{\@IEEEcompsoctitleabstractindextext}\par + \hfill\@IEEEcompsocdiamondline\hfill\hbox{}\par}}}\relax +\else% not a technote + \vskip0.2em{\Huge\@IEEEcompsoconly{\sffamily}\@IEEEcompsocconfonly{\normalfont\normalsize\vskip 2\@IEEEnormalsizeunitybaselineskip + \bfseries\Large}\@title\par}\vskip1.0em\par% + % V1.6 handle \author differently if in conference mode + \ifCLASSOPTIONconference% + {\@IEEEspecialpapernotice\mbox{}\vskip\@IEEEauthorblockconfadjspace% + \mbox{}\hfill\begin{@IEEEauthorhalign}\@author\end{@IEEEauthorhalign}\hfill\mbox{}\par}\relax + \else% peerreviewca, peerreview or journal + \ifCLASSOPTIONpeerreviewca + % peerreviewca handles author names just like conference mode + {\@IEEEcompsoconly{\sffamily}\@IEEEspecialpapernotice\mbox{}\vskip\@IEEEauthorblockconfadjspace% + \mbox{}\hfill\begin{@IEEEauthorhalign}\@author\end{@IEEEauthorhalign}\hfill\mbox{}\par + {\@IEEEcompsoconly{\vskip 1.5em\relax + \@IEEEcompsoctitleabstractindextextbox{\@IEEEcompsoctitleabstractindextext}\par\hfill + \@IEEEcompsocdiamondline\hfill\hbox{}\par}}}\relax + \else% journal or peerreview + {\lineskip.5em\@IEEEcompsoconly{\sffamily}\sublargesize\@author\@IEEEspecialpapernotice\par + {\@IEEEcompsoconly{\vskip 1.5em\relax + \@IEEEcompsoctitleabstractindextextbox{\@IEEEcompsoctitleabstractindextext}\par\hfill + \@IEEEcompsocdiamondline\hfill\hbox{}\par}}}\relax + \fi + \fi +\fi\par\endgroup} + + + +% V1.7 Computer Society "diamond line" which follows index terms for nonconference papers +\def\@IEEEcompsocdiamondline{\vrule depth 0pt height 0.5pt width 4cm\hspace{7.5pt}% +\raisebox{-3.5pt}{\fontfamily{pzd}\fontencoding{U}\fontseries{m}\fontshape{n}\fontsize{11}{12}\selectfont\char70}% +\hspace{7.5pt}\vrule depth 0pt height 0.5pt width 4cm\relax} + +% V1.7 standard LateX2e \thanks, but with \itshape under compsoc. Also make it a \long\def +% We also need to trigger the one-shot footnote rule +\def\@IEEEtriggeroneshotfootnoterule{\global\@IEEEenableoneshotfootnoteruletrue} + + +\long\def\thanks#1{\footnotemark + \protected@xdef\@thanks{\@thanks + \protect\footnotetext[\the\c@footnote]{\@IEEEcompsoconly{\itshape + \protect\@IEEEtriggeroneshotfootnoterule\relax}\ignorespaces#1}}} +\let\@thanks\@empty + +% V1.7 allow \author to contain \par's. This is needed to allow \thanks to contain \par. +\long\def\author#1{\gdef\@author{#1}} + + +% in addition to setting up IEEEitemize, we need to remove a baselineskip space above and +% below it because \list's \pars introduce blank lines because of the footnote struts. +\def\@IEEEsetupcompsocitemizelist{\def\labelitemi{$\bullet$}% +\setlength{\IEEElabelindent}{0pt}\setlength{\parskip}{0pt}% +\setlength{\partopsep}{0pt}\setlength{\topsep}{0.5\baselineskip}\vspace{-1\baselineskip}\relax} + + +% flag for fake non-compsoc \IEEEcompsocthanksitem - prevents line break on very first item +\newif\if@IEEEbreakcompsocthanksitem \@IEEEbreakcompsocthanksitemfalse + +\ifCLASSOPTIONcompsoc +% V1.7 compsoc bullet item \thanks +% also, we need to redefine this to destroy the argument in \@IEEEdynamictitlevspace +\long\def\IEEEcompsocitemizethanks#1{\relax\@IEEEbreakcompsocthanksitemfalse\footnotemark + \protected@xdef\@thanks{\@thanks + \protect\footnotetext[\the\c@footnote]{\itshape\protect\@IEEEtriggeroneshotfootnoterule + {\let\IEEEiedlistdecl\relax\protect\begin{IEEEitemize}[\protect\@IEEEsetupcompsocitemizelist]\ignorespaces#1\relax + \protect\end{IEEEitemize}}\protect\vspace{-1\baselineskip}}}} +\DeclareRobustCommand*{\IEEEcompsocthanksitem}{\item} +\else +% non-compsoc, allow for dual compilation via rerouting to normal \thanks +\long\def\IEEEcompsocitemizethanks#1{\thanks{#1}} +% redirect to "pseudo-par" \hfil\break\indent after swallowing [] from \IEEEcompsocthanksitem[] +\DeclareRobustCommand{\IEEEcompsocthanksitem}{\@ifnextchar [{\@IEEEthanksswallowoptionalarg}% +{\@IEEEthanksswallowoptionalarg[\relax]}} +% be sure and break only after first item, be sure and ignore spaces after optional argument +\def\@IEEEthanksswallowoptionalarg[#1]{\relax\if@IEEEbreakcompsocthanksitem\hfil\break +\indent\fi\@IEEEbreakcompsocthanksitemtrue\ignorespaces} +\fi + + +% V1.6b define the \IEEEpeerreviewmaketitle as needed +\ifCLASSOPTIONpeerreview +\def\IEEEpeerreviewmaketitle{\@IEEEcleardoublepage{empty}% +\ifCLASSOPTIONtwocolumn +\twocolumn[\@IEEEpeerreviewmaketitle\@IEEEdynamictitlevspace] +\else +\newpage\@IEEEpeerreviewmaketitle\@IEEEstatictitlevskip +\fi +\thispagestyle{IEEEtitlepagestyle}} +\else +% \IEEEpeerreviewmaketitle does nothing if peer review option has not been selected +\def\IEEEpeerreviewmaketitle{\relax} +\fi + +% peerreview formats the repeated title like the title in journal papers. +\def\@IEEEpeerreviewmaketitle{\begin{center}\@IEEEcompsoconly{\sffamily}% +\normalfont\normalsize\vskip0.2em{\Huge\@title\par}\vskip1.0em\par +\end{center}} + + + +% V1.6 +% this is a static rubber spacer between the title/authors and the main text +% used for single column text, or when the title appears in the first column +% of two column text (technotes). +\def\@IEEEstatictitlevskip{{\normalfont\normalsize +% adjust spacing to next text +% v1.6b handle peer review papers +\ifCLASSOPTIONpeerreview +% for peer review papers, the same value is used for both title pages +% regardless of the other paper modes + \vskip 1\baselineskip plus 0.375\baselineskip minus 0.1875\baselineskip +\else + \ifCLASSOPTIONconference% conference + \vskip 0.6\baselineskip + \else% + \ifCLASSOPTIONtechnote% technote + \vskip 1\baselineskip plus 0.375\baselineskip minus 0.1875\baselineskip% + \else% journal uses more space + \vskip 2.5\baselineskip plus 0.75\baselineskip minus 0.375\baselineskip% + \fi + \fi +\fi}} + + +% V1.6 +% This is a dynamically determined rigid spacer between the title/authors +% and the main text. This is used only for single column titles over two +% column text (most common) +% This is bit tricky because we have to ensure that the textheight of the +% main text is an integer multiple of \baselineskip +% otherwise underfull vbox problems may develop in the second column of the +% text on the titlepage +% The possible use of \IEEEpubid must also be taken into account. +\def\@IEEEdynamictitlevspace{{% + % we run within a group so that all the macros can be forgotten when we are done + \long\def\thanks##1{\relax}%don't allow \thanks to run when we evaluate the vbox height + \long\def\IEEEcompsocitemizethanks##1{\relax}%don't allow \IEEEcompsocitemizethanks to run when we evaluate the vbox height + \normalfont\normalsize% we declare more descriptive variable names + \let\@IEEEmaintextheight=\@IEEEtrantmpdimenA%height of the main text columns + \let\@IEEEINTmaintextheight=\@IEEEtrantmpdimenB%height of the main text columns with integer # lines + % set the nominal and minimum values for the title spacer + % the dynamic algorithm will not allow the spacer size to + % become less than \@IEEEMINtitlevspace - instead it will be + % lengthened + % default to journal values + \def\@IEEENORMtitlevspace{2.5\baselineskip}% + \def\@IEEEMINtitlevspace{2\baselineskip}% + % conferences and technotes need tighter spacing + \ifCLASSOPTIONconference%conference + \def\@IEEENORMtitlevspace{1\baselineskip}% + \def\@IEEEMINtitlevspace{0.75\baselineskip}% + \fi + \ifCLASSOPTIONtechnote%technote + \def\@IEEENORMtitlevspace{1\baselineskip}% + \def\@IEEEMINtitlevspace{0.75\baselineskip}% + \fi% + % get the height that the title will take up + \ifCLASSOPTIONpeerreview + \settoheight{\@IEEEmaintextheight}{\vbox{\hsize\textwidth \@IEEEpeerreviewmaketitle}}% + \else + \settoheight{\@IEEEmaintextheight}{\vbox{\hsize\textwidth \@maketitle}}% + \fi + \@IEEEmaintextheight=-\@IEEEmaintextheight% title takes away from maintext, so reverse sign + % add the height of the page textheight + \advance\@IEEEmaintextheight by \textheight% + % correct for title pages using pubid + \ifCLASSOPTIONpeerreview\else + % peerreview papers use the pubid on the cover page only. + % And the cover page uses a static spacer. + \if@IEEEusingpubid\advance\@IEEEmaintextheight by -\@IEEEpubidpullup\fi + \fi% + % subtract off the nominal value of the title bottom spacer + \advance\@IEEEmaintextheight by -\@IEEENORMtitlevspace% + % \topskip takes away some too + \advance\@IEEEmaintextheight by -\topskip% + % calculate the column height of the main text for lines + % now we calculate the main text height as if holding + % an integer number of \normalsize lines after the first + % and discard any excess fractional remainder + % we subtracted the first line, because the first line + % is placed \topskip into the maintext, not \baselineskip like the + % rest of the lines. + \@IEEEINTmaintextheight=\@IEEEmaintextheight% + \divide\@IEEEINTmaintextheight by \baselineskip% + \multiply\@IEEEINTmaintextheight by \baselineskip% + % now we calculate how much the title spacer height will + % have to be reduced from nominal (\@IEEEREDUCEmaintextheight is always + % a positive value) so that the maintext area will contain an integer + % number of normal size lines + % we change variable names here (to avoid confusion) as we no longer + % need \@IEEEINTmaintextheight and can reuse its dimen register + \let\@IEEEREDUCEmaintextheight=\@IEEEINTmaintextheight% + \advance\@IEEEREDUCEmaintextheight by -\@IEEEmaintextheight% + \advance\@IEEEREDUCEmaintextheight by \baselineskip% + % this is the calculated height of the spacer + % we change variable names here (to avoid confusion) as we no longer + % need \@IEEEmaintextheight and can reuse its dimen register + \let\@IEEECOMPENSATElen=\@IEEEmaintextheight% + \@IEEECOMPENSATElen=\@IEEENORMtitlevspace% set the nominal value + % we go with the reduced length if it is smaller than an increase + \ifdim\@IEEEREDUCEmaintextheight < 0.5\baselineskip\relax% + \advance\@IEEECOMPENSATElen by -\@IEEEREDUCEmaintextheight% + % if the resulting spacer is too small back out and go with an increase instead + \ifdim\@IEEECOMPENSATElen<\@IEEEMINtitlevspace\relax% + \advance\@IEEECOMPENSATElen by \baselineskip% + \fi% + \else% + % go with an increase because it is closer to the nominal than a decrease + \advance\@IEEECOMPENSATElen by -\@IEEEREDUCEmaintextheight% + \advance\@IEEECOMPENSATElen by \baselineskip% + \fi% + % set the calculated rigid spacer + \vspace{\@IEEECOMPENSATElen}}} + + + +% V1.6 +% we allow the user access to the last part of the title area +% useful in emergencies such as when a different spacing is needed +% This text is NOT compensated for in the dynamic sizer. +\let\@IEEEaftertitletext=\relax +\long\def\IEEEaftertitletext#1{\def\@IEEEaftertitletext{#1}} + +% V1.7 provide a way for users to enter abstract and keywords +% into the onecolumn title are. This text is compensated for +% in the dynamic sizer. +\let\@IEEEcompsoctitleabstractindextext=\relax +\long\def\IEEEcompsoctitleabstractindextext#1{\def\@IEEEcompsoctitleabstractindextext{#1}} +% V1.7 provide a way for users to get the \@IEEEcompsoctitleabstractindextext if +% not in compsoc journal mode - this way abstract and keywords can be placed +% in their conventional position if not in compsoc mode. +\def\IEEEdisplaynotcompsoctitleabstractindextext{% +\ifCLASSOPTIONcompsoc% display if compsoc conf +\ifCLASSOPTIONconference\@IEEEcompsoctitleabstractindextext\fi +\else% or if not compsoc +\@IEEEcompsoctitleabstractindextext\fi} + + +% command to allow alteration of baselinestretch, but only if the current +% baselineskip is unity. Used to tweak the compsoc abstract and keywords line spacing. +\def\@IEEEtweakunitybaselinestretch#1{{\def\baselinestretch{1}\selectfont +\global\@tempskipa\baselineskip}\ifnum\@tempskipa=\baselineskip% +\def\baselinestretch{#1}\selectfont\fi\relax} + + +% abstract and keywords are in \small, except +% for 9pt docs in which they are in \footnotesize +% Because 9pt docs use an 8pt footnotesize, \small +% becomes a rather awkward 8.5pt +\def\@IEEEabskeysecsize{\small} +\ifx\CLASSOPTIONpt\@IEEEptsizenine + \def\@IEEEabskeysecsize{\footnotesize} +\fi + +% compsoc journals use \footnotesize, compsoc conferences use normalsize +\@IEEEcompsoconly{\def\@IEEEabskeysecsize{\footnotesize}} +\@IEEEcompsocconfonly{\def\@IEEEabskeysecsize{\normalsize}} + + + + +% V1.6 have abstract and keywords strip leading spaces, pars and newlines +% so that spacing is more tightly controlled. +\def\abstract{\normalfont + \if@twocolumn + \par\@IEEEabskeysecsize\bfseries\leavevmode\kern-1pt\textit{\abstractname}---\relax + \else + \begin{center}\vspace{-1.78ex}\@IEEEabskeysecsize\textbf{\abstractname}\end{center}\quotation\@IEEEabskeysecsize + \fi\@IEEEgobbleleadPARNLSP} +% V1.6 IEEE wants only 1 pica from end of abstract to introduction heading when in +% conference mode (the heading already has this much above it) +\def\endabstract{\relax\ifCLASSOPTIONconference\vspace{0ex}\else\vspace{1.34ex}\fi\par\if@twocolumn\else\endquotation\fi + \normalfont\normalsize} + +\def\IEEEkeywords{\normalfont + \if@twocolumn + \@IEEEabskeysecsize\bfseries\leavevmode\kern-1pt\textit{\IEEEkeywordsname}---\relax + \else + \begin{center}\@IEEEabskeysecsize\textbf{\IEEEkeywordsname}\end{center}\quotation\@IEEEabskeysecsize + \fi\itshape\@IEEEgobbleleadPARNLSP} +\def\endIEEEkeywords{\relax\ifCLASSOPTIONtechnote\vspace{1.34ex}\else\vspace{0.5ex}\fi + \par\if@twocolumn\else\endquotation\fi% + \normalfont\normalsize} + +% V1.7 compsoc keywords index terms +\ifCLASSOPTIONcompsoc + \ifCLASSOPTIONconference% compsoc conference +\def\abstract{\normalfont + \begin{center}\@IEEEabskeysecsize\textbf{\large\abstractname}\end{center}\vskip 0.5\baselineskip plus 0.1\baselineskip minus 0.1\baselineskip + \if@twocolumn\else\quotation\fi\itshape\@IEEEabskeysecsize% + \par\@IEEEgobbleleadPARNLSP} +\def\IEEEkeywords{\normalfont\vskip 1.5\baselineskip plus 0.25\baselineskip minus 0.25\baselineskip + \begin{center}\@IEEEabskeysecsize\textbf{\large\IEEEkeywordsname}\end{center}\vskip 0.5\baselineskip plus 0.1\baselineskip minus 0.1\baselineskip + \if@twocolumn\else\quotation\fi\itshape\@IEEEabskeysecsize% + \par\@IEEEgobbleleadPARNLSP} + \else% compsoc not conference +\def\abstract{\normalfont\@IEEEtweakunitybaselinestretch{1.15}\sffamily + \if@twocolumn + \@IEEEabskeysecsize\noindent\textbf{\abstractname}---\relax + \else + \begin{center}\vspace{-1.78ex}\@IEEEabskeysecsize\textbf{\abstractname}\end{center}\quotation\@IEEEabskeysecsize% + \fi\@IEEEgobbleleadPARNLSP} +\def\IEEEkeywords{\normalfont\@IEEEtweakunitybaselinestretch{1.15}\sffamily + \if@twocolumn + \@IEEEabskeysecsize\vskip 0.5\baselineskip plus 0.25\baselineskip minus 0.25\baselineskip\noindent + \textbf{\IEEEkeywordsname}---\relax + \else + \begin{center}\@IEEEabskeysecsize\textbf{\IEEEkeywordsname}\end{center}\quotation\@IEEEabskeysecsize% + \fi\@IEEEgobbleleadPARNLSP} + \fi +\fi + + + +% gobbles all leading \, \\ and \par, upon finding first token that +% is not a \ , \\ or a \par, it ceases and returns that token +% +% used to strip leading \, \\ and \par from the input +% so that such things in the beginning of an environment will not +% affect the formatting of the text +\long\def\@IEEEgobbleleadPARNLSP#1{\let\@IEEEswallowthistoken=0% +\let\@IEEEgobbleleadPARNLSPtoken#1% +\let\@IEEEgobbleleadPARtoken=\par% +\let\@IEEEgobbleleadNLtoken=\\% +\let\@IEEEgobbleleadSPtoken=\ % +\def\@IEEEgobbleleadSPMACRO{\ }% +\ifx\@IEEEgobbleleadPARNLSPtoken\@IEEEgobbleleadPARtoken% +\let\@IEEEswallowthistoken=1% +\fi% +\ifx\@IEEEgobbleleadPARNLSPtoken\@IEEEgobbleleadNLtoken% +\let\@IEEEswallowthistoken=1% +\fi% +\ifx\@IEEEgobbleleadPARNLSPtoken\@IEEEgobbleleadSPtoken% +\let\@IEEEswallowthistoken=1% +\fi% +% a control space will come in as a macro +% when it is the last one on a line +\ifx\@IEEEgobbleleadPARNLSPtoken\@IEEEgobbleleadSPMACRO% +\let\@IEEEswallowthistoken=1% +\fi% +% if we have to swallow this token, do so and taste the next one +% else spit it out and stop gobbling +\ifx\@IEEEswallowthistoken 1\let\@IEEEnextgobbleleadPARNLSP=\@IEEEgobbleleadPARNLSP\else% +\let\@IEEEnextgobbleleadPARNLSP=#1\fi% +\@IEEEnextgobbleleadPARNLSP}% + + + + +% TITLING OF SECTIONS +\def\@IEEEsectpunct{:\ \,} % Punctuation after run-in section heading (headings which are + % part of the paragraphs), need little bit more than a single space + % spacing from section number to title +% compsoc conferences use regular period/space punctuation +\ifCLASSOPTIONcompsoc +\ifCLASSOPTIONconference +\def\@IEEEsectpunct{.\ } +\fi\fi + +\def\@seccntformat#1{\hb@xt@ 1.4em{\csname the#1dis\endcsname\hss\relax}} +\def\@seccntformatinl#1{\hb@xt@ 1.1em{\csname the#1dis\endcsname\hss\relax}} +\def\@seccntformatch#1{\csname the#1dis\endcsname\hskip 1em\relax} + +\ifCLASSOPTIONcompsoc +% compsoc journals need extra spacing +\ifCLASSOPTIONconference\else +\def\@seccntformat#1{\csname the#1dis\endcsname\hskip 1em\relax} +\fi\fi + +%v1.7 put {} after #6 to allow for some types of user font control +%and use \@@par rather than \par +\def\@sect#1#2#3#4#5#6[#7]#8{% + \ifnum #2>\c@secnumdepth + \let\@svsec\@empty + \else + \refstepcounter{#1}% + % load section label and spacer into \@svsec + \ifnum #2=1 + \protected@edef\@svsec{\@seccntformatch{#1}\relax}% + \else + \ifnum #2>2 + \protected@edef\@svsec{\@seccntformatinl{#1}\relax}% + \else + \protected@edef\@svsec{\@seccntformat{#1}\relax}% + \fi + \fi + \fi% + \@tempskipa #5\relax + \ifdim \@tempskipa>\z@% tempskipa determines whether is treated as a high + \begingroup #6{\relax% or low level heading + \noindent % subsections are NOT indented + % print top level headings. \@svsec is label, #8 is heading title + % IEEE does not block indent the section title text, it flows like normal + {\hskip #3\relax\@svsec}{\interlinepenalty \@M #8\@@par}}% + \endgroup + \addcontentsline{toc}{#1}{\ifnum #2>\c@secnumdepth\relax\else + \protect\numberline{\csname the#1\endcsname}\fi#7}% + \else % printout low level headings + % svsechd seems to swallow the trailing space, protect it with \mbox{} + % got rid of sectionmark stuff + \def\@svsechd{#6{\hskip #3\relax\@svsec #8\@IEEEsectpunct\mbox{}}% + \addcontentsline{toc}{#1}{\ifnum #2>\c@secnumdepth\relax\else + \protect\numberline{\csname the#1\endcsname}\fi#7}}% + \fi%skip down + \@xsect{#5}} + + +% section* handler +%v1.7 put {} after #4 to allow for some types of user font control +%and use \@@par rather than \par +\def\@ssect#1#2#3#4#5{\@tempskipa #3\relax + \ifdim \@tempskipa>\z@ + %\begingroup #4\@hangfrom{\hskip #1}{\interlinepenalty \@M #5\par}\endgroup + % IEEE does not block indent the section title text, it flows like normal + \begingroup \noindent #4{\relax{\hskip #1}{\interlinepenalty \@M #5\@@par}}\endgroup + % svsechd swallows the trailing space, protect it with \mbox{} + \else \def\@svsechd{#4{\hskip #1\relax #5\@IEEEsectpunct\mbox{}}}\fi + \@xsect{#3}} + + +%% SECTION heading spacing and font +%% +% arguments are: #1 - sectiontype name +% (for \@sect) #2 - section level +% #3 - section heading indent +% #4 - top separation (absolute value used, neg indicates not to indent main text) +% If negative, make stretch parts negative too! +% #5 - (absolute value used) positive: bottom separation after heading, +% negative: amount to indent main text after heading +% Both #4 and #5 negative means to indent main text and use negative top separation +% #6 - font control +% You've got to have \normalfont\normalsize in the font specs below to prevent +% trouble when you do something like: +% \section{Note}{\ttfamily TT-TEXT} is known to ... +% IEEE sometimes REALLY stretches the area before a section +% heading by up to about 0.5in. However, it may not be a good +% idea to let LaTeX have quite this much rubber. +\ifCLASSOPTIONconference% +% IEEE wants section heading spacing to decrease for conference mode +\def\section{\@startsection{section}{1}{\z@}{1.5ex plus 1.5ex minus 0.5ex}% +{1sp}{\normalfont\normalsize\centering\scshape}}% +\def\subsection{\@startsection{subsection}{2}{\z@}{1.5ex plus 1.5ex minus 0.5ex}% +{1sp}{\normalfont\normalsize\itshape}}% +\else % for journals +\def\section{\@startsection{section}{1}{\z@}{3.0ex plus 1.5ex minus 1.5ex}% V1.6 3.0ex from 3.5ex +{0.7ex plus 1ex minus 0ex}{\normalfont\normalsize\centering\scshape}}% +\def\subsection{\@startsection{subsection}{2}{\z@}{3.5ex plus 1.5ex minus 1.5ex}% +{0.7ex plus .5ex minus 0ex}{\normalfont\normalsize\itshape}}% +\fi + +% for both journals and conferences +% decided to put in a little rubber above the section, might help somebody +\def\subsubsection{\@startsection{subsubsection}{3}{\parindent}{0ex plus 0.1ex minus 0.1ex}% +{0ex}{\normalfont\normalsize\itshape}}% +\def\paragraph{\@startsection{paragraph}{4}{2\parindent}{0ex plus 0.1ex minus 0.1ex}% +{0ex}{\normalfont\normalsize\itshape}}% + + +% compsoc +\ifCLASSOPTIONcompsoc +\ifCLASSOPTIONconference +% compsoc conference +\def\section{\@startsection{section}{1}{\z@}{1\baselineskip plus 0.25\baselineskip minus 0.25\baselineskip}% +{1\baselineskip plus 0.25\baselineskip minus 0.25\baselineskip}{\normalfont\large\bfseries}}% +\def\subsection{\@startsection{subsection}{2}{\z@}{1\baselineskip plus 0.25\baselineskip minus 0.25\baselineskip}% +{1\baselineskip plus 0.25\baselineskip minus 0.25\baselineskip}{\normalfont\sublargesize\bfseries}}% +\def\subsubsection{\@startsection{subsubsection}{3}{\z@}{1\baselineskip plus 0.25\baselineskip minus 0.25\baselineskip}% +{0ex}{\normalfont\normalsize\bfseries}}% +\def\paragraph{\@startsection{paragraph}{4}{2\parindent}{0ex plus 0.1ex minus 0.1ex}% +{0ex}{\normalfont\normalsize}}% +\else% compsoc journals +% use negative top separation as compsoc journals do not indent paragraphs after section titles +\def\section{\@startsection{section}{1}{\z@}{-3ex plus -2ex minus -1.5ex}% +{0.7ex plus 1ex minus 0ex}{\normalfont\large\sffamily\bfseries\scshape}}% +% Note that subsection and smaller may not be correct for the Computer Society, +% I have to look up an example. +\def\subsection{\@startsection{subsection}{2}{\z@}{-3.5ex plus -1.5ex minus -1.5ex}% +{0.7ex plus .5ex minus 0ex}{\normalfont\normalsize\sffamily\bfseries}}% +\def\subsubsection{\@startsection{subsubsection}{3}{\z@}{-2.5ex plus -1ex minus -1ex}% +{0.5ex plus 0.5ex minus 0ex}{\normalfont\normalsize\sffamily\itshape}}% +\def\paragraph{\@startsection{paragraph}{4}{2\parindent}{-0ex plus -0.1ex minus -0.1ex}% +{0ex}{\normalfont\normalsize}}% +\fi\fi + + + + +%% ENVIRONMENTS +% "box" symbols at end of proofs +\def\IEEEQEDclosed{\mbox{\rule[0pt]{1.3ex}{1.3ex}}} % for a filled box +% V1.6 some journals use an open box instead that will just fit around a closed one +\def\IEEEQEDopen{{\setlength{\fboxsep}{0pt}\setlength{\fboxrule}{0.2pt}\fbox{\rule[0pt]{0pt}{1.3ex}\rule[0pt]{1.3ex}{0pt}}}} +\ifCLASSOPTIONcompsoc +\def\IEEEQED{\IEEEQEDopen} % default to open for compsoc +\else +\def\IEEEQED{\IEEEQEDclosed} % otherwise default to closed +\fi + +% v1.7 name change to avoid namespace collision with amsthm. Also add support +% for an optional argument. +\def\IEEEproof{\@ifnextchar[{\@IEEEproof}{\@IEEEproof[\IEEEproofname]}} +\def\@IEEEproof[#1]{\par\noindent\hspace{2em}{\itshape #1: }} +\def\endIEEEproof{\hspace*{\fill}~\IEEEQED\par} + + +%\itemindent is set to \z@ by list, so define new temporary variable +\newdimen\@IEEEtmpitemindent +\def\@begintheorem#1#2{\@IEEEtmpitemindent\itemindent\topsep 0pt\rmfamily\trivlist% + \item[\hskip \labelsep{\indent\itshape #1\ #2:}]\itemindent\@IEEEtmpitemindent} +\def\@opargbegintheorem#1#2#3{\@IEEEtmpitemindent\itemindent\topsep 0pt\rmfamily \trivlist% +% V1.6 IEEE is back to using () around theorem names which are also in italics +% Thanks to Christian Peel for reporting this. + \item[\hskip\labelsep{\indent\itshape #1\ #2\ (#3):}]\itemindent\@IEEEtmpitemindent} +% V1.7 remove bogus \unskip that caused equations in theorems to collide with +% lines below. +\def\@endtheorem{\endtrivlist} + +% V1.6 +% display command for the section the theorem is in - so that \thesection +% is not used as this will be in Roman numerals when we want arabic. +% LaTeX2e uses \def\@thmcounter#1{\noexpand\arabic{#1}} for the theorem number +% (second part) display and \def\@thmcountersep{.} as a separator. +% V1.7 intercept calls to the section counter and reroute to \@IEEEthmcounterinsection +% to allow \appendix(ices} to override as needed. +% +% special handler for sections, allows appendix(ices) to override +\gdef\@IEEEthmcounterinsection#1{\arabic{#1}} +% string macro +\edef\@IEEEstringsection{section} + +% redefine the #1#2[#3] form of newtheorem to use a hook to \@IEEEthmcounterinsection +% if section in_counter is used +\def\@xnthm#1#2[#3]{% + \expandafter\@ifdefinable\csname #1\endcsname + {\@definecounter{#1}\@newctr{#1}[#3]% + \edef\@IEEEstringtmp{#3} + \ifx\@IEEEstringtmp\@IEEEstringsection + \expandafter\xdef\csname the#1\endcsname{% + \noexpand\@IEEEthmcounterinsection{#3}\@thmcountersep + \@thmcounter{#1}}% + \else + \expandafter\xdef\csname the#1\endcsname{% + \expandafter\noexpand\csname the#3\endcsname \@thmcountersep + \@thmcounter{#1}}% + \fi + \global\@namedef{#1}{\@thm{#1}{#2}}% + \global\@namedef{end#1}{\@endtheorem}}} + + + +%% SET UP THE DEFAULT PAGESTYLE +\ps@headings +\pagenumbering{arabic} + +% normally the page counter starts at 1 +\setcounter{page}{1} +% however, for peerreview the cover sheet is page 0 or page -1 +% (for duplex printing) +\ifCLASSOPTIONpeerreview + \if@twoside + \setcounter{page}{-1} + \else + \setcounter{page}{0} + \fi +\fi + +% standard book class behavior - let bottom line float up and down as +% needed when single sided +\ifCLASSOPTIONtwoside\else\raggedbottom\fi +% if two column - turn on twocolumn, allow word spacings to stretch more and +% enforce a rigid position for the last lines +\ifCLASSOPTIONtwocolumn +% the peer review option delays invoking twocolumn + \ifCLASSOPTIONpeerreview\else + \twocolumn + \fi +\sloppy +\flushbottom +\fi + + + + +% \APPENDIX and \APPENDICES definitions + +% This is the \@ifmtarg command from the LaTeX ifmtarg package +% by Peter Wilson (CUA) and Donald Arseneau +% \@ifmtarg is used to determine if an argument to a command +% is present or not. +% For instance: +% \@ifmtarg{#1}{\typeout{empty}}{\typeout{has something}} +% \@ifmtarg is used with our redefined \section command if +% \appendices is invoked. +% The command \section will behave slightly differently depending +% on whether the user specifies a title: +% \section{My appendix title} +% or not: +% \section{} +% This way, we can eliminate the blank lines where the title +% would be, and the unneeded : after Appendix in the table of +% contents +\begingroup +\catcode`\Q=3 +\long\gdef\@ifmtarg#1{\@xifmtarg#1QQ\@secondoftwo\@firstoftwo\@nil} +\long\gdef\@xifmtarg#1#2Q#3#4#5\@nil{#4} +\endgroup +% end of \@ifmtarg defs + + +% V1.7 +% command that allows the one time saving of the original definition +% of section to \@IEEEappendixsavesection for \appendix or \appendices +% we don't save \section here as it may be redefined later by other +% packages (hyperref.sty, etc.) +\def\@IEEEsaveoriginalsectiononce{\let\@IEEEappendixsavesection\section +\let\@IEEEsaveoriginalsectiononce\relax} + +% neat trick to grab and process the argument from \section{argument} +% we process differently if the user invoked \section{} with no +% argument (title) +% note we reroute the call to the old \section* +\def\@IEEEprocessthesectionargument#1{% +\@ifmtarg{#1}{% +\@IEEEappendixsavesection*{\appendixname~\thesectiondis}% +\addcontentsline{toc}{section}{\appendixname~\thesection}}{% +\@IEEEappendixsavesection*{\appendixname~\thesectiondis \\* #1}% +\addcontentsline{toc}{section}{\appendixname~\thesection: #1}}} + +% we use this if the user calls \section{} after +% \appendix-- which has no meaning. So, we ignore the +% command and its argument. Then, warn the user. +\def\@IEEEdestroythesectionargument#1{\typeout{** WARNING: Ignoring useless +\protect\section\space in Appendix (line \the\inputlineno).}} + + +% remember \thesection forms will be displayed in \ref calls +% and in the Table of Contents. +% The \sectiondis form is used in the actual heading itself + +% appendix command for one single appendix +% normally has no heading. However, if you want a +% heading, you can do so via the optional argument: +% \appendix[Optional Heading] +\def\appendix{\relax} +\renewcommand{\appendix}[1][]{\@IEEEsaveoriginalsectiononce\par + % v1.6 keep hyperref's identifiers unique + \gdef\theHsection{Appendix.A}% + % v1.6 adjust hyperref's string name for the section + \xdef\Hy@chapapp{appendix}% + \setcounter{section}{0}% + \setcounter{subsection}{0}% + \setcounter{subsubsection}{0}% + \setcounter{paragraph}{0}% + \gdef\thesection{A}% + \gdef\thesectiondis{}% + \gdef\thesubsection{\Alph{subsection}}% + \gdef\@IEEEthmcounterinsection##1{A} + \refstepcounter{section}% update the \ref counter + \@ifmtarg{#1}{\@IEEEappendixsavesection*{\appendixname}% + \addcontentsline{toc}{section}{\appendixname}}{% + \@IEEEappendixsavesection*{\appendixname~\\* #1}% + \addcontentsline{toc}{section}{\appendixname: #1}}% + % redefine \section command for appendix + % leave \section* as is + \def\section{\@ifstar{\@IEEEappendixsavesection*}{% + \@IEEEdestroythesectionargument}}% throw out the argument + % of the normal form +} + + + +% appendices command for multiple appendices +% user then calls \section with an argument (possibly empty) to +% declare the individual appendices +\def\appendices{\@IEEEsaveoriginalsectiononce\par + % v1.6 keep hyperref's identifiers unique + \gdef\theHsection{Appendix.\Alph{section}}% + % v1.6 adjust hyperref's string name for the section + \xdef\Hy@chapapp{appendix}% + \setcounter{section}{-1}% we want \refstepcounter to use section 0 + \setcounter{subsection}{0}% + \setcounter{subsubsection}{0}% + \setcounter{paragraph}{0}% + \ifCLASSOPTIONromanappendices% + \gdef\thesection{\Roman{section}}% + \gdef\thesectiondis{\Roman{section}}% + \@IEEEcompsocconfonly{\gdef\thesectiondis{\Roman{section}.}}% + \gdef\@IEEEthmcounterinsection##1{A\arabic{##1}} + \else% + \gdef\thesection{\Alph{section}}% + \gdef\thesectiondis{\Alph{section}}% + \@IEEEcompsocconfonly{\gdef\thesectiondis{\Alph{section}.}}% + \gdef\@IEEEthmcounterinsection##1{\Alph{##1}} + \fi% + \refstepcounter{section}% update the \ref counter + \setcounter{section}{0}% NEXT \section will be the FIRST appendix + % redefine \section command for appendices + % leave \section* as is + \def\section{\@ifstar{\@IEEEappendixsavesection*}{% process the *-form + \refstepcounter{section}% or is a new section so, + \@IEEEprocessthesectionargument}}% process the argument + % of the normal form +} + + + +% \IEEEPARstart +% Definition for the big two line drop cap letter at the beginning of the +% first paragraph of journal papers. The first argument is the first letter +% of the first word, the second argument is the remaining letters of the +% first word which will be rendered in upper case. +% In V1.6 this has been completely rewritten to: +% +% 1. no longer have problems when the user begins an environment +% within the paragraph that uses \IEEEPARstart. +% 2. auto-detect and use the current font family +% 3. revise handling of the space at the end of the first word so that +% interword glue will now work as normal. +% 4. produce correctly aligned edges for the (two) indented lines. +% +% We generalize things via control macros - playing with these is fun too. +% +% V1.7 added more control macros to make it easy for IEEEtrantools.sty users +% to change the font style. +% +% the number of lines that are indented to clear it +% may need to increase if using decenders +\def\@IEEEPARstartDROPLINES{2} +% minimum number of lines left on a page to allow a \@IEEEPARstart +% Does not take into consideration rubber shrink, so it tends to +% be overly cautious +\def\@IEEEPARstartMINPAGELINES{2} +% V1.7 the height of the drop cap is adjusted to match the height of this text +% in the current font (when \IEEEPARstart is called). +\def\@IEEEPARstartHEIGHTTEXT{T} +% the depth the letter is lowered below the baseline +% the height (and size) of the letter is determined by the sum +% of this value and the height of the \@IEEEPARstartHEIGHTTEXT in the current +% font. It is a good idea to set this value in terms of the baselineskip +% so that it can respond to changes therein. +\def\@IEEEPARstartDROPDEPTH{1.1\baselineskip} +% V1.7 the font the drop cap will be rendered in, +% can take zero or one argument. +\def\@IEEEPARstartFONTSTYLE{\bfseries} +% V1.7 any additional, non-font related commands needed to modify +% the drop cap letter, can take zero or one argument. +\def\@IEEEPARstartCAPSTYLE{\MakeUppercase} +% V1.7 the font that will be used to render the rest of the word, +% can take zero or one argument. +\def\@IEEEPARstartWORDFONTSTYLE{\relax} +% V1.7 any additional, non-font related commands needed to modify +% the rest of the word, can take zero or one argument. +\def\@IEEEPARstartWORDCAPSTYLE{\MakeUppercase} +% This is the horizontal separation distance from the drop letter to the main text. +% Lengths that depend on the font (e.g., ex, em, etc.) will be referenced +% to the font that is active when \IEEEPARstart is called. +\def\@IEEEPARstartSEP{0.15em} +% V1.7 horizontal offset applied to the left of the drop cap. +\def\@IEEEPARstartHOFFSET{0em} +% V1.7 Italic correction command applied at the end of the drop cap. +\def\@IEEEPARstartITLCORRECT{\/} + +% V1.7 compoc uses nonbold drop cap and small caps word style +\ifCLASSOPTIONcompsoc +\def\@IEEEPARstartFONTSTYLE{\mdseries} +\def\@IEEEPARstartWORDFONTSTYLE{\scshape} +\def\@IEEEPARstartWORDCAPSTYLE{\relax} +\fi + +% definition of \IEEEPARstart +% THIS IS A CONTROLLED SPACING AREA, DO NOT ALLOW SPACES WITHIN THESE LINES +% +% The token \@IEEEPARstartfont will be globally defined after the first use +% of \IEEEPARstart and will be a font command which creates the big letter +% The first argument is the first letter of the first word and the second +% argument is the rest of the first word(s). +\def\IEEEPARstart#1#2{\par{% +% if this page does not have enough space, break it and lets start +% on a new one +\@IEEEtranneedspace{\@IEEEPARstartMINPAGELINES\baselineskip}{\relax}% +% V1.7 move this up here in case user uses \textbf for \@IEEEPARstartFONTSTYLE +% which uses command \leavevmode which causes an unwanted \indent to be issued +\noindent +% calculate the desired height of the big letter +% it extends from the top of \@IEEEPARstartHEIGHTTEXT in the current font +% down to \@IEEEPARstartDROPDEPTH below the current baseline +\settoheight{\@IEEEtrantmpdimenA}{\@IEEEPARstartHEIGHTTEXT}% +\addtolength{\@IEEEtrantmpdimenA}{\@IEEEPARstartDROPDEPTH}% +% extract the name of the current font in bold +% and place it in \@IEEEPARstartFONTNAME +\def\@IEEEPARstartGETFIRSTWORD##1 ##2\relax{##1}% +{\@IEEEPARstartFONTSTYLE{\selectfont\edef\@IEEEPARstartFONTNAMESPACE{\fontname\font\space}% +\xdef\@IEEEPARstartFONTNAME{\expandafter\@IEEEPARstartGETFIRSTWORD\@IEEEPARstartFONTNAMESPACE\relax}}}% +% define a font based on this name with a point size equal to the desired +% height of the drop letter +\font\@IEEEPARstartsubfont\@IEEEPARstartFONTNAME\space at \@IEEEtrantmpdimenA\relax% +% save this value as a counter (integer) value (sp points) +\@IEEEtrantmpcountA=\@IEEEtrantmpdimenA% +% now get the height of the actual letter produced by this font size +\settoheight{\@IEEEtrantmpdimenB}{\@IEEEPARstartsubfont\@IEEEPARstartCAPSTYLE{#1}}% +% If something bogus happens like the first argument is empty or the +% current font is strange, do not allow a zero height. +\ifdim\@IEEEtrantmpdimenB=0pt\relax% +\typeout{** WARNING: IEEEPARstart drop letter has zero height! (line \the\inputlineno)}% +\typeout{ Forcing the drop letter font size to 10pt.}% +\@IEEEtrantmpdimenB=10pt% +\fi% +% and store it as a counter +\@IEEEtrantmpcountB=\@IEEEtrantmpdimenB% +% Since a font size doesn't exactly correspond to the height of the capital +% letters in that font, the actual height of the letter, \@IEEEtrantmpcountB, +% will be less than that desired, \@IEEEtrantmpcountA +% we need to raise the font size, \@IEEEtrantmpdimenA +% by \@IEEEtrantmpcountA / \@IEEEtrantmpcountB +% But, TeX doesn't have floating point division, so we have to use integer +% division. Hence the use of the counters. +% We need to reduce the denominator so that the loss of the remainder will +% have minimal affect on the accuracy of the result +\divide\@IEEEtrantmpcountB by 200% +\divide\@IEEEtrantmpcountA by \@IEEEtrantmpcountB% +% Then reequalize things when we use TeX's ability to multiply by +% floating point values +\@IEEEtrantmpdimenB=0.005\@IEEEtrantmpdimenA% +\multiply\@IEEEtrantmpdimenB by \@IEEEtrantmpcountA% +% \@IEEEPARstartfont is globaly set to the calculated font of the big letter +% We need to carry this out of the local calculation area to to create the +% big letter. +\global\font\@IEEEPARstartfont\@IEEEPARstartFONTNAME\space at \@IEEEtrantmpdimenB% +% Now set \@IEEEtrantmpdimenA to the width of the big letter +% We need to carry this out of the local calculation area to set the +% hanging indent +\settowidth{\global\@IEEEtrantmpdimenA}{\@IEEEPARstartfont +\@IEEEPARstartCAPSTYLE{#1\@IEEEPARstartITLCORRECT}}}% +% end of the isolated calculation environment +% add in the extra clearance we want +\advance\@IEEEtrantmpdimenA by \@IEEEPARstartSEP\relax% +% add in the optional offset +\advance\@IEEEtrantmpdimenA by \@IEEEPARstartHOFFSET\relax% +% V1.7 don't allow negative offsets to produce negative hanging indents +\@IEEEtrantmpdimenB\@IEEEtrantmpdimenA +\ifnum\@IEEEtrantmpdimenB < 0 \@IEEEtrantmpdimenB 0pt\fi +% \@IEEEtrantmpdimenA has the width of the big letter plus the +% separation space and \@IEEEPARstartfont is the font we need to use +% Now, we make the letter and issue the hanging indent command +% The letter is placed in a box of zero width and height so that other +% text won't be displaced by it. +\hangindent\@IEEEtrantmpdimenB\hangafter=-\@IEEEPARstartDROPLINES% +\makebox[0pt][l]{\hspace{-\@IEEEtrantmpdimenA}% +\raisebox{-\@IEEEPARstartDROPDEPTH}[0pt][0pt]{\hspace{\@IEEEPARstartHOFFSET}% +\@IEEEPARstartfont\@IEEEPARstartCAPSTYLE{#1\@IEEEPARstartITLCORRECT}% +\hspace{\@IEEEPARstartSEP}}}% +{\@IEEEPARstartWORDFONTSTYLE{\@IEEEPARstartWORDCAPSTYLE{\selectfont#2}}}} + + + + + + +% determines if the space remaining on a given page is equal to or greater +% than the specified space of argument one +% if not, execute argument two (only if the remaining space is greater than zero) +% and issue a \newpage +% +% example: \@IEEEtranneedspace{2in}{\vfill} +% +% Does not take into consideration rubber shrinkage, so it tends to +% be overly cautious +% Based on an example posted by Donald Arseneau +% Note this macro uses \@IEEEtrantmpdimenB internally for calculations, +% so DO NOT PASS \@IEEEtrantmpdimenB to this routine +% if you need a dimen register, import with \@IEEEtrantmpdimenA instead +\def\@IEEEtranneedspace#1#2{\penalty-100\begingroup%shield temp variable +\@IEEEtrantmpdimenB\pagegoal\advance\@IEEEtrantmpdimenB-\pagetotal% space left +\ifdim #1>\@IEEEtrantmpdimenB\relax% not enough space left +\ifdim\@IEEEtrantmpdimenB>\z@\relax #2\fi% +\newpage% +\fi\endgroup} + + + +% IEEEbiography ENVIRONMENT +% Allows user to enter biography leaving place for picture (adapts to font size) +% As of V1.5, a new optional argument allows you to have a real graphic! +% V1.5 and later also fixes the "colliding biographies" which could happen when a +% biography's text was shorter than the space for the photo. +% MDS 7/2001 +% V1.6 prevent multiple biographies from making multiple TOC entries +\newif\if@IEEEbiographyTOCentrynotmade +\global\@IEEEbiographyTOCentrynotmadetrue + +% biography counter so hyperref can jump directly to the biographies +% and not just the previous section +\newcounter{IEEEbiography} +\setcounter{IEEEbiography}{0} + +% photo area size +\def\@IEEEBIOphotowidth{1.0in} % width of the biography photo area +\def\@IEEEBIOphotodepth{1.25in} % depth (height) of the biography photo area +% area cleared for photo +\def\@IEEEBIOhangwidth{1.14in} % width cleared for the biography photo area +\def\@IEEEBIOhangdepth{1.25in} % depth cleared for the biography photo area + % actual depth will be a multiple of + % \baselineskip, rounded up +\def\@IEEEBIOskipN{4\baselineskip}% nominal value of the vskip above the biography + +\newenvironment{IEEEbiography}[2][]{\normalfont\@IEEEcompsoconly{\sffamily}\footnotesize% +\unitlength 1in\parskip=0pt\par\parindent 1em\interlinepenalty500% +% we need enough space to support the hanging indent +% the nominal value of the spacer +% and one extra line for good measure +\@IEEEtrantmpdimenA=\@IEEEBIOhangdepth% +\advance\@IEEEtrantmpdimenA by \@IEEEBIOskipN% +\advance\@IEEEtrantmpdimenA by 1\baselineskip% +% if this page does not have enough space, break it and lets start +% with a new one +\@IEEEtranneedspace{\@IEEEtrantmpdimenA}{\relax}% +% nominal spacer can strech, not shrink use 1fil so user can out stretch with \vfill +\vskip \@IEEEBIOskipN plus 1fil minus 0\baselineskip% +% the default box for where the photo goes +\def\@IEEEtempbiographybox{{\setlength{\fboxsep}{0pt}\framebox{% +\begin{minipage}[b][\@IEEEBIOphotodepth][c]{\@IEEEBIOphotowidth}\centering PLACE\\ PHOTO\\ HERE \end{minipage}}}}% +% +% detect if the optional argument was supplied, this requires the +% \@ifmtarg command as defined in the appendix section above +% and if so, override the default box with what they want +\@ifmtarg{#1}{\relax}{\def\@IEEEtempbiographybox{\mbox{\begin{minipage}[b][\@IEEEBIOphotodepth][c]{\@IEEEBIOphotowidth}% +\centering% +#1% +\end{minipage}}}}% end if optional argument supplied +% Make an entry into the table of contents only if we have not done so before +\if@IEEEbiographyTOCentrynotmade% +% link labels to the biography counter so hyperref will jump +% to the biography, not the previous section +\setcounter{IEEEbiography}{-1}% +\refstepcounter{IEEEbiography}% +\addcontentsline{toc}{section}{Biographies}% +\global\@IEEEbiographyTOCentrynotmadefalse% +\fi% +% one more biography +\refstepcounter{IEEEbiography}% +% Make an entry for this name into the table of contents +\addcontentsline{toc}{subsection}{#2}% +% V1.6 properly handle if a new paragraph should occur while the +% hanging indent is still active. Do this by redefining \par so +% that it will not start a new paragraph. (But it will appear to the +% user as if it did.) Also, strip any leading pars, newlines, or spaces. +\let\@IEEEBIOORGparCMD=\par% save the original \par command +\edef\par{\hfil\break\indent}% the new \par will not be a "real" \par +\settoheight{\@IEEEtrantmpdimenA}{\@IEEEtempbiographybox}% get height of biography box +\@IEEEtrantmpdimenB=\@IEEEBIOhangdepth% +\@IEEEtrantmpcountA=\@IEEEtrantmpdimenB% countA has the hang depth +\divide\@IEEEtrantmpcountA by \baselineskip% calculates lines needed to produce the hang depth +\advance\@IEEEtrantmpcountA by 1% ensure we overestimate +% set the hanging indent +\hangindent\@IEEEBIOhangwidth% +\hangafter-\@IEEEtrantmpcountA% +% reference the top of the photo area to the top of a capital T +\settoheight{\@IEEEtrantmpdimenB}{\mbox{T}}% +% set the photo box, give it zero width and height so as not to disturb anything +\noindent\makebox[0pt][l]{\hspace{-\@IEEEBIOhangwidth}\raisebox{\@IEEEtrantmpdimenB}[0pt][0pt]{% +\raisebox{-\@IEEEBIOphotodepth}[0pt][0pt]{\@IEEEtempbiographybox}}}% +% now place the author name and begin the bio text +\noindent\textbf{#2\ }\@IEEEgobbleleadPARNLSP}{\relax\let\par=\@IEEEBIOORGparCMD\par% +% 7/2001 V1.5 detect when the biography text is shorter than the photo area +% and pad the unused area - preventing a collision from the next biography entry +% MDS +\ifnum \prevgraf <\@IEEEtrantmpcountA\relax% detect when the biography text is shorter than the photo + \advance\@IEEEtrantmpcountA by -\prevgraf% calculate how many lines we need to pad + \advance\@IEEEtrantmpcountA by -1\relax% we compensate for the fact that we indented an extra line + \@IEEEtrantmpdimenA=\baselineskip% calculate the length of the padding + \multiply\@IEEEtrantmpdimenA by \@IEEEtrantmpcountA% + \noindent\rule{0pt}{\@IEEEtrantmpdimenA}% insert an invisible support strut +\fi% +\par\normalfont} + + + +% V1.6 +% added biography without a photo environment +\newenvironment{IEEEbiographynophoto}[1]{% +% Make an entry into the table of contents only if we have not done so before +\if@IEEEbiographyTOCentrynotmade% +% link labels to the biography counter so hyperref will jump +% to the biography, not the previous section +\setcounter{IEEEbiography}{-1}% +\refstepcounter{IEEEbiography}% +\addcontentsline{toc}{section}{Biographies}% +\global\@IEEEbiographyTOCentrynotmadefalse% +\fi% +% one more biography +\refstepcounter{IEEEbiography}% +% Make an entry for this name into the table of contents +\addcontentsline{toc}{subsection}{#1}% +\normalfont\@IEEEcompsoconly{\sffamily}\footnotesize\interlinepenalty500% +\vskip 4\baselineskip plus 1fil minus 0\baselineskip% +\parskip=0pt\par% +\noindent\textbf{#1\ }\@IEEEgobbleleadPARNLSP}{\relax\par\normalfont} + + +% provide the user with some old font commands +% got this from article.cls +\DeclareOldFontCommand{\rm}{\normalfont\rmfamily}{\mathrm} +\DeclareOldFontCommand{\sf}{\normalfont\sffamily}{\mathsf} +\DeclareOldFontCommand{\tt}{\normalfont\ttfamily}{\mathtt} +\DeclareOldFontCommand{\bf}{\normalfont\bfseries}{\mathbf} +\DeclareOldFontCommand{\it}{\normalfont\itshape}{\mathit} +\DeclareOldFontCommand{\sl}{\normalfont\slshape}{\@nomath\sl} +\DeclareOldFontCommand{\sc}{\normalfont\scshape}{\@nomath\sc} +\DeclareRobustCommand*\cal{\@fontswitch\relax\mathcal} +\DeclareRobustCommand*\mit{\@fontswitch\relax\mathnormal} + + +% SPECIAL PAPER NOTICE COMMANDS +% +% holds the special notice text +\def\@IEEEspecialpapernotice{\relax} + +% for special papers, like invited papers, the user can do: +% \IEEEspecialpapernotice{(Invited Paper)} before \maketitle +\def\IEEEspecialpapernotice#1{\ifCLASSOPTIONconference% +\def\@IEEEspecialpapernotice{{\Large#1\vspace*{1em}}}% +\else% +\def\@IEEEspecialpapernotice{{\\*[1.5ex]\sublargesize\textit{#1}}\vspace*{-2ex}}% +\fi} + + + + +% PUBLISHER ID COMMANDS +% to insert a publisher's ID footer +% V1.6 \IEEEpubid has been changed so that the change in page size and style +% occurs in \maketitle. \IEEEpubid must now be issued prior to \maketitle +% use \IEEEpubidadjcol as before - in the second column of the title page +% These changes allow \maketitle to take the reduced page height into +% consideration when dynamically setting the space between the author +% names and the maintext. +% +% the amount the main text is pulled up to make room for the +% publisher's ID footer +% IEEE uses about 1.3\baselineskip for journals, +% dynamic title spacing will clean up the fraction +\def\@IEEEpubidpullup{1.3\baselineskip} +\ifCLASSOPTIONtechnote +% for technotes it must be an integer of baselineskip as there can be no +% dynamic title spacing for two column mode technotes (the title is in the +% in first column) and we should maintain an integer number of lines in the +% second column +% There are some examples (such as older issues of "Transactions on +% Information Theory") in which IEEE really pulls the text off the ID for +% technotes - about 0.55in (or 4\baselineskip). We'll use 2\baselineskip +% and call it even. +\def\@IEEEpubidpullup{2\baselineskip} +\fi + +% V1.7 compsoc does not use a pullup +\ifCLASSOPTIONcompsoc +\def\@IEEEpubidpullup{0pt} +\fi + +% holds the ID text +\def\@IEEEpubid{\relax} + +% flag so \maketitle can tell if \IEEEpubid was called +\newif\if@IEEEusingpubid +\global\@IEEEusingpubidfalse +% issue this command in the page to have the ID at the bottom +% V1.6 use before \maketitle +\def\IEEEpubid#1{\def\@IEEEpubid{#1}\global\@IEEEusingpubidtrue} + + +% command which will pull up (shorten) the column it is executed in +% to make room for the publisher ID. Place in the second column of +% the title page when using \IEEEpubid +% Is smart enough not to do anything when in single column text or +% if the user hasn't called \IEEEpubid +% currently needed in for the second column of a page with the +% publisher ID. If not needed in future releases, please provide this +% command and define it as \relax for backward compatibility +% v1.6b do not allow command to operate if the peer review option has been +% selected because \IEEEpubidadjcol will not be on the cover page. +% V1.7 do nothing if compsoc +\def\IEEEpubidadjcol{\ifCLASSOPTIONcompsoc\else\ifCLASSOPTIONpeerreview\else +\if@twocolumn\if@IEEEusingpubid\enlargethispage{-\@IEEEpubidpullup}\fi\fi\fi\fi} + +% Special thanks to Peter Wilson, Daniel Luecking, and the other +% gurus at comp.text.tex, for helping me to understand how best to +% implement the IEEEpubid command in LaTeX. + + + +%% Lockout some commands under various conditions + +% general purpose bit bucket +\newsavebox{\@IEEEtranrubishbin} + +% flags to prevent multiple warning messages +\newif\if@IEEEWARNthanks +\newif\if@IEEEWARNIEEEPARstart +\newif\if@IEEEWARNIEEEbiography +\newif\if@IEEEWARNIEEEbiographynophoto +\newif\if@IEEEWARNIEEEpubid +\newif\if@IEEEWARNIEEEpubidadjcol +\newif\if@IEEEWARNIEEEmembership +\newif\if@IEEEWARNIEEEaftertitletext +\@IEEEWARNthankstrue +\@IEEEWARNIEEEPARstarttrue +\@IEEEWARNIEEEbiographytrue +\@IEEEWARNIEEEbiographynophototrue +\@IEEEWARNIEEEpubidtrue +\@IEEEWARNIEEEpubidadjcoltrue +\@IEEEWARNIEEEmembershiptrue +\@IEEEWARNIEEEaftertitletexttrue + + +%% Lockout some commands when in various modes, but allow them to be restored if needed +%% +% save commands which might be locked out +% so that the user can later restore them if needed +\let\@IEEESAVECMDthanks\thanks +\let\@IEEESAVECMDIEEEPARstart\IEEEPARstart +\let\@IEEESAVECMDIEEEbiography\IEEEbiography +\let\@IEEESAVECMDendIEEEbiography\endIEEEbiography +\let\@IEEESAVECMDIEEEbiographynophoto\IEEEbiographynophoto +\let\@IEEESAVECMDendIEEEbiographynophoto\endIEEEbiographynophoto +\let\@IEEESAVECMDIEEEpubid\IEEEpubid +\let\@IEEESAVECMDIEEEpubidadjcol\IEEEpubidadjcol +\let\@IEEESAVECMDIEEEmembership\IEEEmembership +\let\@IEEESAVECMDIEEEaftertitletext\IEEEaftertitletext + + +% disable \IEEEPARstart when in draft mode +% This may have originally been done because the pre-V1.6 drop letter +% algorithm had problems with a non-unity baselinestretch +% At any rate, it seems too formal to have a drop letter in a draft +% paper. +\ifCLASSOPTIONdraftcls +\def\IEEEPARstart#1#2{#1#2\if@IEEEWARNIEEEPARstart\typeout{** ATTENTION: \noexpand\IEEEPARstart + is disabled in draft mode (line \the\inputlineno).}\fi\global\@IEEEWARNIEEEPARstartfalse} +\fi +% and for technotes +\ifCLASSOPTIONtechnote +\def\IEEEPARstart#1#2{#1#2\if@IEEEWARNIEEEPARstart\typeout{** WARNING: \noexpand\IEEEPARstart + is locked out for technotes (line \the\inputlineno).}\fi\global\@IEEEWARNIEEEPARstartfalse} +\fi + + +% lockout unneeded commands when in conference mode +\ifCLASSOPTIONconference +% when locked out, \thanks, \IEEEbiography, \IEEEbiographynophoto, \IEEEpubid, +% \IEEEmembership and \IEEEaftertitletext will all swallow their given text. +% \IEEEPARstart will output a normal character instead +% warn the user about these commands only once to prevent the console screen +% from filling up with redundant messages +\def\thanks#1{\if@IEEEWARNthanks\typeout{** WARNING: \noexpand\thanks + is locked out when in conference mode (line \the\inputlineno).}\fi\global\@IEEEWARNthanksfalse} +\def\IEEEPARstart#1#2{#1#2\if@IEEEWARNIEEEPARstart\typeout{** WARNING: \noexpand\IEEEPARstart + is locked out when in conference mode (line \the\inputlineno).}\fi\global\@IEEEWARNIEEEPARstartfalse} + + +% LaTeX treats environments and commands with optional arguments differently. +% the actual ("internal") command is stored as \\commandname +% (accessed via \csname\string\commandname\endcsname ) +% the "external" command \commandname is a macro with code to determine +% whether or not the optional argument is presented and to provide the +% default if it is absent. So, in order to save and restore such a command +% we would have to save and restore \\commandname as well. But, if LaTeX +% ever changes the way it names the internal names, the trick would break. +% Instead let us just define a new environment so that the internal +% name can be left undisturbed. +\newenvironment{@IEEEbogusbiography}[2][]{\if@IEEEWARNIEEEbiography\typeout{** WARNING: \noexpand\IEEEbiography + is locked out when in conference mode (line \the\inputlineno).}\fi\global\@IEEEWARNIEEEbiographyfalse% +\setbox\@IEEEtranrubishbin\vbox\bgroup}{\egroup\relax} +% and make biography point to our bogus biography +\let\IEEEbiography=\@IEEEbogusbiography +\let\endIEEEbiography=\end@IEEEbogusbiography + +\renewenvironment{IEEEbiographynophoto}[1]{\if@IEEEWARNIEEEbiographynophoto\typeout{** WARNING: \noexpand\IEEEbiographynophoto + is locked out when in conference mode (line \the\inputlineno).}\fi\global\@IEEEWARNIEEEbiographynophotofalse% +\setbox\@IEEEtranrubishbin\vbox\bgroup}{\egroup\relax} + +\def\IEEEpubid#1{\if@IEEEWARNIEEEpubid\typeout{** WARNING: \noexpand\IEEEpubid + is locked out when in conference mode (line \the\inputlineno).}\fi\global\@IEEEWARNIEEEpubidfalse} +\def\IEEEpubidadjcol{\if@IEEEWARNIEEEpubidadjcol\typeout{** WARNING: \noexpand\IEEEpubidadjcol + is locked out when in conference mode (line \the\inputlineno).}\fi\global\@IEEEWARNIEEEpubidadjcolfalse} +\def\IEEEmembership#1{\if@IEEEWARNIEEEmembership\typeout{** WARNING: \noexpand\IEEEmembership + is locked out when in conference mode (line \the\inputlineno).}\fi\global\@IEEEWARNIEEEmembershipfalse} +\def\IEEEaftertitletext#1{\if@IEEEWARNIEEEaftertitletext\typeout{** WARNING: \noexpand\IEEEaftertitletext + is locked out when in conference mode (line \the\inputlineno).}\fi\global\@IEEEWARNIEEEaftertitletextfalse} +\fi + + +% provide a way to restore the commands that are locked out +\def\IEEEoverridecommandlockouts{% +\typeout{** ATTENTION: Overriding command lockouts (line \the\inputlineno).}% +\let\thanks\@IEEESAVECMDthanks% +\let\IEEEPARstart\@IEEESAVECMDIEEEPARstart% +\let\IEEEbiography\@IEEESAVECMDIEEEbiography% +\let\endIEEEbiography\@IEEESAVECMDendIEEEbiography% +\let\IEEEbiographynophoto\@IEEESAVECMDIEEEbiographynophoto% +\let\endIEEEbiographynophoto\@IEEESAVECMDendIEEEbiographynophoto% +\let\IEEEpubid\@IEEESAVECMDIEEEpubid% +\let\IEEEpubidadjcol\@IEEESAVECMDIEEEpubidadjcol% +\let\IEEEmembership\@IEEESAVECMDIEEEmembership% +\let\IEEEaftertitletext\@IEEESAVECMDIEEEaftertitletext} + + + +% need a backslash character for typeout output +{\catcode`\|=0 \catcode`\\=12 +|xdef|@IEEEbackslash{\}} + + +% hook to allow easy disabling of all legacy warnings +\def\@IEEElegacywarn#1#2{\typeout{** ATTENTION: \@IEEEbackslash #1 is deprecated (line \the\inputlineno). +Use \@IEEEbackslash #2 instead.}} + + +% provide for legacy commands +\def\authorblockA{\@IEEElegacywarn{authorblockA}{IEEEauthorblockA}\IEEEauthorblockA} +\def\authorblockN{\@IEEElegacywarn{authorblockN}{IEEEauthorblockN}\IEEEauthorblockN} +\def\authorrefmark{\@IEEElegacywarn{authorrefmark}{IEEEauthorrefmark}\IEEEauthorrefmark} +\def\PARstart{\@IEEElegacywarn{PARstart}{IEEEPARstart}\IEEEPARstart} +\def\pubid{\@IEEElegacywarn{pubid}{IEEEpubid}\IEEEpubid} +\def\pubidadjcol{\@IEEElegacywarn{pubidadjcol}{IEEEpubidadjcol}\IEEEpubidadjcol} +\def\QED{\@IEEElegacywarn{QED}{IEEEQED}\IEEEQED} +\def\QEDclosed{\@IEEElegacywarn{QEDclosed}{IEEEQEDclosed}\IEEEQEDclosed} +\def\QEDopen{\@IEEElegacywarn{QEDopen}{IEEEQEDopen}\IEEEQEDopen} +\def\specialpapernotice{\@IEEElegacywarn{specialpapernotice}{IEEEspecialpapernotice}\IEEEspecialpapernotice} + + + +% provide for legacy environments +\def\biography{\@IEEElegacywarn{biography}{IEEEbiography}\IEEEbiography} +\def\biographynophoto{\@IEEElegacywarn{biographynophoto}{IEEEbiographynophoto}\IEEEbiographynophoto} +\def\keywords{\@IEEElegacywarn{keywords}{IEEEkeywords}\IEEEkeywords} +\def\endbiography{\endIEEEbiography} +\def\endbiographynophoto{\endIEEEbiographynophoto} +\def\endkeywords{\endIEEEkeywords} + + +% provide for legacy IED commands/lengths when possible +\let\labelindent\IEEElabelindent +\def\calcleftmargin{\@IEEElegacywarn{calcleftmargin}{IEEEcalcleftmargin}\IEEEcalcleftmargin} +\def\setlabelwidth{\@IEEElegacywarn{setlabelwidth}{IEEEsetlabelwidth}\IEEEsetlabelwidth} +\def\usemathlabelsep{\@IEEElegacywarn{usemathlabelsep}{IEEEusemathlabelsep}\IEEEusemathlabelsep} +\def\iedlabeljustifyc{\@IEEElegacywarn{iedlabeljustifyc}{IEEEiedlabeljustifyc}\IEEEiedlabeljustifyc} +\def\iedlabeljustifyl{\@IEEElegacywarn{iedlabeljustifyl}{IEEEiedlabeljustifyl}\IEEEiedlabeljustifyl} +\def\iedlabeljustifyr{\@IEEElegacywarn{iedlabeljustifyr}{IEEEiedlabeljustifyr}\IEEEiedlabeljustifyr} + + + +% let \proof use the IEEEtran version even after amsthm is loaded +% \proof is now deprecated in favor of \IEEEproof +\AtBeginDocument{\def\proof{\@IEEElegacywarn{proof}{IEEEproof}\IEEEproof}\def\endproof{\endIEEEproof}} + +% V1.7 \overrideIEEEmargins is no longer supported. +\def\overrideIEEEmargins{% +\typeout{** WARNING: \string\overrideIEEEmargins \space no longer supported (line \the\inputlineno).}% +\typeout{** Use the \string\CLASSINPUTinnersidemargin, \string\CLASSINPUToutersidemargin \space controls instead.}} + + +\endinput + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% End of IEEEtran.cls %%%%%%%%%%%%%%%%%%%%%%%%%%%% +% That's all folks! + diff --git a/spec/consensus/consensus-paper/README.md b/spec/consensus/consensus-paper/README.md new file mode 100644 index 000000000..33e395806 --- /dev/null +++ b/spec/consensus/consensus-paper/README.md @@ -0,0 +1,24 @@ +# Tendermint-spec + +The repository contains the specification (and the proofs) of the Tendermint +consensus protocol. + +## How to install Latex on Mac OS + +MacTex is Latex distribution for Mac OS. You can download it [here](http://www.tug.org/mactex/mactex-download.html). + +Popular IDE for Latex-based projects is TexStudio. It can be downloaded +[here](https://www.texstudio.org/). + +## How to build project + +In order to compile the latex files (and write bibliography), execute + +`$ pdflatex paper`
+`$ bibtex paper`
+`$ pdflatex paper`
+`$ pdflatex paper`
+ +The generated file is paper.pdf. You can open it with + +`$ open paper.pdf` diff --git a/spec/consensus/consensus-paper/algorithmicplus.sty b/spec/consensus/consensus-paper/algorithmicplus.sty new file mode 100644 index 000000000..de7ca01ea --- /dev/null +++ b/spec/consensus/consensus-paper/algorithmicplus.sty @@ -0,0 +1,195 @@ +% ALGORITHMICPLUS STYLE +% for LaTeX version 2e +% Original ``algorithmic.sty'' by -- 1994 Peter Williams +% Bug fix (13 July 2004) by Arnaud Giersch +% Includes ideas from 'algorithmicext' by Martin Biely +% and 'distribalgo' by Xavier Defago +% Modifications: Martin Hutle +% +% This style file is free software; you can redistribute it and/or +% modify it under the terms of the GNU Lesser General Public +% License as published by the Free Software Foundation; either +% version 2 of the License, or (at your option) any later version. +% +% This style file is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +% Lesser General Public License for more details. +% +% You should have received a copy of the GNU Lesser General Public +% License along with this style file; if not, write to the +% Free Software Foundation, Inc., 59 Temple Place - Suite 330, +% Boston, MA 02111-1307, USA. +% +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{algorithmicplus} +\typeout{Document Style `algorithmicplus' - environment, replaces `algorithmic'} +% +\RequirePackage{ifthen} +\RequirePackage{calc} +\newboolean{ALC@noend} +\setboolean{ALC@noend}{false} +\newcounter{ALC@line} +\newcounter{ALC@rem} +\newcounter{ALC@depth} +\newcounter{ALCPLUS@lastline} +\newlength{\ALC@tlm} +% +\DeclareOption{noend}{\setboolean{ALC@noend}{true}} +% +\ProcessOptions +% +% ALGORITHMIC +\newcommand{\algorithmiclnosize}{\small} +\newcommand{\algorithmiclnofont}{\tt} +\newcommand{\algorithmiclnodelimiter}{:} +% +\newcommand{\algorithmicrequire}{\textbf{Require:}} +\newcommand{\algorithmicensure}{\textbf{Ensure:}} +\newcommand{\algorithmiccomment}[1]{\{#1\}} +\newcommand{\algorithmicend}{\textbf{end}} +\newcommand{\algorithmicif}{\textbf{if}} +\newcommand{\algorithmicthen}{\textbf{then}} +\newcommand{\algorithmicelse}{\textbf{else}} +\newcommand{\algorithmicelsif}{\algorithmicelse\ \algorithmicif} +\newcommand{\algorithmicendif}{\algorithmicend\ \algorithmicif} +\newcommand{\algorithmicfor}{\textbf{for}} +\newcommand{\algorithmicforall}{\textbf{for all}} +\newcommand{\algorithmicdo}{\textbf{do}} +\newcommand{\algorithmicendfor}{\algorithmicend\ \algorithmicfor} +\newcommand{\algorithmicwhile}{\textbf{while}} +\newcommand{\algorithmicendwhile}{\algorithmicend\ \algorithmicwhile} +\newcommand{\algorithmicloop}{\textbf{loop}} +\newcommand{\algorithmicendloop}{\algorithmicend\ \algorithmicloop} +\newcommand{\algorithmicrepeat}{\textbf{repeat}} +\newcommand{\algorithmicuntil}{\textbf{until}} +\def\ALC@item[#1]{% +\if@noparitem \@donoparitem + \else \if@inlabel \indent \par \fi + \ifhmode \unskip\unskip \par \fi + \if@newlist \if@nobreak \@nbitem \else + \addpenalty\@beginparpenalty + \addvspace\@topsep \addvspace{-\parskip}\fi + \else \addpenalty\@itempenalty \addvspace\itemsep + \fi + \global\@inlabeltrue +\fi +\everypar{\global\@minipagefalse\global\@newlistfalse + \if@inlabel\global\@inlabelfalse \hskip -\parindent \box\@labels + \penalty\z@ \fi + \everypar{}}\global\@nobreakfalse +\if@noitemarg \@noitemargfalse \if@nmbrlist \refstepcounter{\@listctr}\fi \fi +\sbox\@tempboxa{\makelabel{#1}}% +\global\setbox\@labels + \hbox{\unhbox\@labels \hskip \itemindent + \hskip -\labelwidth \hskip -\ALC@tlm + \ifdim \wd\@tempboxa >\labelwidth + \box\@tempboxa + \else \hbox to\labelwidth {\unhbox\@tempboxa}\fi + \hskip \ALC@tlm}\ignorespaces} +% +\newenvironment{algorithmic}[1][0]{ +\setcounter{ALC@depth}{\@listdepth}% +\let\@listdepth\c@ALC@depth% +\let\@item\ALC@item + \newcommand{\ALC@lno}{% +\ifthenelse{\equal{\arabic{ALC@rem}}{0}} +{{\algorithmiclnosize\algorithmiclnofont \arabic{ALC@line}\algorithmiclnodelimiter}}{}% +} +\let\@listii\@listi +\let\@listiii\@listi +\let\@listiv\@listi +\let\@listv\@listi +\let\@listvi\@listi +\let\@listvii\@listi + \newenvironment{ALC@g}{ + \begin{list}{\ALC@lno}{ \itemsep\z@ \itemindent\z@ + \listparindent\z@ \rightmargin\z@ + \topsep\z@ \partopsep\z@ \parskip\z@\parsep\z@ + \leftmargin 1em + \addtolength{\ALC@tlm}{\leftmargin} + } + } + {\end{list}} + \newcommand{\ALC@it}{\refstepcounter{ALC@line}\addtocounter{ALC@rem}{1}\ifthenelse{\equal{\arabic{ALC@rem}}{#1}}{\setcounter{ALC@rem}{0}}{}\item} + \newcommand{\ALC@com}[1]{\ifthenelse{\equal{##1}{default}}% +{}{\ \algorithmiccomment{##1}}} + \newcommand{\REQUIRE}{\item[\algorithmicrequire]} + \newcommand{\ENSURE}{\item[\algorithmicensure]} + \newcommand{\STATE}{\ALC@it} + \newcommand{\COMMENT}[1]{\algorithmiccomment{##1}} + \newenvironment{ALC@if}{\begin{ALC@g}}{\end{ALC@g}} + \newenvironment{ALC@for}{\begin{ALC@g}}{\end{ALC@g}} + \newenvironment{ALC@whl}{\begin{ALC@g}}{\end{ALC@g}} + \newenvironment{ALC@loop}{\begin{ALC@g}}{\end{ALC@g}} + \newenvironment{ALC@rpt}{\begin{ALC@g}}{\end{ALC@g}} + \renewcommand{\\}{\@centercr} + \newcommand{\IF}[2][default]{\ALC@it\algorithmicif\ ##2\ \algorithmicthen% +\ALC@com{##1}\begin{ALC@if}} + \newcommand{\ELSE}[1][default]{\end{ALC@if}\ALC@it\algorithmicelse% +\ALC@com{##1}\begin{ALC@if}} + \newcommand{\ELSIF}[2][default]% +{\end{ALC@if}\ALC@it\algorithmicelsif\ ##2\ \algorithmicthen% +\ALC@com{##1}\begin{ALC@if}} + \newcommand{\FOR}[2][default]{\ALC@it\algorithmicfor\ ##2\ \algorithmicdo% +\ALC@com{##1}\begin{ALC@for}} + \newcommand{\FORALL}[2][default]{\ALC@it\algorithmicforall\ ##2\ % +\algorithmicdo% +\ALC@com{##1}\begin{ALC@for}} + \newcommand{\WHILE}[2][default]{\ALC@it\algorithmicwhile\ ##2\ % +\algorithmicdo% +\ALC@com{##1}\begin{ALC@whl}} + \newcommand{\LOOP}[1][default]{\ALC@it\algorithmicloop% +\ALC@com{##1}\begin{ALC@loop}} + \newcommand{\REPEAT}[1][default]{\ALC@it\algorithmicrepeat% +\ALC@com{##1}\begin{ALC@rpt}} + \newcommand{\UNTIL}[1]{\end{ALC@rpt}\ALC@it\algorithmicuntil\ ##1} + \ifthenelse{\boolean{ALC@noend}}{ + \newcommand{\ENDIF}{\end{ALC@if}} + \newcommand{\ENDFOR}{\end{ALC@for}} + \newcommand{\ENDWHILE}{\end{ALC@whl}} + \newcommand{\ENDLOOP}{\end{ALC@loop}} + }{ + \newcommand{\ENDIF}{\end{ALC@if}\ALC@it\algorithmicendif} + \newcommand{\ENDFOR}{\end{ALC@for}\ALC@it\algorithmicendfor} + \newcommand{\ENDWHILE}{\end{ALC@whl}\ALC@it\algorithmicendwhile} + \newcommand{\ENDLOOP}{\end{ALC@loop}\ALC@it\algorithmicendloop} + } + \renewcommand{\@toodeep}{} + \begin{list}{\ALC@lno}{\setcounter{ALC@line}{0}\setcounter{ALC@rem}{0}% + \itemsep\z@ \itemindent\z@ \listparindent\z@% + \partopsep\z@ \parskip\z@ \parsep\z@% + \labelsep 0.5em \topsep 0.2em% +\ifthenelse{\equal{#1}{0}} + {\labelwidth 0.5em } + {\labelwidth 1.2em } +\leftmargin\labelwidth \addtolength{\leftmargin}{\labelsep} + \ALC@tlm\labelsep + } +} +{% +\setcounter{ALCPLUS@lastline}{\value{ALC@line}}% +\end{list}} + +\newcommand{\continuecounting}{\setcounter{ALC@line}{\value{ALCPLUS@lastline}}} +\newcommand{\startcounting}[1]{\setcounter{ALC@line}{#1}\addtocounter{ALC@line}{-1}} + +\newcommand{\EMPTY}{\item[]} +\newcommand{\SPACE}{\vspace{3mm}} +\newcommand{\SHORTSPACE}{\vspace{1mm}} +\newcommand{\newlinetag}[3]{\newcommand{#1}[#2]{\item[#3]}} +\newcommand{\newconstruct}[5]{% + \newenvironment{ALC@\string#1}{\begin{ALC@g}}{\end{ALC@g}} + \newcommand{#1}[2][default]{\ALC@it#2\ ##2\ #3% + \ALC@com{##1}\begin{ALC@\string#1}} + \ifthenelse{\boolean{ALC@noend}}{ + \newcommand{#4}{\end{ALC@\string#1}} + }{ + \newcommand{#4}{\end{ALC@\string#1}\ALC@it#5} + } +} + +\newconstruct{\INDENT}{}{}{\ENDINDENT}{} + +\newcommand{\setlinenosize}[1]{\renewcommand{\algorithmiclnosize}{#1}} +\newcommand{\setlinenofont}[1]{\renewcommand{\algorithmiclnofont}{#1}} diff --git a/spec/consensus/consensus-paper/conclusion.tex b/spec/consensus/consensus-paper/conclusion.tex new file mode 100644 index 000000000..dd17ccf44 --- /dev/null +++ b/spec/consensus/consensus-paper/conclusion.tex @@ -0,0 +1,16 @@ +\section{Conclusion} \label{sec:conclusion} + +We have proposed a new Byzantine-fault tolerant consensus algorithm that is the +core of the Tendermint BFT SMR platform. The algorithm is designed for the wide +area network with high number of mutually distrusted nodes that communicate +over gossip based peer-to-peer network. It has only a single mode of execution +and the communication pattern is very similar to the "normal" case of the +state-of-the art PBFT algorithm. The algorithm ensures termination with a novel +mechanism that takes advantage of the gossip based communication between nodes. +The proposed algorithm and the proofs are simple and elegant, and we believe +that this makes it easier to understand and implement correctly. + +\section*{Acknowledgment} + +We would like to thank Anton Kaliaev, Ismail Khoffi and Dahlia Malkhi for comments on an earlier version of the paper. We also want to thank Marko Vukolic, Ming Chuan Lin, Maria Potop-Butucaru, Sara Tucci, Antonella Del Pozzo and Yackolley Amoussou-Guenou for pointing out the liveness issues +in the previous version of the algorithm. Finally, we want to thank the Tendermint team members and all project contributors for making Tendermint such a great platform. diff --git a/spec/consensus/consensus-paper/consensus.tex b/spec/consensus/consensus-paper/consensus.tex new file mode 100644 index 000000000..3265b61c7 --- /dev/null +++ b/spec/consensus/consensus-paper/consensus.tex @@ -0,0 +1,397 @@ + +\section{Tendermint consensus algorithm} \label{sec:tendermint} + +\newcommand\Disseminate{\textbf{Disseminate}} + +\newcommand\Proposal{\mathsf{PROPOSAL}} +\newcommand\ProposalPart{\mathsf{PROPOSAL\mbox{-}PART}} +\newcommand\PrePrepare{\mathsf{INIT}} \newcommand\Prevote{\mathsf{PREVOTE}} +\newcommand\Precommit{\mathsf{PRECOMMIT}} +\newcommand\Decision{\mathsf{DECISION}} + +\newcommand\ViewChange{\mathsf{VC}} +\newcommand\ViewChangeAck{\mathsf{VC\mbox{-}ACK}} +\newcommand\NewPrePrepare{\mathsf{VC\mbox{-}INIT}} +\newcommand\coord{\mathsf{proposer}} + +\newcommand\newHeight{newHeight} \newcommand\newRound{newRound} +\newcommand\nil{nil} \newcommand\id{id} \newcommand{\propose}{propose} +\newcommand\prevote{prevote} \newcommand\prevoteWait{prevoteWait} +\newcommand\precommit{precommit} \newcommand\precommitWait{precommitWait} +\newcommand\commit{commit} + +\newcommand\timeoutPropose{timeoutPropose} +\newcommand\timeoutPrevote{timeoutPrevote} +\newcommand\timeoutPrecommit{timeoutPrecommit} +\newcommand\proofOfLocking{proof\mbox{-}of\mbox{-}locking} + +\begin{algorithm}[htb!] \def\baselinestretch{1} \scriptsize\raggedright + \begin{algorithmic}[1] + \SHORTSPACE + \INIT{} + \STATE $h_p := 0$ + \COMMENT{current height, or consensus instance we are currently executing} + \STATE $round_p := 0$ \COMMENT{current round number} + \STATE $step_p \in \set{\propose, \prevote, \precommit}$ + \STATE $decision_p[] := nil$ + \STATE $lockedValue_p := nil$ + \STATE $lockedRound_p := -1$ + \STATE $validValue_p := nil$ + \STATE $validRound_p := -1$ + \ENDINIT + \SHORTSPACE + \STATE \textbf{upon} start \textbf{do} $StartRound(0)$ + \SHORTSPACE + \FUNCTION{$StartRound(round)$} \label{line:tab:startRound} + \STATE $round_p \assign round$ + \STATE $step_p \assign \propose$ + \IF{$\coord(h_p, round_p) = p$} + \IF{$validValue_p \neq \nil$} \label{line:tab:isThereLockedValue} + \STATE $proposal \assign validValue_p$ \ELSE \STATE $proposal \assign + getValue()$ + \label{line:tab:getValidValue} + \ENDIF + \STATE \Broadcast\ $\li{\Proposal,h_p, round_p, proposal, validRound_p}$ + \label{line:tab:send-proposal} + \ELSE + \STATE \textbf{schedule} $OnTimeoutPropose(h_p, + round_p)$ to be executed \textbf{after} $\timeoutPropose(round_p)$ + \ENDIF + \ENDFUNCTION + + \SPACE + \UPON{$\li{\Proposal,h_p,round_p, v, -1}$ \From\ $\coord(h_p,round_p)$ + \With\ $step_p = \propose$} \label{line:tab:recvProposal} + \IF{$valid(v) \wedge (lockedRound_p = -1 \vee lockedValue_p = v$)} + \label{line:tab:accept-proposal-2} + \STATE \Broadcast \ $\li{\Prevote,h_p,round_p,id(v)}$ + \label{line:tab:prevote-proposal} + \ELSE + \label{line:tab:acceptProposal1} + \STATE \Broadcast \ $\li{\Prevote,h_p,round_p,\nil}$ + \label{line:tab:prevote-nil} + \ENDIF + \STATE $step_p \assign \prevote$ \label{line:tab:setStateToPrevote1} + \ENDUPON + + \SPACE + \UPON{$\li{\Proposal,h_p,round_p, v, vr}$ \From\ $\coord(h_p,round_p)$ + \textbf{AND} $2f+1$ $\li{\Prevote,h_p, vr,id(v)}$ \With\ $step_p = \propose \wedge (vr \ge 0 \wedge vr < round_p)$} + \label{line:tab:acceptProposal} + \IF{$valid(v) \wedge (lockedRound_p \le vr + \vee lockedValue_p = v)$} \label{line:tab:cond-prevote-higher-proposal} + \STATE \Broadcast \ $\li{\Prevote,h_p,round_p,id(v)}$ + \label{line:tab:prevote-higher-proposal} + \ELSE + \label{line:tab:acceptProposal2} + \STATE \Broadcast \ $\li{\Prevote,h_p,round_p,\nil}$ + \label{line:tab:prevote-nil2} + \ENDIF + \STATE $step_p \assign \prevote$ \label{line:tab:setStateToPrevote3} + \ENDUPON + + \SPACE + \UPON{$2f+1$ $\li{\Prevote,h_p, round_p,*}$ \With\ $step_p = \prevote$ for the first time} + \label{line:tab:recvAny2/3Prevote} + \STATE \textbf{schedule} $OnTimeoutPrevote(h_p, round_p)$ to be executed \textbf{after} $\timeoutPrevote(round_p)$ \label{line:tab:timeoutPrevote} + \ENDUPON + + \SPACE + \UPON{$\li{\Proposal,h_p,round_p, v, *}$ \From\ $\coord(h_p,round_p)$ + \textbf{AND} $2f+1$ $\li{\Prevote,h_p, round_p,id(v)}$ \With\ $valid(v) \wedge step_p \ge \prevote$ for the first time} + \label{line:tab:recvPrevote} + \IF{$step_p = \prevote$} + \STATE $lockedValue_p \assign v$ \label{line:tab:setLockedValue} + \STATE $lockedRound_p \assign round_p$ \label{line:tab:setLockedRound} + \STATE \Broadcast \ $\li{\Precommit,h_p,round_p,id(v))}$ + \label{line:tab:precommit-v} + \STATE $step_p \assign \precommit$ \label{line:tab:setStateToCommit} + \ENDIF + \STATE $validValue_p \assign v$ \label{line:tab:setValidRound} + \STATE $validRound_p \assign round_p$ \label{line:tab:setValidValue} + \ENDUPON + + \SHORTSPACE + \UPON{$2f+1$ $\li{\Prevote,h_p,round_p, \nil}$ + \With\ $step_p = \prevote$} + \STATE \Broadcast \ $\li{\Precommit,h_p,round_p, \nil}$ + \label{line:tab:precommit-v-1} + \STATE $step_p \assign \precommit$ + \ENDUPON + + \SPACE + \UPON{$2f+1$ $\li{\Precommit,h_p,round_p,*}$ for the first time} + \label{line:tab:startTimeoutPrecommit} + \STATE \textbf{schedule} $OnTimeoutPrecommit(h_p, round_p)$ to be executed \textbf{after} $\timeoutPrecommit(round_p)$ + + \ENDUPON + + \SPACE + \UPON{$\li{\Proposal,h_p,r, v, *}$ \From\ $\coord(h_p,r)$ \textbf{AND} + $2f+1$ $\li{\Precommit,h_p,r,id(v)}$ \With\ $decision_p[h_p] = \nil$} + \label{line:tab:onDecideRule} + \IF{$valid(v)$} \label{line:tab:validDecisionValue} + \STATE $decision_p[h_p] = v$ \label{line:tab:decide} + \STATE$h_p \assign h_p + 1$ \label{line:tab:increaseHeight} + \STATE reset $lockedRound_p$, $lockedValue_p$, $validRound_p$ and $validValue_p$ to initial values + and empty message log + \STATE $StartRound(0)$ + \ENDIF + \ENDUPON + + \SHORTSPACE + \UPON{$f+1$ $\li{*,h_p,round, *, *}$ \textbf{with} $round > round_p$} + \label{line:tab:skipRounds} + \STATE $StartRound(round)$ \label{line:tab:nextRound2} + \ENDUPON + + \SHORTSPACE + \FUNCTION{$OnTimeoutPropose(height,round)$} \label{line:tab:onTimeoutPropose} + \IF{$height = h_p \wedge round = round_p \wedge step_p = \propose$} + \STATE \Broadcast \ $\li{\Prevote,h_p,round_p, \nil}$ + \label{line:tab:prevote-nil-on-timeout} + \STATE $step_p \assign \prevote$ + \ENDIF + \ENDFUNCTION + + \SHORTSPACE + \FUNCTION{$OnTimeoutPrevote(height,round)$} \label{line:tab:onTimeoutPrevote} + \IF{$height = h_p \wedge round = round_p \wedge step_p = \prevote$} + \STATE \Broadcast \ $\li{\Precommit,h_p,round_p,\nil}$ + \label{line:tab:precommit-nil-onTimeout} + \STATE $step_p \assign \precommit$ + \ENDIF + \ENDFUNCTION + + \SHORTSPACE + \FUNCTION{$OnTimeoutPrecommit(height,round)$} \label{line:tab:onTimeoutPrecommit} + \IF{$height = h_p \wedge round = round_p$} + \STATE $StartRound(round_p + 1)$ \label{line:tab:nextRound} + \ENDIF + \ENDFUNCTION + \end{algorithmic} \caption{Tendermint consensus algorithm} + \label{alg:tendermint} +\end{algorithm} + +In this section we present the Tendermint Byzantine fault-tolerant consensus +algorithm. The algorithm is specified by the pseudo-code shown in +Algorithm~\ref{alg:tendermint}. We present the algorithm as a set of \emph{upon +rules} that are executed atomically\footnote{In case several rules are active +at the same time, the first rule to be executed is picked randomly. The +correctness of the algorithm does not depend on the order in which rules are +executed.}. We assume that processes exchange protocol messages using a gossip +protocol and that both sent and received messages are stored in a local message +log for every process. An upon rule is triggered once the message log contains +messages such that the corresponding condition evaluates to $\tt{true}$. The +condition that assumes reception of $X$ messages of a particular type and +content denotes reception of messages whose senders have aggregate voting power at +least equal to $X$. For example, the condition $2f+1$ $\li{\Precommit,h_p,r,id(v)}$, +evaluates to true upon reception of $\Precommit$ messages for height $h_p$, +a round $r$ and with value equal to $id(v)$ whose senders have aggregate voting +power at least equal to $2f+1$. Some of the rules ends with "for the first time" constraint +to denote that it is triggered only the first time a corresponding condition evaluates +to $\tt{true}$. This is because those rules do not always change the state of algorithm +variables so without this constraint, the algorithm could keep +executing those rules forever. The variables with index $p$ are process local state +variables, while variables without index $p$ are value placeholders. The sign +$*$ denotes any value. + +We denote with $n$ the total voting power of processes in the system, and we +assume that the total voting power of faulty processes in the system is bounded +with a system parameter $f$. The algorithm assumes that $n > 3f$, i.e., it +requires that the total voting power of faulty processes is smaller than one +third of the total voting power. For simplicity we present the algorithm for +the case $n = 3f + 1$. + +The algorithm proceeds in rounds, where each round has a dedicated +\emph{proposer}. The mapping of rounds to proposers is known to all processes +and is given as a function $\coord(h, round)$, returning the proposer for +the round $round$ in the consensus instance $h$. We +assume that the proposer selection function is weighted round-robin, where +processes are rotated proportional to their voting power\footnote{A validator +with more voting power is selected more frequently, proportional to its power. +More precisely, during a sequence of rounds of size $n$, every process is +proposer in a number of rounds equal to its voting power.}. +The internal protocol state transitions are triggered by message reception and +by expiration of timeouts. There are three timeouts in Algorithm \ref{alg:tendermint}: +$\timeoutPropose$, $\timeoutPrevote$ and $\timeoutPrecommit$. +The timeouts prevent the algorithm from blocking and +waiting forever for some condition to be true, ensure that processes continuously +transition between rounds, and guarantee that eventually (after GST) communication +between correct processes is timely and reliable so they can decide. +The last role is achieved by increasing the timeouts with every new round $r$, +i.e, $timeoutX(r) = initTimeoutX + r*timeoutDelta$; +they are reset for every new height (consensus +instance). + +Processes exchange the following messages in Tendermint: $\Proposal$, +$\Prevote$ and $\Precommit$. The $\Proposal$ message is used by the proposer of +the current round to suggest a potential decision value, while $\Prevote$ and +$\Precommit$ are votes for a proposed value. According to the classification of +consensus algorithms from \cite{RMS10:dsn}, Tendermint, like PBFT +\cite{CL02:tcs} and DLS \cite{DLS88:jacm}, belongs to class 3, so it requires +two voting steps (three communication exchanges in total) to decide a value. +The Tendermint consensus algorithm is designed for the blockchain context where +the value to decide is a block of transactions (ie. it is potentially quite +large, consisting of many transactions). Therefore, in the Algorithm +\ref{alg:tendermint} (similar as in \cite{CL02:tcs}) we are explicit about +sending a value (block of transactions) and a small, constant size value id (a +unique value identifier, normally a hash of the value, i.e., if $\id(v) = +\id(v')$, then $v=v'$). The $\Proposal$ message is the only one carrying the +value; $\Prevote$ and $\Precommit$ messages carry the value id. A correct +process decides on a value $v$ in Tendermint upon receiving the $\Proposal$ for +$v$ and $2f+1$ voting-power equivalent $\Precommit$ messages for $\id(v)$ in +some round $r$. In order to send $\Precommit$ message for $v$ in a round $r$, a +correct process waits to receive the $\Proposal$ and $2f+1$ of the +corresponding $\Prevote$ messages in the round $r$. Otherwise, +it sends $\Precommit$ message with a special $\nil$ value. +This ensures that correct processes can $\Precommit$ only a +single value (or $\nil$) in a round. As +proposers may be faulty, the proposed value is treated by correct processes as +a suggestion (it is not blindly accepted), and a correct process tells others +if it accepted the $\Proposal$ for value $v$ by sending $\Prevote$ message for +$\id(v)$; otherwise it sends $\Prevote$ message with the special $\nil$ value. + +Every process maintains the following variables in the Algorithm +\ref{alg:tendermint}: $step$, $lockedValue$, $lockedRound$, $validValue$ and +$validRound$. The $step$ denotes the current state of the internal Tendermint +state machine, i.e., it reflects the stage of the algorithm execution in the +current round. The $lockedValue$ stores the most recent value (with respect to +a round number) for which a $\Precommit$ message has been sent. The +$lockedRound$ is the last round in which the process sent a $\Precommit$ +message that is not $\nil$. We also say that a correct process locks a value +$v$ in a round $r$ by setting $lockedValue = v$ and $lockedRound = r$ before +sending $\Precommit$ message for $\id(v)$. As a correct process can decide a +value $v$ only if $2f+1$ $\Precommit$ messages for $\id(v)$ are received, this +implies that a possible decision value is a value that is locked by at least +$f+1$ voting power equivalent of correct processes. Therefore, any value $v$ +for which $\Proposal$ and $2f+1$ of the corresponding $\Prevote$ messages are +received in some round $r$ is a \emph{possible decision} value. The role of the +$validValue$ variable is to store the most recent possible decision value; the +$validRound$ is the last round in which $validValue$ is updated. Apart from +those variables, a process also stores the current consensus instance ($h_p$, +called \emph{height} in Tendermint), and the current round number ($round_p$) +and attaches them to every message. Finally, a process also stores an array of +decisions, $decision_p$ (Tendermint assumes a sequence of consensus instances, +one for each height). + +Every round starts by a proposer suggesting a value with the $\Proposal$ +message (see line \ref{line:tab:send-proposal}). In the initial round of each +height, the proposer is free to chose the value to suggest. In the +Algorithm~\ref{alg:tendermint}, a correct process obtains a value to propose +using an external function $getValue()$ that returns a valid value to +propose. In the following rounds, a correct proposer will suggest a new value +only if $validValue = \nil$; otherwise $validValue$ is proposed (see +lines~\ref{line:tab:isThereLockedValue}-\ref{line:tab:getValidValue}). +In addition to the value proposed, the $\Proposal$ message also +contains the $validRound$ so other processes are informed about the last round +in which the proposer observed $validValue$ as a possible decision value. +Note that if a correct proposer $p$ sends $validValue$ with the $validRound$ in the +$\Proposal$, this implies that the process $p$ received $\Proposal$ and the +corresponding $2f+1$ $\Prevote$ messages for $validValue$ in the round +$validRound$. +If a correct process sends $\Proposal$ message with $validValue$ ($validRound > -1$) +at time $t > GST$, by the \emph{Gossip communication} property, the +corresponding $\Proposal$ and the $\Prevote$ messages will be received by all +correct processes before time $t+\Delta$. Therefore, all correct processes will +be able to verify the correctness of the suggested value as it is supported by +the $\Proposal$ and the corresponding $2f+1$ voting power equivalent $\Prevote$ +messages. + +A correct process $p$ accepts the proposal for a value $v$ (send $\Prevote$ +for $id(v)$) if an external \emph{valid} function returns $true$ for the value +$v$, and if $p$ hasn't locked any value ($lockedRound = -1$) or $p$ has locked +the value $v$ ($lockedValue = v$); see the line +\ref{line:tab:accept-proposal-2}. In case the proposed pair is $(v,vr \ge 0)$ and a +correct process $p$ has locked some value, it will accept +$v$ if it is a more recent possible decision value\footnote{As +explained above, the possible decision value in a round $r$ is the one for +which $\Proposal$ and the corresponding $2f+1$ $\Prevote$ messages are received +for the round $r$.}, $vr > lockedRound_p$, or if $lockedValue = v$ +(see line~\ref{line:tab:cond-prevote-higher-proposal}). Otherwise, a correct +process will reject the proposal by sending $\Prevote$ message with $\nil$ +value. A correct process will send $\Prevote$ message with $\nil$ value also in +case $\timeoutPropose$ expired (it is triggered when a correct process starts a +new round) and a process has not sent $\Prevote$ message in the current round +yet (see the line \ref{line:tab:onTimeoutPropose}). + +If a correct process receives $\Proposal$ message for some value $v$ and $2f+1$ +$\Prevote$ messages for $\id(v)$, then it sends $\Precommit$ message with +$\id(v)$. Otherwise, it sends $\Precommit$ $\nil$. A correct process will send +$\Precommit$ message with $\nil$ value also in case $\timeoutPrevote$ expired +(it is started when a correct process sent $\Prevote$ message and received any +$2f+1$ $\Prevote$ messages) and a process has not sent $\Precommit$ message in +the current round yet (see the line \ref{line:tab:onTimeoutPrecommit}). A +correct process decides on some value $v$ if it receives in some round $r$ +$\Proposal$ message for $v$ and $2f+1$ $\Precommit$ messages with $\id(v)$ (see +the line \ref{line:tab:decide}). To prevent the algorithm from blocking and +waiting forever for this condition to be true, the Algorithm +\ref{alg:tendermint} relies on $\timeoutPrecommit$. It is triggered after a +process receives any set of $2f+1$ $\Precommit$ messages for the current round. +If the $\timeoutPrecommit$ expires and a process has not decided yet, the +process starts the next round (see the line \ref{line:tab:onTimeoutPrecommit}). +When a correct process $p$ decides, it starts the next consensus instance +(for the next height). The \emph{Gossip communication} property ensures +that $\Proposal$ and $2f+1$ $\Prevote$ messages that led $p$ to decide +are eventually received by all correct processes, so they will also decide. + +\subsection{Termination mechanism} + +Tendermint ensures termination by a novel mechanism that benefits from the +gossip based nature of communication (see \emph{Gossip communication} +property). It requires managing two additional variables, $validValue$ and +$validRound$ that are then used by the proposer during the propose step as +explained above. The $validValue$ and $validRound$ are updated to $v$ and $r$ +by a correct process in a round $r$ when the process receives valid $\Proposal$ +message for the value $v$ and the corresponding $2f+1$ $\Prevote$ messages for +$id(v)$ in the round $r$ (see the rule at line~\ref{line:tab:recvPrevote}). + +We now give briefly the intuition how managing and proposing $validValue$ +and $validRound$ ensures termination. Formal treatment is left for +Section~\ref{sec:proof}. + +The first thing to note is that during good period, because of the +\emph{Gossip communication} property, if a correct process $p$ locks a value +$v$ in some round $r$, all correct processes will update $validValue$ to $v$ +and $validRound$ to $r$ before the end of the round $r$ (we prove this formally +in the Section~\ref{sec:proof}). The intuition is that messages that led to $p$ +locking a value $v$ in the round $r$ will be gossiped to all correct processes +before the end of the round $r$, so it will update $validValue$ and +$validRound$ (the line~\ref{line:tab:recvPrevote}). Therefore, if a correct +process locks some value during good period, $validValue$ and $validRound$ are +updated by all correct processes so that the value proposed in the following +rounds will be acceptable by all correct processes. Note +that it could happen that during good period, no correct process locks a value, +but some correct process $q$ updates $validValue$ and $validRound$ during some +round. As no correct process locks a value in this case, $validValue_q$ and +$validRound_q$ will also be acceptable by all correct processes as +$validRound_q > lockedRound_c$ for every correct process $c$ and as the +\emph{Gossip communication} property ensures that the corresponding $\Prevote$ +messages that $q$ received in the round $validRound_q$ are received by all +correct processes $\Delta$ time later. + +Finally, it could happen that after GST, there is a long sequence of rounds in which +no correct process neither locks a value nor update $validValue$ and $validRound$. +In this case, during this sequence of rounds, the proposed value suggested by correct +processes was not accepted by all correct processes. Note that this sequence of rounds +is always finite as at the beginning of every +round there is at least a single correct process $c$ such that $validValue_c$ +and $validRound_c$ are acceptable by every correct process. This is true as +there exists a correct process $c$ such that for every other correct process +$p$, $validRound_c > lockedRound_p$ or $validValue_c = lockedValue_p$. This is +true as $c$ is the process that has locked a value in the most recent round +among all correct processes (or no correct process locked any value). Therefore, +eventually $c$ will be the proper in some round and the proposed value will be accepted +by all correct processes, terminating therefore this sequence of +rounds. + +Therefore, updating $validValue$ and $validRound$ variables, and the +\emph{Gossip communication} property, together ensures that eventually, during +the good period, there exists a round with a correct proposer whose proposed +value will be accepted by all correct processes, and all correct processes will +terminate in that round. Note that this mechanism, contrary to the common +termination mechanism illustrated in the +Figure~\ref{ch3:fig:coordinator-change}, does not require exchanging any +additional information in addition to messages already sent as part of what is +normally being called "normal" case. + diff --git a/spec/consensus/consensus-paper/definitions.tex b/spec/consensus/consensus-paper/definitions.tex new file mode 100644 index 000000000..454dd445d --- /dev/null +++ b/spec/consensus/consensus-paper/definitions.tex @@ -0,0 +1,126 @@ +\section{Definitions} \label{sec:definitions} + +\subsection{Model} + +We consider a system of processes that communicate by exchanging messages. +Processes can be correct or faulty, where a faulty process can behave in an +arbitrary way, i.e., we consider Byzantine faults. We assume that each process +has some amount of voting power (voting power of a process can be $0$). +Processes in our model are not part of a single administrative domain; +therefore we cannot enforce a direct network connectivity between all +processes. Instead, we assume that each process is connected to a subset of +processes called peers, such that there is an indirect communication channel +between all correct processes. Communication between processes is established +using a gossip protocol \cite{Dem1987:gossip}. + +Formally, we model the network communication using a variant of the \emph{partially +synchronous system model}~\cite{DLS88:jacm}: in all executions of the system +there is a bound $\Delta$ and an instant GST (Global Stabilization Time) such +that all communication among correct processes after GST is reliable and +$\Delta$-timely, i.e., if a correct process $p$ sends message $m$ at time $t +\ge GST$ to a correct process $q$, then $q$ will receive $m$ before $t + +\Delta$\footnote{Note that as we do not assume direct communication channels + among all correct processes, this implies that before the message $m$ + reaches $q$, it might pass through a number of correct processes that will +forward the message $m$ using gossip protocol towards $q$.}. +In addition to the standard \emph{partially + synchronous system model}~\cite{DLS88:jacm}, we assume an auxiliary property +that captures gossip-based nature of communication\footnote{The details of the Tendermint gossip protocol will be discussed in a separate + technical report. }: + + +\begin{itemize} \item \emph{Gossip communication:} If a correct process $p$ + sends some message $m$ at time $t$, all correct processes will receive + $m$ before $max\{t, GST\} + \Delta$. Furthermore, if a correct process $p$ + receives some message $m$ at time $t$, all correct processes will receive + $m$ before $max\{t, GST\} + \Delta$. \end{itemize} + + +The bound $\Delta$ and GST are system +parameters whose values are not required to be known for the safety of our +algorithm. Termination of the algorithm is guaranteed within a bounded duration +after GST. In practice, the algorithm will work correctly in the slightly +weaker variant of the model where the system alternates between (long enough) +good periods (corresponds to the \emph{after} GST period where system is +reliable and $\Delta$-timely) and bad periods (corresponds to the period +\emph{before} GST during which the system is asynchronous and messages can be +lost), but consideration of the GST model simplifies the discussion. + +We assume that process steps (which might include sending and receiving +messages) take zero time. Processes are equipped with clocks so they can +measure local timeouts. +Spoofing/impersonation attacks are assumed to be impossible at all times due to +the use of public-key cryptography, i.e., we assume that all protocol messages contains a digital signature. +Therefore, when a correct +process $q$ receives a signed message $m$ from its peer, the process $q$ can +verify who was the original sender of the message $m$ and if the message signature is valid. +We do not explicitly state a signature verification step in the pseudo-code of the algorithm to improve readability; +we assume that only messages with the valid signature are considered at that level (and messages with invalid signatures +are dropped). + + + +%Messages that are being gossiped are created by the consensus layer. We can + %think about consensus protocol as a content creator, which %defines what + %messages should be disseminated using the gossip protocol. A correct + %process creates the message for dissemination either i) %explicitly, by + %invoking \emph{send} function as part of the consensus protocol or ii) + %implicitly, by receiving a message from some other %process. Note that in + %the case ii) gossiping of messages is implicit, i.e., it happens without + %explicit send clause in the consensus algorithm %whenever a correct + %process receives some messages in the consensus algorithm\footnote{If a + %message is received by a correct process at %the consensus level then it + %is considered valid from the protocol point of view, i.e., it has a + %correct signature, a proper message structure %and a valid height and + %round number.}. + +%\item Processes keep resending messages (in case of failures or message loss) + %until all its peers get them. This ensures that every message %sent or + %received by a correct process is eventually received by all correct + %processes. + +\subsection{State Machine Replication} + +State machine replication (SMR) is a general approach for replicating services +modeled as a deterministic state machine~\cite{Lam78:cacm,Sch90:survey}. The +key idea of this approach is to guarantee that all replicas start in the same +state and then apply requests from clients in the same order, thereby +guaranteeing that the replicas' states will not diverge. Following +Schneider~\cite{Sch90:survey}, we note that the following is key for +implementing a replicated state machine tolerant to (Byzantine) faults: + +\begin{itemize} \item \emph{Replica Coordination.} All [non-faulty] replicas + receive and process the same sequence of requests. \end{itemize} + +Moreover, as Schneider also notes, this property can be decomposed into two +parts, \emph{Agreement} and \emph{Order}: Agreement requires all (non-faulty) +replicas to receive all requests, and Order requires that the order of received +requests is the same at all replicas. + +There is an additional requirement that needs to be ensured by Byzantine +tolerant state machine replication: only requests (called transactions in the +Tendermint terminology) proposed by clients are executed. In Tendermint, +transaction verification is the responsibility of the service that is being +replicated; upon receiving a transaction from the client, the Tendermint +process will ask the service if the request is valid, and only valid requests +will be processed. + + \subsection{Consensus} \label{sec:consensus} + +Tendermint solves state machine replication by sequentially executing consensus +instances to agree on each block of transactions that are +then executed by the service being replicated. We consider a variant of the +Byzantine consensus problem called Validity Predicate-based Byzantine consensus +that is motivated by blockchain systems~\cite{GLR17:red-belly-bc}. The problem +is defined by an agreement, a termination, and a validity property. + + \begin{itemize} \item \emph{Agreement:} No two correct processes decide on + different values. \item \emph{Termination:} All correct processes + eventually decide on a value. \item \emph{Validity:} A decided value + is valid, i.e., it satisfies the predefined predicate denoted + \emph{valid()}. \end{itemize} + + This variant of the Byzantine consensus problem has an application-specific + \emph{valid()} predicate to indicate whether a value is valid. In the context + of blockchain systems, for example, a value is not valid if it does not + contain an appropriate hash of the last value (block) added to the blockchain. diff --git a/spec/consensus/consensus-paper/homodel.sty b/spec/consensus/consensus-paper/homodel.sty new file mode 100644 index 000000000..19f83e926 --- /dev/null +++ b/spec/consensus/consensus-paper/homodel.sty @@ -0,0 +1,32 @@ +\newcommand{\NC}{\mbox{\it NC}} +\newcommand{\HO}{\mbox{\it HO}} +\newcommand{\AS}{\mbox{\it AS}} +\newcommand{\SK}{\mbox{\it SK}} +\newcommand{\SHO}{\mbox{\it SHO}} +\newcommand{\AHO}{\mbox{\it AHO}} +\newcommand{\CONS}{\mbox{\it CONS}} +\newcommand{\K}{\mbox{\it K}} + +\newcommand{\Alg}{\mathcal{A}} +\newcommand{\Pred}{\mathcal{P}} +\newcommand{\Spr}{S_p^r} +\newcommand{\Tpr}{T_p^r} +\newcommand{\mupr}{\vec{\mu}_p^{\,r}} + +\newcommand{\MSpr}{S_p^{\rho}} +\newcommand{\MTpr}{T_p^{\rho}} + + + +\newconstruct{\SEND}{$\Spr$:}{}{\ENDSEND}{} +\newconstruct{\TRAN}{$\Tpr$:}{}{\ENDTRAN}{} +\newconstruct{\ROUND}{\textbf{Round}}{\!\textbf{:}}{\ENDROUND}{} +\newconstruct{\VARIABLES}{\textbf{Variables:}}{}{\ENDVARIABLES}{} +\newconstruct{\INIT}{\textbf{Initialization:}}{}{\ENDINIT}{} + +\newconstruct{\MSEND}{$\MSpr$:}{}{\ENDMSEND}{} +\newconstruct{\MTRAN}{$\MTpr$:}{}{\ENDMTRAN}{} + +\newconstruct{\SROUND}{\textbf{Selection Round}}{\!\textbf{:}}{\ENDSROUND}{} +\newconstruct{\VROUND}{\textbf{Validation Round}}{\!\textbf{:}}{\ENDVROUND}{} +\newconstruct{\DROUND}{\textbf{Decision Round}}{\!\textbf{:}}{\ENDDROUND}{} diff --git a/spec/consensus/consensus-paper/intro.tex b/spec/consensus/consensus-paper/intro.tex new file mode 100644 index 000000000..493b509e9 --- /dev/null +++ b/spec/consensus/consensus-paper/intro.tex @@ -0,0 +1,138 @@ +\section{Introduction} \label{sec:tendermint} + +Consensus is a fundamental problem in distributed computing. It +is important because of it's role in State Machine Replication (SMR), a generic +approach for replicating services that can be modeled as a deterministic state +machine~\cite{Lam78:cacm, Sch90:survey}. The key idea of this approach is that +service replicas start in the same initial state, and then execute requests +(also called transactions) in the same order; thereby guaranteeing that +replicas stay in sync with each other. The role of consensus in the SMR +approach is ensuring that all replicas receive transactions in the same order. +Traditionally, deployments of SMR based systems are in data-center settings +(local area network), have a small number of replicas (three to seven) and are +typically part of a single administration domain (e.g., Chubby +\cite{Bur:osdi06}); therefore they handle benign (crash) failures only, as more +general forms of failure (in particular, malicious or Byzantine faults) are +considered to occur with only negligible probability. + +The success of cryptocurrencies and blockchain systems in recent years (e.g., +\cite{Nak2012:bitcoin, But2014:ethereum}) pose a whole new set of challenges on +the design and deployment of SMR based systems: reaching agreement over wide +area network, among large number of nodes (hundreds or thousands) that are not +part of the same administrative domain, and where a subset of nodes can behave +maliciously (Byzantine faults). Furthermore, contrary to the previous +data-center deployments where nodes are fully connected to each other, in +blockchain systems, a node is only connected to a subset of other nodes, so +communication is achieved by gossip-based peer-to-peer protocols. +The new requirements demand designs and algorithms that are not necessarily +present in the classical academic literature on Byzantine fault tolerant +consensus (or SMR) systems (e.g., \cite{DLS88:jacm, CL02:tcs}) as the primary +focus was different setup. + +In this paper we describe a novel Byzantine-fault tolerant consensus algorithm +that is the core of the BFT SMR platform called Tendermint\footnote{The + Tendermint platform is available open source at + https://github.com/tendermint/tendermint.}. The Tendermint platform consists of +a high-performance BFT SMR implementation written in Go, a flexible interface +for +building arbitrary deterministic applications above the consensus, and a suite +of tools for deployment and management. + +The Tendermint consensus algorithm is inspired by the PBFT SMR +algorithm~\cite{CL99:osdi} and the DLS algorithm for authenticated faults (the +Algorithm 2 from \cite{DLS88:jacm}). Similar to DLS algorithm, Tendermint +proceeds in +rounds\footnote{Tendermint is not presented in the basic round model of + \cite{DLS88:jacm}. Furthermore, we use the term round differently than in + \cite{DLS88:jacm}; in Tendermint a round denotes a sequence of communication + steps instead of a single communication step in \cite{DLS88:jacm}.}, where each +round has a dedicated proposer (also called coordinator or +leader) and a process proceeds to a new round as part of normal +processing (not only in case the proposer is faulty or suspected as being faulty +by enough processes as in PBFT). +The communication pattern of each round is very similar to the "normal" case +of PBFT. Therefore, in preferable conditions (correct proposer, timely and +reliable communication between correct processes), Tendermint decides in three +communication steps (the same as PBFT). + +The major novelty and contribution of the Tendermint consensus algorithm is a +new termination mechanism. As explained in \cite{MHS09:opodis, RMS10:dsn}, the +existing BFT consensus (and SMR) algorithms for the partially synchronous +system model (for example PBFT~\cite{CL99:osdi}, \cite{DLS88:jacm}, +\cite{MA06:tdsc}) typically relies on the communication pattern illustrated in +Figure~\ref{ch3:fig:coordinator-change} for termination. The +Figure~\ref{ch3:fig:coordinator-change} illustrates messages exchanged during +the proposer change when processes start a new round\footnote{There is no + consistent terminology in the distributed computing terminology on naming + sequence of communication steps that corresponds to a logical unit. It is + sometimes called a round, phase or a view.}. It guarantees that eventually (ie. +after some Global Stabilization Time, GST), there exists a round with a correct +proposer that will bring the system into a univalent configuration. +Intuitively, in a round in which the proposed value is accepted +by all correct processes, and communication between correct processes is +timely and reliable, all correct processes decide. + + +\begin{figure}[tbh!] \def\rdstretch{5} \def\ystretch{3} \centering + \begin{rounddiag}{4}{2} \round{1}{~} \rdmessage{1}{1}{$v_1$} + \rdmessage{2}{1}{$v_2$} \rdmessage{3}{1}{$v_3$} \rdmessage{4}{1}{$v_4$} + \round{2}{~} \rdmessage{1}{1}{$x, [v_{1..4}]$} + \rdmessage{1}{2}{$~~~~~~x, [v_{1..4}]$} \rdmessage{1}{3}{$~~~~~~~~x, + [v_{1..4}]$} \rdmessage{1}{4}{$~~~~~~~x, [v_{1..4}]$} \end{rounddiag} + \vspace{-5mm} \caption{\boldmath Proposer (coordinator) change: $p_1$ is the + new proposer.} \label{ch3:fig:coordinator-change} \end{figure} + +To ensure that a proposed value is accepted by all correct +processes\footnote{The proposed value is not blindly accepted by correct + processes in BFT algorithms. A correct process always verifies if the proposed + value is safe to be accepted so that safety properties of consensus are not + violated.} +a proposer will 1) build the global state by receiving messages from other +processes, 2) select the safe value to propose and 3) send the selected value +together with the signed messages +received in the first step to support it. The +value $v_i$ that a correct process sends to the next proposer normally +corresponds to a value the process considers as acceptable for a decision: + +\begin{itemize} \item in PBFT~\cite{CL99:osdi} and DLS~\cite{DLS88:jacm} it is + not the value itself but a set of $2f+1$ signed messages with the same + value id, \item in Fast Byzantine Paxos~\cite{MA06:tdsc} the value + itself is being sent. \end{itemize} + +In both cases, using this mechanism in our system model (ie. high +number of nodes over gossip based network) would have high communication +complexity that increases with the number of processes: in the first case as +the message sent depends on the total number of processes, and in the second +case as the value (block of transactions) is sent by each process. The set of +messages received in the first step are normally piggybacked on the proposal +message (in the Figure~\ref{ch3:fig:coordinator-change} denoted with +$[v_{1..4}]$) to justify the choice of the selected value $x$. Note that +sending this message also does not scale with the number of processes in the +system. + +We designed a novel termination mechanism for Tendermint that better suits the +system model we consider. It does not require additional communication (neither +sending new messages nor piggybacking information on the existing messages) and +it is fully based on the communication pattern that is very similar to the +normal case in PBFT \cite{CL99:osdi}. Therefore, there is only a single mode of +execution in Tendermint, i.e., there is no separation between the normal and +the recovery mode, which is the case in other PBFT-like protocols (e.g., +\cite{CL99:osdi}, \cite{Ver09:spinning} or \cite{Cle09:aardvark}). We believe +this makes Tendermint simpler to understand and implement correctly. + +Note that the orthogonal approach for reducing message complexity in order to +improve +scalability and decentralization (number of processes) of BFT consensus +algorithms is using advanced cryptography (for example Boneh-Lynn-Shacham (BLS) +signatures \cite{BLS2001:crypto}) as done for example in SBFT +\cite{Gue2018:sbft}. + +The remainder of the paper is as follows: Section~\ref{sec:definitions} defines +the system model and gives the problem definitions. Tendermint +consensus algorithm is presented in Section~\ref{sec:tendermint} and the +proofs are given in Section~\ref{sec:proof}. We conclude in +Section~\ref{sec:conclusion}. + + + + diff --git a/spec/consensus/consensus-paper/latex8.bst b/spec/consensus/consensus-paper/latex8.bst new file mode 100644 index 000000000..2c7af5647 --- /dev/null +++ b/spec/consensus/consensus-paper/latex8.bst @@ -0,0 +1,1124 @@ + +% --------------------------------------------------------------- +% +% $Id: latex8.bst,v 1.1 1995/09/15 15:13:49 ienne Exp $ +% +% by Paolo.Ienne@di.epfl.ch +% + +% --------------------------------------------------------------- +% +% no guarantee is given that the format corresponds perfectly to +% IEEE 8.5" x 11" Proceedings, but most features should be ok. +% +% --------------------------------------------------------------- +% +% `latex8' from BibTeX standard bibliography style `abbrv' +% version 0.99a for BibTeX versions 0.99a or later, LaTeX version 2.09. +% Copyright (C) 1985, all rights reserved. +% Copying of this file is authorized only if either +% (1) you make absolutely no changes to your copy, including name, or +% (2) if you do make changes, you name it something other than +% btxbst.doc, plain.bst, unsrt.bst, alpha.bst, and abbrv.bst. +% This restriction helps ensure that all standard styles are identical. +% The file btxbst.doc has the documentation for this style. + +ENTRY + { address + author + booktitle + chapter + edition + editor + howpublished + institution + journal + key + month + note + number + organization + pages + publisher + school + series + title + type + volume + year + } + {} + { label } + +INTEGERS { output.state before.all mid.sentence after.sentence after.block } + +FUNCTION {init.state.consts} +{ #0 'before.all := + #1 'mid.sentence := + #2 'after.sentence := + #3 'after.block := +} + +STRINGS { s t } + +FUNCTION {output.nonnull} +{ 's := + output.state mid.sentence = + { ", " * write$ } + { output.state after.block = + { add.period$ write$ + newline$ + "\newblock " write$ + } + { output.state before.all = + 'write$ + { add.period$ " " * write$ } + if$ + } + if$ + mid.sentence 'output.state := + } + if$ + s +} + +FUNCTION {output} +{ duplicate$ empty$ + 'pop$ + 'output.nonnull + if$ +} + +FUNCTION {output.check} +{ 't := + duplicate$ empty$ + { pop$ "empty " t * " in " * cite$ * warning$ } + 'output.nonnull + if$ +} + +FUNCTION {output.bibitem} +{ newline$ + "\bibitem{" write$ + cite$ write$ + "}" write$ + newline$ + "" + before.all 'output.state := +} + +FUNCTION {fin.entry} +{ add.period$ + write$ + newline$ +} + +FUNCTION {new.block} +{ output.state before.all = + 'skip$ + { after.block 'output.state := } + if$ +} + +FUNCTION {new.sentence} +{ output.state after.block = + 'skip$ + { output.state before.all = + 'skip$ + { after.sentence 'output.state := } + if$ + } + if$ +} + +FUNCTION {not} +{ { #0 } + { #1 } + if$ +} + +FUNCTION {and} +{ 'skip$ + { pop$ #0 } + if$ +} + +FUNCTION {or} +{ { pop$ #1 } + 'skip$ + if$ +} + +FUNCTION {new.block.checka} +{ empty$ + 'skip$ + 'new.block + if$ +} + +FUNCTION {new.block.checkb} +{ empty$ + swap$ empty$ + and + 'skip$ + 'new.block + if$ +} + +FUNCTION {new.sentence.checka} +{ empty$ + 'skip$ + 'new.sentence + if$ +} + +FUNCTION {new.sentence.checkb} +{ empty$ + swap$ empty$ + and + 'skip$ + 'new.sentence + if$ +} + +FUNCTION {field.or.null} +{ duplicate$ empty$ + { pop$ "" } + 'skip$ + if$ +} + +FUNCTION {emphasize} +{ duplicate$ empty$ + { pop$ "" } + { "{\em " swap$ * "}" * } + if$ +} + +INTEGERS { nameptr namesleft numnames } + +FUNCTION {format.names} +{ 's := + #1 'nameptr := + s num.names$ 'numnames := + numnames 'namesleft := + { namesleft #0 > } + { s nameptr "{f.~}{vv~}{ll}{, jj}" format.name$ 't := + nameptr #1 > + { namesleft #1 > + { ", " * t * } + { numnames #2 > + { "," * } + 'skip$ + if$ + t "others" = + { " et~al." * } + { " and " * t * } + if$ + } + if$ + } + 't + if$ + nameptr #1 + 'nameptr := + + namesleft #1 - 'namesleft := + } + while$ +} + +FUNCTION {format.authors} +{ author empty$ + { "" } + { author format.names } + if$ +} + +FUNCTION {format.editors} +{ editor empty$ + { "" } + { editor format.names + editor num.names$ #1 > + { ", editors" * } + { ", editor" * } + if$ + } + if$ +} + +FUNCTION {format.title} +{ title empty$ + { "" } + { title "t" change.case$ } + if$ +} + +FUNCTION {n.dashify} +{ 't := + "" + { t empty$ not } + { t #1 #1 substring$ "-" = + { t #1 #2 substring$ "--" = not + { "--" * + t #2 global.max$ substring$ 't := + } + { { t #1 #1 substring$ "-" = } + { "-" * + t #2 global.max$ substring$ 't := + } + while$ + } + if$ + } + { t #1 #1 substring$ * + t #2 global.max$ substring$ 't := + } + if$ + } + while$ +} + +FUNCTION {format.date} +{ year empty$ + { month empty$ + { "" } + { "there's a month but no year in " cite$ * warning$ + month + } + if$ + } + { month empty$ + 'year + { month " " * year * } + if$ + } + if$ +} + +FUNCTION {format.btitle} +{ title emphasize +} + +FUNCTION {tie.or.space.connect} +{ duplicate$ text.length$ #3 < + { "~" } + { " " } + if$ + swap$ * * +} + +FUNCTION {either.or.check} +{ empty$ + 'pop$ + { "can't use both " swap$ * " fields in " * cite$ * warning$ } + if$ +} + +FUNCTION {format.bvolume} +{ volume empty$ + { "" } + { "volume" volume tie.or.space.connect + series empty$ + 'skip$ + { " of " * series emphasize * } + if$ + "volume and number" number either.or.check + } + if$ +} + +FUNCTION {format.number.series} +{ volume empty$ + { number empty$ + { series field.or.null } + { output.state mid.sentence = + { "number" } + { "Number" } + if$ + number tie.or.space.connect + series empty$ + { "there's a number but no series in " cite$ * warning$ } + { " in " * series * } + if$ + } + if$ + } + { "" } + if$ +} + +FUNCTION {format.edition} +{ edition empty$ + { "" } + { output.state mid.sentence = + { edition "l" change.case$ " edition" * } + { edition "t" change.case$ " edition" * } + if$ + } + if$ +} + +INTEGERS { multiresult } + +FUNCTION {multi.page.check} +{ 't := + #0 'multiresult := + { multiresult not + t empty$ not + and + } + { t #1 #1 substring$ + duplicate$ "-" = + swap$ duplicate$ "," = + swap$ "+" = + or or + { #1 'multiresult := } + { t #2 global.max$ substring$ 't := } + if$ + } + while$ + multiresult +} + +FUNCTION {format.pages} +{ pages empty$ + { "" } + { pages multi.page.check + { "pages" pages n.dashify tie.or.space.connect } + { "page" pages tie.or.space.connect } + if$ + } + if$ +} + +FUNCTION {format.vol.num.pages} +{ volume field.or.null + number empty$ + 'skip$ + { "(" number * ")" * * + volume empty$ + { "there's a number but no volume in " cite$ * warning$ } + 'skip$ + if$ + } + if$ + pages empty$ + 'skip$ + { duplicate$ empty$ + { pop$ format.pages } + { ":" * pages n.dashify * } + if$ + } + if$ +} + +FUNCTION {format.chapter.pages} +{ chapter empty$ + 'format.pages + { type empty$ + { "chapter" } + { type "l" change.case$ } + if$ + chapter tie.or.space.connect + pages empty$ + 'skip$ + { ", " * format.pages * } + if$ + } + if$ +} + +FUNCTION {format.in.ed.booktitle} +{ booktitle empty$ + { "" } + { editor empty$ + { "In " booktitle emphasize * } + { "In " format.editors * ", " * booktitle emphasize * } + if$ + } + if$ +} + +FUNCTION {empty.misc.check} + +{ author empty$ title empty$ howpublished empty$ + month empty$ year empty$ note empty$ + and and and and and + key empty$ not and + { "all relevant fields are empty in " cite$ * warning$ } + 'skip$ + if$ +} + +FUNCTION {format.thesis.type} +{ type empty$ + 'skip$ + { pop$ + type "t" change.case$ + } + if$ +} + +FUNCTION {format.tr.number} +{ type empty$ + { "Technical Report" } + 'type + if$ + number empty$ + { "t" change.case$ } + { number tie.or.space.connect } + if$ +} + +FUNCTION {format.article.crossref} +{ key empty$ + { journal empty$ + { "need key or journal for " cite$ * " to crossref " * crossref * + warning$ + "" + } + { "In {\em " journal * "\/}" * } + if$ + } + { "In " key * } + if$ + " \cite{" * crossref * "}" * +} + +FUNCTION {format.crossref.editor} +{ editor #1 "{vv~}{ll}" format.name$ + editor num.names$ duplicate$ + #2 > + { pop$ " et~al." * } + { #2 < + 'skip$ + { editor #2 "{ff }{vv }{ll}{ jj}" format.name$ "others" = + { " et~al." * } + { " and " * editor #2 "{vv~}{ll}" format.name$ * } + if$ + } + if$ + } + if$ +} + +FUNCTION {format.book.crossref} +{ volume empty$ + { "empty volume in " cite$ * "'s crossref of " * crossref * warning$ + "In " + } + { "Volume" volume tie.or.space.connect + " of " * + } + if$ + editor empty$ + editor field.or.null author field.or.null = + or + { key empty$ + { series empty$ + { "need editor, key, or series for " cite$ * " to crossref " * + crossref * warning$ + "" * + } + { "{\em " * series * "\/}" * } + if$ + } + { key * } + if$ + } + { format.crossref.editor * } + if$ + " \cite{" * crossref * "}" * +} + +FUNCTION {format.incoll.inproc.crossref} +{ editor empty$ + editor field.or.null author field.or.null = + or + { key empty$ + { booktitle empty$ + { "need editor, key, or booktitle for " cite$ * " to crossref " * + crossref * warning$ + "" + } + { "In {\em " booktitle * "\/}" * } + if$ + } + { "In " key * } + if$ + } + { "In " format.crossref.editor * } + if$ + " \cite{" * crossref * "}" * +} + +FUNCTION {article} +{ output.bibitem + format.authors "author" output.check + new.block + format.title "title" output.check + new.block + crossref missing$ + { journal emphasize "journal" output.check + format.vol.num.pages output + format.date "year" output.check + } + { format.article.crossref output.nonnull + format.pages output + } + if$ + new.block + note output + fin.entry +} + +FUNCTION {book} +{ output.bibitem + author empty$ + { format.editors "author and editor" output.check } + { format.authors output.nonnull + crossref missing$ + { "author and editor" editor either.or.check } + 'skip$ + if$ + } + if$ + new.block + format.btitle "title" output.check + crossref missing$ + { format.bvolume output + new.block + format.number.series output + new.sentence + publisher "publisher" output.check + address output + } + { new.block + format.book.crossref output.nonnull + } + if$ + format.edition output + format.date "year" output.check + new.block + note output + fin.entry +} + +FUNCTION {booklet} +{ output.bibitem + format.authors output + new.block + format.title "title" output.check + howpublished address new.block.checkb + howpublished output + address output + format.date output + new.block + note output + fin.entry +} + +FUNCTION {inbook} +{ output.bibitem + author empty$ + { format.editors "author and editor" output.check } + { format.authors output.nonnull + + crossref missing$ + { "author and editor" editor either.or.check } + 'skip$ + if$ + } + if$ + new.block + format.btitle "title" output.check + crossref missing$ + { format.bvolume output + format.chapter.pages "chapter and pages" output.check + new.block + format.number.series output + new.sentence + publisher "publisher" output.check + address output + } + { format.chapter.pages "chapter and pages" output.check + new.block + format.book.crossref output.nonnull + } + if$ + format.edition output + format.date "year" output.check + new.block + note output + fin.entry +} + +FUNCTION {incollection} +{ output.bibitem + format.authors "author" output.check + new.block + format.title "title" output.check + new.block + crossref missing$ + { format.in.ed.booktitle "booktitle" output.check + format.bvolume output + format.number.series output + format.chapter.pages output + new.sentence + publisher "publisher" output.check + address output + format.edition output + format.date "year" output.check + } + { format.incoll.inproc.crossref output.nonnull + format.chapter.pages output + } + if$ + new.block + note output + fin.entry +} + +FUNCTION {inproceedings} +{ output.bibitem + format.authors "author" output.check + new.block + format.title "title" output.check + new.block + crossref missing$ + { format.in.ed.booktitle "booktitle" output.check + format.bvolume output + format.number.series output + format.pages output + address empty$ + { organization publisher new.sentence.checkb + organization output + publisher output + format.date "year" output.check + } + { address output.nonnull + format.date "year" output.check + new.sentence + organization output + publisher output + } + if$ + } + { format.incoll.inproc.crossref output.nonnull + format.pages output + } + if$ + new.block + note output + fin.entry +} + +FUNCTION {conference} { inproceedings } + +FUNCTION {manual} +{ output.bibitem + author empty$ + { organization empty$ + 'skip$ + { organization output.nonnull + address output + } + if$ + } + { format.authors output.nonnull } + if$ + new.block + format.btitle "title" output.check + author empty$ + { organization empty$ + { address new.block.checka + address output + } + 'skip$ + if$ + } + { organization address new.block.checkb + organization output + address output + } + if$ + format.edition output + format.date output + new.block + note output + fin.entry +} + +FUNCTION {mastersthesis} +{ output.bibitem + format.authors "author" output.check + new.block + format.title "title" output.check + new.block + "Master's thesis" format.thesis.type output.nonnull + school "school" output.check + address output + format.date "year" output.check + new.block + note output + fin.entry +} + +FUNCTION {misc} +{ output.bibitem + format.authors output + title howpublished new.block.checkb + format.title output + howpublished new.block.checka + howpublished output + format.date output + new.block + note output + fin.entry + empty.misc.check +} + +FUNCTION {phdthesis} +{ output.bibitem + format.authors "author" output.check + new.block + format.btitle "title" output.check + new.block + "PhD thesis" format.thesis.type output.nonnull + school "school" output.check + address output + format.date "year" output.check + new.block + note output + fin.entry +} + +FUNCTION {proceedings} +{ output.bibitem + editor empty$ + { organization output } + { format.editors output.nonnull } + + if$ + new.block + format.btitle "title" output.check + format.bvolume output + format.number.series output + address empty$ + { editor empty$ + { publisher new.sentence.checka } + { organization publisher new.sentence.checkb + organization output + } + if$ + publisher output + format.date "year" output.check + } + { address output.nonnull + format.date "year" output.check + new.sentence + editor empty$ + 'skip$ + { organization output } + if$ + publisher output + } + if$ + new.block + note output + fin.entry +} + +FUNCTION {techreport} +{ output.bibitem + format.authors "author" output.check + new.block + format.title "title" output.check + new.block + format.tr.number output.nonnull + institution "institution" output.check + address output + format.date "year" output.check + new.block + note output + fin.entry +} + +FUNCTION {unpublished} +{ output.bibitem + format.authors "author" output.check + new.block + format.title "title" output.check + new.block + note "note" output.check + format.date output + fin.entry +} + +FUNCTION {default.type} { misc } + +MACRO {jan} {"Jan."} + +MACRO {feb} {"Feb."} + +MACRO {mar} {"Mar."} + +MACRO {apr} {"Apr."} + +MACRO {may} {"May"} + +MACRO {jun} {"June"} + +MACRO {jul} {"July"} + +MACRO {aug} {"Aug."} + +MACRO {sep} {"Sept."} + +MACRO {oct} {"Oct."} + +MACRO {nov} {"Nov."} + +MACRO {dec} {"Dec."} + +MACRO {acmcs} {"ACM Comput. Surv."} + +MACRO {acta} {"Acta Inf."} + +MACRO {cacm} {"Commun. ACM"} + +MACRO {ibmjrd} {"IBM J. Res. Dev."} + +MACRO {ibmsj} {"IBM Syst.~J."} + +MACRO {ieeese} {"IEEE Trans. Softw. Eng."} + +MACRO {ieeetc} {"IEEE Trans. Comput."} + +MACRO {ieeetcad} + {"IEEE Trans. Comput.-Aided Design Integrated Circuits"} + +MACRO {ipl} {"Inf. Process. Lett."} + +MACRO {jacm} {"J.~ACM"} + +MACRO {jcss} {"J.~Comput. Syst. Sci."} + +MACRO {scp} {"Sci. Comput. Programming"} + +MACRO {sicomp} {"SIAM J. Comput."} + +MACRO {tocs} {"ACM Trans. Comput. Syst."} + +MACRO {tods} {"ACM Trans. Database Syst."} + +MACRO {tog} {"ACM Trans. Gr."} + +MACRO {toms} {"ACM Trans. Math. Softw."} + +MACRO {toois} {"ACM Trans. Office Inf. Syst."} + +MACRO {toplas} {"ACM Trans. Prog. Lang. Syst."} + +MACRO {tcs} {"Theoretical Comput. Sci."} + +READ + +FUNCTION {sortify} +{ purify$ + "l" change.case$ +} + +INTEGERS { len } + +FUNCTION {chop.word} +{ 's := + 'len := + s #1 len substring$ = + { s len #1 + global.max$ substring$ } + 's + if$ +} + +FUNCTION {sort.format.names} +{ 's := + #1 'nameptr := + "" + s num.names$ 'numnames := + numnames 'namesleft := + { namesleft #0 > } + { nameptr #1 > + { " " * } + 'skip$ + if$ + s nameptr "{vv{ } }{ll{ }}{ f{ }}{ jj{ }}" format.name$ 't := + nameptr numnames = t "others" = and + { "et al" * } + { t sortify * } + if$ + nameptr #1 + 'nameptr := + namesleft #1 - 'namesleft := + } + while$ +} + +FUNCTION {sort.format.title} +{ 't := + "A " #2 + "An " #3 + "The " #4 t chop.word + chop.word + chop.word + sortify + #1 global.max$ substring$ +} + +FUNCTION {author.sort} +{ author empty$ + { key empty$ + { "to sort, need author or key in " cite$ * warning$ + "" + } + { key sortify } + if$ + } + { author sort.format.names } + if$ +} + +FUNCTION {author.editor.sort} +{ author empty$ + { editor empty$ + { key empty$ + { "to sort, need author, editor, or key in " cite$ * warning$ + "" + } + { key sortify } + if$ + } + { editor sort.format.names } + if$ + } + { author sort.format.names } + if$ +} + +FUNCTION {author.organization.sort} +{ author empty$ + + { organization empty$ + { key empty$ + { "to sort, need author, organization, or key in " cite$ * warning$ + "" + } + { key sortify } + if$ + } + { "The " #4 organization chop.word sortify } + if$ + } + { author sort.format.names } + if$ +} + +FUNCTION {editor.organization.sort} +{ editor empty$ + { organization empty$ + { key empty$ + { "to sort, need editor, organization, or key in " cite$ * warning$ + "" + } + { key sortify } + if$ + } + { "The " #4 organization chop.word sortify } + if$ + } + { editor sort.format.names } + if$ +} + +FUNCTION {presort} +{ type$ "book" = + type$ "inbook" = + or + 'author.editor.sort + { type$ "proceedings" = + 'editor.organization.sort + { type$ "manual" = + 'author.organization.sort + 'author.sort + if$ + } + if$ + } + if$ + " " + * + year field.or.null sortify + * + " " + * + title field.or.null + sort.format.title + * + #1 entry.max$ substring$ + 'sort.key$ := +} + +ITERATE {presort} + +SORT + +STRINGS { longest.label } + +INTEGERS { number.label longest.label.width } + +FUNCTION {initialize.longest.label} +{ "" 'longest.label := + #1 'number.label := + #0 'longest.label.width := +} + +FUNCTION {longest.label.pass} +{ number.label int.to.str$ 'label := + number.label #1 + 'number.label := + label width$ longest.label.width > + { label 'longest.label := + label width$ 'longest.label.width := + } + 'skip$ + if$ +} + +EXECUTE {initialize.longest.label} + +ITERATE {longest.label.pass} + +FUNCTION {begin.bib} +{ preamble$ empty$ + 'skip$ + { preamble$ write$ newline$ } + if$ + "\begin{thebibliography}{" longest.label * + "}\setlength{\itemsep}{-1ex}\small" * write$ newline$ +} + +EXECUTE {begin.bib} + +EXECUTE {init.state.consts} + +ITERATE {call.type$} + +FUNCTION {end.bib} +{ newline$ + "\end{thebibliography}" write$ newline$ +} + +EXECUTE {end.bib} + +% end of file latex8.bst +% --------------------------------------------------------------- + + + diff --git a/spec/consensus/consensus-paper/latex8.sty b/spec/consensus/consensus-paper/latex8.sty new file mode 100644 index 000000000..1e6b0dc7e --- /dev/null +++ b/spec/consensus/consensus-paper/latex8.sty @@ -0,0 +1,168 @@ +% --------------------------------------------------------------- +% +% $Id: latex8.sty,v 1.2 1995/09/15 15:31:13 ienne Exp $ +% +% by Paolo.Ienne@di.epfl.ch +% +% --------------------------------------------------------------- +% +% no guarantee is given that the format corresponds perfectly to +% IEEE 8.5" x 11" Proceedings, but most features should be ok. +% +% --------------------------------------------------------------- +% with LaTeX2e: +% ============= +% +% use as +% \documentclass[times,10pt,twocolumn]{article} +% \usepackage{latex8} +% \usepackage{times} +% +% --------------------------------------------------------------- + +% with LaTeX 2.09: +% ================ +% +% use as +% \documentstyle[times,art10,twocolumn,latex8]{article} +% +% --------------------------------------------------------------- +% with both versions: +% =================== +% +% specify \pagestyle{empty} to omit page numbers in the final +% version +% +% specify references as +% \bibliographystyle{latex8} +% \bibliography{...your files...} +% +% use Section{} and SubSection{} instead of standard section{} +% and subsection{} to obtain headings in the form +% "1.3. My heading" +% +% --------------------------------------------------------------- + +\typeout{IEEE 8.5 x 11-Inch Proceedings Style `latex8.sty'.} + +% ten point helvetica bold required for captions +% in some sites the name of the helvetica bold font may differ, +% change the name here: +\font\tenhv = phvb at 10pt +%\font\tenhv = phvb7t at 10pt + +% eleven point times bold required for second-order headings +% \font\elvbf = cmbx10 scaled 1100 +\font\elvbf = ptmb scaled 1100 + +% set dimensions of columns, gap between columns, and paragraph indent +\setlength{\textheight}{8.875in} +\setlength{\textwidth}{6.875in} +\setlength{\columnsep}{0.3125in} +\setlength{\topmargin}{0in} +\setlength{\headheight}{0in} +\setlength{\headsep}{0in} +\setlength{\parindent}{1pc} +\setlength{\oddsidemargin}{-.304in} +\setlength{\evensidemargin}{-.304in} + +% memento from size10.clo +% \normalsize{\@setfontsize\normalsize\@xpt\@xiipt} +% \small{\@setfontsize\small\@ixpt{11}} +% \footnotesize{\@setfontsize\footnotesize\@viiipt{9.5}} +% \scriptsize{\@setfontsize\scriptsize\@viipt\@viiipt} +% \tiny{\@setfontsize\tiny\@vpt\@vipt} +% \large{\@setfontsize\large\@xiipt{14}} +% \Large{\@setfontsize\Large\@xivpt{18}} +% \LARGE{\@setfontsize\LARGE\@xviipt{22}} +% \huge{\@setfontsize\huge\@xxpt{25}} +% \Huge{\@setfontsize\Huge\@xxvpt{30}} + +\def\@maketitle + { + \newpage + \null + \vskip .375in + \begin{center} + {\Large \bf \@title \par} + % additional two empty lines at the end of the title + \vspace*{24pt} + { + \large + \lineskip .5em + \begin{tabular}[t]{c} + \@author + \end{tabular} + \par + } + % additional small space at the end of the author name + \vskip .5em + { + \large + \begin{tabular}[t]{c} + \@affiliation + \end{tabular} + \par + \ifx \@empty \@email + \else + \begin{tabular}{r@{~}l} + E-mail: & {\tt \@email} + \end{tabular} + \par + \fi + } + % additional empty line at the end of the title block + \vspace*{12pt} + \end{center} + } + +\def\abstract + {% + \centerline{\large\bf Abstract}% + \vspace*{12pt}% + \it% + } + +\def\endabstract + { + % additional empty line at the end of the abstract + \vspace*{12pt} + } + +\def\affiliation#1{\gdef\@affiliation{#1}} \gdef\@affiliation{} + +\def\email#1{\gdef\@email{#1}} +\gdef\@email{} + +\newlength{\@ctmp} +\newlength{\@figindent} +\setlength{\@figindent}{1pc} + +\long\def\@makecaption#1#2{ + \vskip 10pt + \setbox\@tempboxa\hbox{\tenhv\noindent #1.~#2} + \setlength{\@ctmp}{\hsize} + \addtolength{\@ctmp}{-\@figindent}\addtolength{\@ctmp}{-\@figindent} + % IF longer than one indented paragraph line + \ifdim \wd\@tempboxa >\@ctmp + % THEN set as an indented paragraph + \begin{list}{}{\leftmargin\@figindent \rightmargin\leftmargin} + \item[]\tenhv #1.~#2\par + \end{list} + \else + % ELSE center + \hbox to\hsize{\hfil\box\@tempboxa\hfil} + \fi} + +% correct heading spacing and type +\def\section{\@startsection {section}{1}{\z@} + {14pt plus 2pt minus 2pt}{14pt plus 2pt minus 2pt} {\large\bf}} +\def\subsection{\@startsection {subsection}{2}{\z@} + {13pt plus 2pt minus 2pt}{13pt plus 2pt minus 2pt} {\elvbf}} + +% add the period after section numbers +\newcommand{\Section}[1]{\section{\hskip -1em.~#1}} +\newcommand{\SubSection}[1]{\subsection{\hskip -1em.~#1}} + +% end of file latex8.sty +% --------------------------------------------------------------- diff --git a/spec/consensus/consensus-paper/lit.bib b/spec/consensus/consensus-paper/lit.bib new file mode 100644 index 000000000..4abc83e70 --- /dev/null +++ b/spec/consensus/consensus-paper/lit.bib @@ -0,0 +1,1659 @@ +%--- conferences -------------------------------------------------- +@STRING{WDAG96 = "Proceedings of the 10th International Workshop + on Distributed Algorithms (WDAG'96)"} +@STRING{WDAG97 = "Proceedings of the 11th International Workshop + on Distributed Algorithms (WDAG'97)"} +@STRING{DISC98 = "Proceedings of the 12th International Conference + on Distributed Computing ({DISC}'98)"} +@STRING{DISC99 = "Proceedings of the 13th International Conference + on Distributed Computing ({DISC}'99)"} +@STRING{DISC98 = "Proceedings of the 13th International Conference + on Distributed Computing ({DISC}'98)"} +@STRING{DISC99 = "Proceedings of the 13th International Conference + on Distributed Computing ({DISC}'99)"} +@STRING{DISC00 = "Proceedings of the 14th International Conference + on Distributed Computing ({DISC}'00)"} +@STRING{DISC01 = "Proceedings of the 15th International Conference + on Distributed Computing ({DISC}'01)"} +@STRING{DISC02 = "Proceedings of the 16th International Conference + on Distributed Computing ({DISC}'02)"} +@STRING{DISC03 = "Proceedings of the 17th International Conference + on Distributed Computing ({DISC}'03)"} +@STRING{DISC04 = "Proceedings of the 18th International Conference + on Distributed Computing ({DISC}'04)"} +@STRING{DISC05 = "Proceedings of the 19th International Conference + on Distributed Computing ({DISC}'05)"} +@STRING{PODC83 = "Proceeding of the 1st Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'83)"} +@STRING{PODC91 = "Proceeding of the 9th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'91)"} +@STRING{PODC94 = "Proceeding of the 12th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'94)"} +@STRING{PODC95 = "Proceeding of the 13th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'95)"} +@STRING{PODC96 = "Proceeding of the 14th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'96)"} +@STRING{PODC97 = "Proceeding of the 15th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'97)"} +@STRING{PODC98 = "Proceeding of the 16th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'98)"} +@STRING{PODC99 = "Proceeding of the 17th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'99)"} +@STRING{PODC00 = "Proceeding of the 18th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'00)"} +@STRING{PODC01 = "Proceeding of the 19th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'01)"} +@STRING{PODC02 = "Proceeding of the 20th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'02)"} +@STRING{PODC03 = "Proceeding of the 21st Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'03)"} +@STRING{PODC03 = "Proceeding of the 22nd Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'03)"} +@STRING{PODC04 = "Proceeding of the 23rd Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'04)"} +@STRING{PODC05 = "Proceeding of the 24th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'05)"} +@STRING{PODC06 = "Proceedings of the 25th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'06)"} +@STRING{PODC07 = "Proceedings of the 26th Annual {ACM} Symposium on + Principles of Distributed Computing ({PODC}'07)"} +@STRING{STOC91 = "Proceedings of the 23rd Annual {ACM} Symposium on + Theory of Computing ({STOC}'91)"} +@STRING{WSS01 = "Proceedings of the 5th International Workshop on + Self-Stabilizing Systems ({WSS} '01)"} +@STRING{SSS06 = "Proceedings of the 8th International Symposium on + Stabilization, Safety, and Security of Distributed + Systems ({SSS} '06)"} +@STRING{DSN00 = "Dependable Systems and Networks ({DSN} 2000)"} +@STRING{DSN05 = "Dependable Systems and Networks ({DSN} 2005)"} +@STRING{DSN06 = "Dependable Systems and Networks ({DSN} 2006)"} +@STRING{DSN07 = "Dependable Systems and Networks ({DSN} 2007)"} + +%--- journals ----------------------------------------------------- +@STRING{PPL = "Parallel Processing Letters"} +@STRING{IPL = "Information Processing Letters"} +@STRING{DC = "Distributed Computing"} +@STRING{JACM = "Journal of the ACM"} +@STRING{IC = "Information and Control"} +@STRING{TCS = "Theoretical Computer Science"} +@STRING{ACMTCS = "ACM Transactions on Computer Systems"} +@STRING{TDSC = "Transactions on Dependable and Secure Computing"} +@STRING{TPLS = "ACM Trans. Program. Lang. Syst."} + +%--- publisher ---------------------------------------------------- +@STRING{ACM = "ACM Press"} +@STRING{IEEE = "IEEE"} +@STRING{SPR = "Springer-Verlag"} + +%--- institution -------------------------------------------------- +@STRING{TUAuto = {Technische Universit\"at Wien, Department of + Automation}} +@STRING{TUECS = {Technische Universit\"at Wien, Embedded Computing + Systems Group}} + + +%------------------------------------------------------------------ +@article{ABND+90:jacm, + author = {Hagit Attiya and Amotz Bar-Noy and Danny Dolev and + David Peleg and R{\"u}diger Reischuk}, + title = {Renaming in an asynchronous environment}, + journal = JACM, + volume = {37}, + number = {3}, + year = {1990}, + pages = {524--548}, + publisher = ACM, + address = {New York, NY, USA}, +} + +@article{ABND95:jacm, + author = {Hagit Attiya and Amotz Bar-Noy and Danny Dolev}, + title = {Sharing memory robustly in message-passing systems}, + journal = JACM, + volume = {42}, + number = {1}, + year = {1995}, + pages = {124--142}, + publisher = ACM, + address = {New York, NY, USA}, +} + +@inproceedings{ACKM04:podc, + author = {Ittai Abraham and Gregory Chockler and Idit Keidar + and Dahlia Malkhi}, + title = {Byzantine disk paxos: optimal resilience with + byzantine shared memory.}, + booktitle = PODC04, + year = {2004}, + pages = {226-235} +} + +@article{ACKM05:dc, + author = {Ittai Abraham and Gregory Chockler and Idit Keidar + and Dahlia Malkhi}, + title = {Byzantine disk paxos: optimal resilience with + byzantine shared memory.}, + journal = DC, + volume = {18}, + number = {5}, + year = {2006}, + pages = {387-408} +} + +@article{ACT00:dc, + author = "Marcos Kawazoe Aguilera and Wei Chen and Sam Toueg", + title = "Failure Detection and Consensus in the + Crash-Recovery Model", + journal = DC, + year = 2000, + month = apr, + volume = 13, + number = 2, + pages = "99--125", + url = + "http://www.cs.cornell.edu/home/sam/FDpapers/crash-recovery-finaldcversion.ps" +} + +@article{ACT00:siam, + author = "Marcos Kawazoe Aguilera and Wei Chen and Sam Toueg", + title = "On quiescent reliable communication", + journal = "SIAM Journal of Computing", + year = 2000, + volume = 29, + number = 6, + pages = "2040--2073", + month = apr +} + +@inproceedings{ACT97:wdag, + author = "Marcos Kawazoe Aguilera and Wei Chen and Sam Toueg", + title = "Heartbeat: A Timeout-Free Failure Detector for + Quiescent Reliable Communication", + booktitle = WDAG97, + year = 1997, + pages = "126--140", + url = + "http://simon.cs.cornell.edu/Info/People/weichen/research/mypapers/wdag97final.ps" +} + +@article{ACT98:disc, + author = "Marcos Kawazoe Aguilera and Wei Chen and Sam Toueg", + title = "Failure Detection and Consensus in the + Crash-Recovery Model", + journal = DISC98, + year = 1998, + pages = "231--245", + publisher = SPR +} + +@article{ACT99:tcs, + author = "Marcos Kawazoe Aguilera and Wei Chen and Sam Toueg", + title = "Using the Heartbeat Failure Detector for Quiescent + Reliable Communication and Consensus in + Partitionable Networks", + journal = "Theoretical Computer Science", + year = 1999, + month = jun, + volume = 220, + number = 1, + pages = "3--30", + url = + "http://www.cs.cornell.edu/home/sam/FDpapers/TCS98final.ps" +} + +@inproceedings{ADGF+04:ispdc, + author = {Anceaume, Emmanuelle and Delporte-Gallet, Carole and + Fauconnier, Hugues and Hurfin, Michel and Le Lann, + G{\'e}rard }, + title = {Designing Modular Services in the Scattered + Byzantine Failure Model.}, + booktitle = {ISPDC/HeteroPar}, + year = {2004}, + pages = {262-269} +} + +@inproceedings{ADGF+06:dsn, + author = {Marcos Kawazoe Aguilera and Carole Delporte-Gallet + and Hugues Fauconnier and Sam Toueg}, + title = {Consensus with Byzantine Failures and Little System + Synchrony.}, + booktitle = DSN06, + year = {2006}, + pages = {147-155} +} + +@inproceedings{ADGFT01:disc, + author = "Marcos Kawazoe Aguilera and Carole Delporte-Gallet + and Hugues Fauconnier and Sam Toueg", + title = "Stable Leader Election", + booktitle = DISC01, + year = 2001, + pages = "108--122", + publisher = SPR +} + +@inproceedings{ADGFT03:podc, + author = "Marcos K. Aguilera and Carole Delporte-Gallet and + Hugues Fauconnier and Sam Toueg", + title = "On implementing {O}mega with weak reliability and + synchrony assumptions", + booktitle = PODC03, + year = 2003, + publisher = ACM +} + +@inproceedings{ADGFT04:podc, + author = {Marcos K. Aguilera and Carole Delporte-Gallet and + Hugues Fauconnier and Sam Toueg}, + title = {Communication-efficient leader election and + consensus with limited link synchrony}, + booktitle = PODC04, + year = 2004, + pages = {328--337}, + address = {St. John's, Newfoundland, Canada}, + publisher = ACM +} + +@inproceedings{ADGFT06:dsn, + author = {Marcos Kawazoe Aguilera and Carole Delporte-Gallet + and Hugues Fauconnier and Sam Toueg}, + title = {Consensus with Byzantine Failures and Little System + Synchrony.}, + booktitle = DSN06, + year = 2006, + pages = {147-155}, + ee = + {http://doi.ieeecomputersociety.org/10.1109/DSN.2006.22}, + bibsource = {DBLP, http://dblp.uni-trier.de} +} + +@inproceedings{ADLS91:stoc, + author = "Hagit Attiya and Cynthia Dwork and Nancy A. Lynch + and Larry J. Stockmeyer", + title = "Bounds on the Time to Reach Agreement in the + Presence of Timing Uncertainty", + booktitle = STOC91, + year = 1991, + pages = "359--369", +} + +@article{AT99:ipl, + author = "Marcos Kawazoe Aguilera and Sam Toueg", + title = "A Simple Bivalency Proof that t -Resilient Consensus + Requires t + 1 Rounds", + journal = IPL, + volume = "71", + number = "3-4", + pages = "155--158", + year = "1999" +} + +@Book{AW04:book, + author = {Attiya, Hagit and Welch, Jennifer}, + title = {Distributed Computing}, + publisher = {John Wiley {\&} Sons}, + edition = {2nd}, + year = {2004} +} + +@Book{AW98:book, + author = {Hagit Attiya and Jennifer Welch}, + title = {Distributed Computing}, + publisher = {McGraw-Hill Publishing Company}, + year = {1998} +} + +@InBook{AW98:book:chap12, + author = {Hagit Attiya and Jennifer Welch}, + title = {Distributed Computing}, + publisher = {McGraw-Hill Publishing Company}, + year = {1998}, + chapter = {12, "Improving the fault-tolerance of algorithms"} +} + +@inproceedings{ABHMS11:disc, + author = {Hagit Attiya and + Fatemeh Borran and + Martin Hutle and + Zarko Milosevic and + Andr{\'e} Schiper}, + title = {Structured Derivation of Semi-Synchronous Algorithms}, + booktitle = {DISC}, + year = {2011}, + pages = {374-388} +} + +@inproceedings{BCBG+07:podc, + author = {Martin Biely and Bernadette Charron-Bost and Antoine + Gaillard and Martin Hutle and Andr{\'e} Schiper and + Josef Widder}, + title = {Tolerating Corrupted Communication}, + publisher = ACM, + booktitle = PODC07, + year = {2007} +} + +@InProceedings{BCBT96:wdag, + author = {Anindya Basu and Bernadette Charron-Bost and Sam + Toueg}, + title = {Simulating Reliable Links with Unreliable Links in + the Presence of Process Crashes}, + pages = {105--122}, + booktitle = {WDAG 1996}, + editor = {Babao{\u g}lu, {\"O}zalp}, + year = {1996}, + month = {Oct}, + volume = {1151}, + ISBN = {3-540-61769-8}, + pubisher = {Springer}, + series = {Lecture Notes in Computer Science}, +} + +@article{BDFG03:sigact, + author = "R. Boichat and P. Dutta and S. Frolund and + R. Guerraoui", + title = "Reconstructing {P}axos", + journal = "ACM SIGACT News", + year = "2003", + volume = "34", + number = "1", + pages = "47-67" +} + +@unpublished{BHR+06:note, + author = "Martin Biely and Martin Hutle and Sergio Rajsbaum + and Ulrich Schmid and Corentin Travers and Josef + Widder", + title = "Discussion note on moving timely links", + note = "Unpublished", + month = apr, + year = 2006 +} + +@article{BHRT03:jda, + author = {Roberto Baldoni and Jean-Michel H{\'e}lary and + Michel Raynal and L{\'e}naick Tanguy}, + title = {Consensus in Byzantine asynchronous systems.}, + journal = {J. Discrete Algorithms}, + volume = {1}, + number = {2}, + year = {2003}, + pages = {185-210}, + ee = {http://dx.doi.org/10.1016/S1570-8667(03)00025-X}, + bibsource = {DBLP, http://dblp.uni-trier.de} +} + +@unpublished{BHSS08:tdsc, + author = {Fatemeh Borran and Martin Hutle and Nuno Santos and + Andr{\'e} Schiper}, + title = {Solving Consensus with Communication Predicates: + A~Quantitative Approach}, + note = {Under submission}, + year = {2008} +} + +@inproceedings{Ben83:podc, + author = {Michael Ben-Or}, + title = {Another Advantage of Free Choice: Completely + Asynchronous Agreement Protocols}, + booktitle = {PODC}, + year = {1983}, +} + +@inproceedings{Bra04:podc, + author = {Bracha, Gabriel}, + title = {An asynchronous [(n - 1)/3]-resilient consensus protocol}, + booktitle = {PODC '84: Proceedings of the third annual ACM symposium on Principles of distributed computing}, + year = {1984}, + isbn = {0-89791-143-1}, + pages = {154--162}, + location = {Vancouver, British Columbia, Canada}, + doi = {http://doi.acm.org/10.1145/800222.806743}, + publisher = {ACM}, + address = {New York, NY, USA}, + } + + +@inproceedings{CBGS00:dsn, + author = "Bernadette Charron-Bost and Rachid Guerraoui and + Andr{\'{e}} Schiper", + title = "Synchronous System and Perfect Failure Detector: + {S}olvability and efficiency issues", + booktitle = DSN00, + publisher = "{IEEE} Computer Society", + address = "New York, {USA}", + pages = "523--532", + year = "2000" +} + +@inproceedings{CBS06:prdc, + author = {Bernadette Charron-Bost and Andr{\'e} Schiper}, + title = {Improving Fast Paxos: being optimistic with no + overhead}, + booktitle = {Pacific Rim Dependable Computing, Proceedings}, + year = {2006} +} + +@article{CBS09, + author = {B. Charron-Bost and A. Schiper}, + title = {The {H}eard-{O}f model: computing in distributed systems with benign failures}, + journal ={Distributed Computing}, + number = {1}, + volume = {22}, + pages = {49-71}, + year ={2009} + } + + +@article{CBS07:sigact, + author = {Bernadette Charron-Bost and Andr\'{e} Schiper}, + title = {Harmful dogmas in fault tolerant distributed + computing}, + journal = {SIGACT News}, + volume = {38}, + number = {1}, + year = {2007}, + pages = {53--61}, +} + +@techreport{CBS07:tr, + author = {Charron-Bost, Bernadette and Schiper, Andr{\'{e}}}, + title = {The Heard-Of Model: Unifying all Benign Failures}, + institution = {EPFL}, + year = 2007, + OPTnumber = {LSR-REPORT-2006-004} +} + +@article{CELT00:jacm, + author = {Soma Chaudhuri and Maurice Erlihy and Nancy A. Lynch + and Mark R. Tuttle}, + title = {Tight bounds for k-set agreement}, + journal = JACM, + volume = {47}, + number = {5}, + year = {2000}, + pages = {912--943}, + publisher = ACM, + address = {New York, NY, USA}, +} + +@article{CF99:tpds, + author = "Flaviu Cristian and Christof Fetzer", + title = "The Timed Asynchronous Distributed System Model", + journal = "IEEE Transactions on Parallel and Distributed + Systems", + volume = "10", + number = "6", + pages = "642--657", + year = "1999" +} + +@article{CHT96:jacm, + author = "Tushar Deepak Chandra and Vassos Hadzilacos and Sam + Toueg", + title = "The Weakest Failure Detector for Solving Consensus", + journal = {JACM}, + year = {1996}, +} + +@article{CL02:tcs, + author = {Miguel Castro and Barbara Liskov}, + title = {Practical byzantine fault tolerance and proactive + recovery}, + journal = {ACMTCS}, + year = {2002}, +} + +@inproceedings{CL99:osdi, + author = {Miguel Castro and Barbara Liskov}, + title = {Practical byzantine fault tolerance and proactive + recovery}, + booktitle = {Proceedings of the 3rd Symposium on Operating + Systems Design and Implementation}, + year = {1999}, + month = feb +} + +@inproceedings{CT91:podc, + author = {Tushar Deepak Chandra and Sam Toueg}, + title = {Unreliable Failure Detectors for Asynchronous + Systems (Preliminary Version)}, + booktitle = PODC91, + year = {1991}, + pages = {325-340} +} + +@article{CT96:jacm1, + author = "Tushar Deepak Chandra and Sam Toueg", + title = "Unreliable Failure Detectors for Reliable + Distributed Systems", + journal = {JACM}, + year = {1996}, +} + +@inproceedings{CTA00:dsn, + author = "Wei Chen and Sam Toueg and Marcos Kawazoe Aguilera", + title = "On the Quality of Service of Failure Detectors", + booktitle = "Proceedings IEEE International Conference on + Dependable Systems and Networks (DSN / FTCS'30)", + address = "New York City, USA", + year = 2000 +} + +@TechReport{DFKM96:tr, + author = {Danny Dolev and Roy Friedman and Idit Keidar and + Dahlia Malkhi}, + title = {Failure detectors in omission failure environments}, + institution = {Department of Computer Science, Cornell University}, + year = {1996}, + type = {Technical Report}, + number = {96-1608} +} + +@inproceedings{DG02:podc, + author = {Partha Dutta and Rachid Guerraoui}, + title = {The inherent price of indulgence}, + booktitle = PODC02, + year = 2002, + pages = {88--97}, + location = {Monterey, California}, + publisher = ACM, + address = {New York, NY, USA}, +} + +@inproceedings{DGFG+04:podc, + author = {Carole Delporte-Gallet and Hugues Fauconnier and + Rachid Guerraoui and Vassos Hadzilacos and Petr + Kouznetsov and Sam Toueg}, + title = {The weakest failure detectors to solve certain + fundamental problems in distributed computing}, + booktitle = PODC04, + year = 2004, + pages = {338--346}, + location = {St. John's, Newfoundland, Canada}, + publisher = ACM, + address = {New York, NY, USA} +} + +@inproceedings{DGL05:dsn, + author = {Partha Dutta and Rachid Guerraoui and Leslie + Lamport}, + title = {How Fast Can Eventual Synchrony Lead to Consensus?}, + booktitle = {Proceedings of the 2005 International Conference on + Dependable Systems and Networks (DSN'05)}, + pages = {22--27}, + year = {2005}, + address = {Los Alamitos, CA, USA} +} + +@article{DLS88:jacm, + author = "Cynthia Dwork and Nancy Lynch and Larry Stockmeyer", + title = "Consensus in the Presence of Partial Synchrony", + journal = {JACM}, + year = {1988}, +} + +@article{DPLL00:tcs, + author = "De Prisco, Roberto and Butler Lampson and Nancy + Lynch", + title = "Revisiting the {PAXOS} algorithm", + journal = TCS, + volume = "243", + number = "1--2", + pages = "35--91", + year = "2000" +} + +@techreport{DS97:tr, + author = {A. Doudou and A. Schiper}, + title = {Muteness Failure Detectors for Consensus with + {B}yzantine Processes}, + institution = {EPFL, Dept d'Informatique}, + year = {1997}, + type = {TR}, + month = {October}, + number = {97/230}, +} + +@inproceedings{DS98:podc, + author = {A. Doudou and A. Schiper}, + title = {Muteness Detectors for Consensus with {B}yzantine + Processes ({B}rief {A}nnouncement)}, + booktitle = {PODC}, + month = jul, + year = {1998} +} + +@article{DSU04:survey, + author = {D{\'e}fago, Xavier and Schiper, Andr{\'e} and Urb\'{a}n, P{\'e}ter}, + title = {Total order broadcast and multicast algorithms: Taxonomy and survey}, + journal = {ACM Comput. Surv.}, + issue_date = {December 2004}, + volume = {36}, + number = {4}, + month = dec, + year = {2004}, + issn = {0360-0300}, + pages = {372--421}, + numpages = {50}, + publisher = {ACM}, + address = {New York, NY, USA}, + keywords = {Distributed systems, agreement problems, atomic broadcast, atomic multicast, classification, distributed algorithms, fault-tolerance, global ordering, group communication, message passing, survey, taxonomy, total ordering}, +} + +@article{DeCandia07:dynamo, + author = {DeCandia, Giuseppe and Hastorun, Deniz and Jampani, Madan and Kakulapati, Gunavardhan and Lakshman, Avinash and Pilchin, Alex and Sivasubramanian, Swaminathan and Vosshall, Peter and Vogels, Werner}, + title = {Dynamo: amazon's highly available key-value store}, + journal = {SIGOPS Oper. Syst. Rev.}, + issue_date = {December 2007}, + volume = {41}, + number = {6}, + month = oct, + year = {2007}, + issn = {0163-5980}, + pages = {205--220}, + numpages = {16}, + publisher = {ACM}, + address = {New York, NY, USA}, + keywords = {performance, reliability, scalability}, +} + + +@book{Dol00:book, + author = {Shlomi Dolev}, + title = {Self-Stabilization}, + publisher = {The MIT Press}, + year = {2000} +} + +@inproceedings{FC95:podc, + author = "Christof Fetzer and Flaviu Cristian", + title = "Lower Bounds for Convergence Function Based Clock + Synchronization", + booktitle = PODC95, + year = 1995, + pages = "137--143" +} + +@article{FLP85:jacm, + author = "Michael J. Fischer and Nancy A. Lynch and + M. S. Paterson", + title = "Impossibility of Distributed Consensus with one + Faulty Process", + journal = {JACM}, + year = {1985}, +} + +@article{FMR05:tdsc, + author = {Roy Friedman and Achour Most{\'e}faoui and Michel + Raynal}, + title = {Simple and Efficient Oracle-Based Consensus + Protocols for Asynchronous Byzantine Systems.}, + journal = TDSC, + volume = {2}, + number = {1}, + year = {2005}, + pages = {46-56}, + ee = {http://dx.doi.org/10.1109/TDSC.2005.13}, + bibsource = {DBLP, http://dblp.uni-trier.de} +} + +@inproceedings{FS04:podc, + author = "Christof Fetzer and Ulrich Schmid", + title = "Brief announcement: on the possibility of consensus + in asynchronous systems with finite average response + times.", + booktitle = PODC04, + year = 2004, + pages = 402 +} + +@InProceedings{GL00:disc, + author = {Eli Gafni and Lesli Lamport}, + title = {Disk Paxos}, + booktitle = DISC00, + pages = {330--344}, + year = {2000}, +} + +@Article{GL03:dc, + author = {Eli Gafni and Lesli Lamport}, + title = {Disk Paxos}, + journal = DC, + year = 2003, + volume = {16}, + number = {1}, + pages = {1--20} +} + +@inproceedings{GP01:wss, + author = "Felix C. G{\"a}rtner and Stefan Pleisch", + title = "({I}m)Possibilities of Predicate Detection in + Crash-Affected Systems", + booktitle = WSS01, + year = 2001, + pages = "98--113" +} + +@inproceedings{GP02:disc, + author = "Felix C. G{\"a}rtner and Stefan Pleisch", + title = "Failure Detection Sequencers: Necessary and + Sufficient Information about Failures to Solve + Predicate Detection", + booktitle = DISC02, + year = 2002, + pages = "280--294" +} + +@inproceedings{GS96:wdag, + author = {Rachid Guerraoui and Andr{\'e} Schiper}, + title = {{``Gamma-Accurate''} Failure Detectors}, + booktitle = WDAG96, + year = {1996}, + pages = {269--286}, + publisher = SPR, + address = {London, UK} +} + +@inproceedings{Gaf98:podc, + author = {Eli Gafni}, + title = {Round-by-round fault detectors (extended abstract): + unifying synchrony and asynchrony}, + booktitle = PODC98, + year = {1998}, + pages = {143--152}, + address = {Puerto Vallarta, Mexico}, + publisher = ACM +} + +@incollection{Gra78:book, + author = {Jim N. Gray}, + title = {Notes on data base operating systems}, + booktitle = {Operating Systems: An Advanced Course}, + chapter = {3.F}, + publisher = {Springer}, + year = {1978}, + editor = {R. Bayer, R.M. Graham, G. Seegm\"uller}, + volume = {60}, + series = {Lecture Notes in Computer Science}, + address = {New York}, + pages = {465}, +} + +@InProceedings{HMR98:srds, + author = {Hurfin, M. and Mostefaoui, A. and Raynal, M.}, + title = {Consensus in asynchronous systems where processes + can crash and recover}, + booktitle = {Seventeenth IEEE Symposium on Reliable Distributed + Systems, Proceedings. }, + pages = { 280--286}, + year = {1998}, + address = {West Lafayette, IN}, + month = oct, + organization = {IEEE} +} + +@inproceedings{HMSZ06:sss, + author = "Martin Hutle and Dahlia Malkhi and Ulrich Schmid and + Lidong Zhou", + title = "Brief Announcement: Chasing the Weakest System Model + for Implementing {$\Omega$} and Consensus", + booktitle = SSS06, + year = 2006 +} + +@incollection{HT93:ds, + author = {Hadzilacos, Vassos and Toueg, Sam}, + title = {Fault-tolerant broadcasts and related problems}, + booktitle = {Distributed systems (2nd Ed.)}, + editor = {Mullender, Sape}, + year = {1993}, + isbn = {0-201-62427-3}, + pages = {97--145}, + numpages = {49} +} + + +@inproceedings{HS06:opodis, + author = {Heinrich Moser and Ulrich Schmid}, + title = {Optimal Clock Synchronization Revisited: Upper and + Lower Bounds in Real-Time Systems}, + booktitle = { Principles of Distributed Systems}, + pages = {94--109}, + year = {2006}, + volume = {4305}, + series = {Lecture Notes in Computer Science}, + publisher = SPR +} + +@techreport{HS06:tr, + author = {Martin Hutle and Andr{\'e} Schiper}, + title = { Communication predicates: A high-level abstraction + for coping with transient and dynamic faults}, + institution = {EPFL}, + number = { LSR-REPORT-2006-006 }, + year = {2006} +} + +@inproceedings{HS07:dsn, + author = {Martin Hutle and Andr{\'e} Schiper}, + title = { Communication predicates: A high-level abstraction + for coping with transient and dynamic faults}, + year = 2007, + booktitle = DSN07, + publisher = IEEE, + location = {Edinburgh,UK}, + pages = {92--10}, + month = jun +} + +@article{Her91:tpls, + author = {Maurice Herlihy}, + title = {Wait-free synchronization}, + journal = TPLS, + volume = {13}, + number = {1}, + year = {1991}, + pages = {124--149}, + publisher = ACM, + address = {New York, NY, USA}, +} + +@article{Kot09:zyzzyva, + author = {Kotla, Ramakrishna and Alvisi, Lorenzo and Dahlin, Mike and Clement, Allen and Wong, Edmund}, + title = {Zyzzyva: Speculative Byzantine fault tolerance}, + journal = {ACM Trans. Comput. Syst.}, + issue_date = {December 2009}, + volume = {27}, + number = {4}, + month = jan, + year = {2010}, + issn = {0734-2071}, + pages = {7:1--7:39}, + articleno = {7}, + numpages = {39}, + publisher = {ACM}, + address = {New York, NY, USA}, + keywords = {Byzantine fault tolerance, output commit, replication, speculative execution}, +} + + +@inproceedings{KMMS97:opodis, + author = "Kim Potter Kihlstrom and Louise E. Moser and + P. M. Melliar-Smith", + title = "Solving Consensus in a Byzantine Environment Using + an Unreliable Fault Detector", + booktitle = "Proceedings of the International Conference on + Principles of Distributed Systems (OPODIS)", + year = 1997, + month = dec, + address = "Chantilly, France", + pages = "61--75" +} + +@inproceedings{KS06:podc, + author = {Idit Keidar and Alexander Shraer}, + title = {Timeliness, failure-detectors, and consensus + performance}, + booktitle = PODC06, + year = {2006}, + pages = {169--178}, + location = {Denver, Colorado, USA}, + publisher = {ACM Press}, + address = {New York, NY, USA}, +} + +@InProceedings{LFA99:disc, + author = {Mikel Larrea and Antonio Fern\'andez and Sergio + Ar\'evalo}, + title = {Efficient algorithms to implement unreliable failure + detectors in partially synchronous systems}, + year = 1999, + month = sep, + pages = {34-48}, + series = "LNCS 1693", + booktitle = DISC99, + publisher = SPR, + address = {Bratislava, Slovaquia}, +} + +@article{LL84:ic, + author = "Jennifer Lundelius and Nancy A. Lynch", + title = "An Upper and Lower Bound for Clock Synchronization", + journal = IC, + volume = 62, + number = {2/3}, + year = 1984, + pages = {190--204} +} + +@techreport{LLS03:tr, + title = {How to Implement a Timer-free Perfect Failure + Detector in Partially Synchronous Systems}, + author = {Le Lann, G\'erard and Schmid, Ulrich}, + institution = TUAuto, + number = "183/1-127", + month = jan, + year = 2003 +} + +@article{LSP82:tpls, + author = {Leslie Lamport and Robert Shostak and Marshall + Pease}, + title = {The {B}yzantine Generals Problem}, + journal = {ACM Trans. Program. Lang. Syst.}, + year = {1982}, +} + +@inproceedings{Lam01:podc, + author = {Butler Lampson}, + title = {The ABCD's of Paxos}, + booktitle = {PODC}, + year = {2001}, + +} + +@inproceedings{Lam03:fddc, + author = {Leslie Lamport}, + title = {Lower Bounds for Asynchronous Consensus}, + booktitle = {Future Directions in Distributed Computing}, + pages = {22--23}, + year = {2003}, + editor = {Andr{\'e} Schiper and Alex A. Shvartsman and Hakim + Weatherspoon and Ben Y. Zhao}, + number = {2584}, + series = {Lecture Notes in Computer Science}, + publisher = SPR +} + +@techreport{Lam04:tr, + author = {Leslie Lamport}, + title = {Lower Bounds for Asynchronous Consensus}, + institution = {Microsoft Research}, + year = {2004}, + number = {MSR-TR-2004-72} +} + +@techreport{Lam05:tr, + author = {Leslie Lamport}, + title = {Fast Paxos}, + institution = {Microsoft Research}, + year = {2005}, + number = {MSR-TR-2005-12} +} + +@techreport{Lam05:tr-33, + author = {Leslie Lamport}, + title = {Generalized Consensus and Paxos}, + institution = {Microsoft Research}, + year = {2005}, + number = {MSR-TR-2005-33} +} + +@Misc{Lam06:slides, + author = {Leslie Lamport}, + title = {Byzantine Paxos}, + howpublished = {Unpublished slides}, + year = {2006} +} + +@Article{Lam86:dc, + author = {Lesli Lamport}, + title = {On Interprocess Communication--Part I: Basic + Formalism, Part II: Algorithms}, + journal = DC, + year = 1986, + volume = 1, + number = 2, + pages = {77--101} +} + +@Article {Lam98:tcs, + author = {Leslie Lamport}, + title = {The part-time parliament}, + journal = ACMTCS, + year = 1998, + volume = 16, + number = 2, + month = may, + pages = {133-169}, +} + +@book{Lyn96:book, + author = {Nancy Lynch}, + title = {Distributed Algorithms}, + publisher = {Morgan Kaufman}, + year = {1996}, +} + +@inproceedings{MA05:dsn, + author = {Martin, J.-P. and Alvisi, L. }, + title = {Fast Byzantine consensus}, + booktitle = DSN05, + pages = {402--411}, + year = {2005}, + month = jun, + organization = {IEEE}, +} + +@article{MA06:tdsc, + author = {Martin, J.-P. and Alvisi, L. }, + title = {Fast {B}yzantine Consensus}, + journal = {TDSC}, + year = {2006}, +} + +@InProceedings{MOZ05:dsn, + author = {Dahlia Malkhi and Florin Oprea and Lidong Zhou}, + title = {{$\Omega$} Meets Paxos: Leader Election and + Stability without Eventual Timely Links}, + booktitle = DSN05, + year = {2005} +} + +@inproceedings{MR00:podc, + author = "Achour Most{\'e}faoui and Michel Raynal", + title = "k-set agreement with limited accuracy failure + detectors", + booktitle = PODC00, + year = 2000, + pages = {143--152}, + location = {Portland, Oregon, United States}, + publisher = ACM +} + +@article{MR01:ppl, + author = "Achour Most{\'e}faoui and Michel Raynal", + title = "Leader-Based Consensus", + journal = PPL, + volume = 11, + number = 1, + year = 2001, + pages = {95--107} +} + +@techreport{OGS97:tr, + author = "Rui Oliveira and Rachid Guerraoui and {Andr\'e} + Schiper", + title = "Consensus in the crash-recover model", + number = "TR-97/239", + year = "1997" +} + +@article{PSL80:jacm, + author = {M. Pease and R. Shostak and L. Lamport}, + title = {Reaching Agreement in the Presence of Faults}, + journal = JACM, + volume = {27}, + number = {2}, + year = {1980}, + pages = {228--234}, + publisher = ACM, + address = ACMADDR, +} + +@article{ST87:jacm, + author = "T. K. Srikanth and Sam Toueg", + title = "Optimal clock synchronization", + journal = JACM, + volume = 34, + number = 3, + year = 1987, + pages = "626--645" +} + +@article{ST87:dc, + author = {T. K. Srikanth and Sam Toueg,}, + title = {Simulating authenticated broadcasts to derive simple fault-tolerant algorithms}, + journal = DC, + volume = {2}, + number = {2}, + year = {1987}, + pages = {80-94} +} + + +@inproceedings{SW89:stacs, + author = {Santoro, Nicola and Widmayer, Peter}, + title = {Time is not a healer}, + booktitle = {Proc.\ 6th Annual Symposium on Theor.\ Aspects of + Computer Science (STACS'89)}, + publisher = "Springer-Verlag", + series = {LNCS}, + volume = "349", + address = "Paderborn, Germany", + pages = "304-313", + year = "1989", + month = feb, +} + +@inproceedings{SW90:sigal, + author = {Nicola Santoro and Peter Widmayer}, + title = {Distributed Function Evaluation in the Presence of + Transmission Faults.}, + booktitle = {SIGAL International Symposium on Algorithms}, + year = {1990}, + pages = {358-367} +} + +@inproceedings{SWR02:icdcs, + author = {Ulrich Schmid and Bettina Weiss and John Rushby}, + title = {Formally Verified Byzantine Agreement in Presence of + Link Faults}, + booktitle = "22nd International Conference on Distributed + Computing Systems (ICDCS'02)", + year = 2002, + month = jul # " 2-5, ", + pages = "608--616", + address = "Vienna, Austria", +} + +@incollection{Sch93a:mullender, + Author = {F. B. Schneider}, + Title = {What Good are Models and What Models are Good}, + BookTitle = {Distributed Systems}, + Year = {1993}, + Editor = {Sape Mullender}, + Publisher = {ACM Press}, + Pages = {169-197}, +} + +@article{VL96:ic, + author = {George Varghese and Nancy A. Lynch}, + title = {A Tradeoff Between Safety and Liveness for + Randomized Coordinated Attack.}, + journal = {Inf. Comput.}, + volume = {128}, + number = {1}, + year = 1996, + pages = {57--71} +} + +@inproceedings{WGWB07:dsn, + title = {Synchronous Consensus with Mortal Byzantines}, + author = {Josef Widder and Günther Gridling and Bettina Weiss + and Jean-Paul Blanquart}, + year = {2007}, + booktitle = DSN07, + publisher = IEEE +} + +@inproceedings{Wid03:disc, + author = {Josef Widder}, + title = {Booting clock Synchronization in Partially + Synchronous Systems}, + booktitle = DISC03, + year = {2003}, + pages = {121--135} +} + +@techreport{Zie04:tr, + author = {Piotr Zieli{\'n}ski}, + title = {Paxos at War}, + institution = {University of Cambridge}, + year = {2004}, + number = {UCAM-CL-TR-593}, +} + +@article{Lam78:cacm, + author = {Leslie Lamport}, + title = {Time, clocks, and the ordering of events in a + distributed system}, + journal = {Commun. ACM}, + year = {1978}, +} + +@Article{Gue06:cj, + author = {Guerraoui, R. and Raynal, M.}, + journal = {The {C}omputer {J}ournal}, + title = {The {A}lpha of {I}ndulgent {C}onsensus}, + year = {2006} +} + +@Article{Gue03:toc, + affiliation = {EPFL}, + author = {Guerraoui, Rachid and Raynal, Michel}, + journal = {{IEEE} {T}rans. on {C}omputers}, + title = {The {I}nformation {S}tructure of {I}ndulgent {C}onsensus}, + year = {2004}, +} + +@techreport{Cas00, + author = {Castro, Miguel}, + title = {Practical {B}yzantine Fault-Tolerance. {PhD} thesis}, + institution = {MIT}, + year = 2000, +} + +@inproceedings{SongRSD08:icdcn, + author = {Yee Jiun Song and + Robbert van Renesse and + Fred B. Schneider and + Danny Dolev}, + title = {The Building Blocks of Consensus}, + booktitle = {ICDCN}, + year = {2008}, +} + + +@inproceedings{BS09:icdcn, + author = {Borran, Fatemeh and Schiper, Andr{\'e}}, + + title = {A {L}eader-free {B}yzantine {C}onsensus {A}lgorithm}, + note = {To appear in ICDCN, 2010}, +} + + +@inproceedings{MHS09:opodis, + author = {Zarko Milosevic and Martin Hutle and Andr{\'e} + Schiper}, + title = {Unifying {B}yzantine Consensus Algorithms with {W}eak + {I}nteractive {C}onsistency}, + note = {To appear in OPODIS 2009}, +} + +@inproceedings{MRR:dsn02, + author = {Most\'{e}faoui, Achour and Rajsbaum, Sergio and Raynal, Michel}, + title = {A Versatile and Modular Consensus Protocol}, + booktitle = {DSN}, + year = {2002}, + } + +@article{MR98:dc, + author = {Dahlia Malkhi and + Michael K. Reiter}, + title = {Byzantine Quorum Systems}, + journal = {Distributed Computing}, + year = {1998}, +} + +@inproceedings{Rei:ccs94, + author = {Reiter, Michael K.}, + title = {Secure agreement protocols: reliable and atomic group multicast in rampart}, + booktitle = {CCS}, + year = {1994}, + pages = {68--80}, + numpages = {13} +} + + +@techreport{RMS09-tr, + author = {Olivier R\"utti and Zarko Milosevic and Andr\'e Schiper}, + title = {{G}eneric construction of consensus algorithm for benign and {B}yzantine faults}, + institution = {EPFL-IC}, + number = {LSR-REPORT-2009-005}, + year = 2009, +} + +@inproceedings{Li:srds07, + author = {Li, Harry C. and Clement, Allen and Aiyer, Amitanand S. and Alvisi, Lorenzo}, + title = {The Paxos Register}, + booktitle = {SRDS}, + year = {2007}, + } + + @article{Amir11:prime, + author = {Amir, Yair and Coan, Brian and Kirsch, Jonathan and Lane, John}, + title = {Prime: Byzantine Replication under Attack}, + journal = {IEEE Trans. Dependable Secur. Comput.}, + issue_date = {July 2011}, + volume = {8}, + number = {4}, + month = jul, + year = {2011}, + issn = {1545-5971}, + pages = {564--577}, + numpages = {14}, + publisher = {IEEE Computer Society Press}, + address = {Los Alamitos, CA, USA}, + keywords = {Performance under attack, Byzantine fault tolerance, replicated state machines, distributed systems.}, +} + +@inproceedings{Mao08:mencius, + author = {Mao, Yanhua and Junqueira, Flavio P. and Marzullo, Keith}, + title = {Mencius: building efficient replicated state machines for WANs}, + booktitle = {OSDI}, + year = {2008}, + pages = {369--384}, + numpages = {16} +} + +@article{Sch90:survey, + author = {Schneider, Fred B.}, + title = {Implementing fault-tolerant services using the state machine approach: a tutorial}, + journal = {ACM Comput. Surv.}, + volume = {22}, + number = {4}, + month = dec, + year = {1990} +} + + +@techreport{HT94:TR, + author = {Hadzilacos, Vassos and Toueg, Sam}, + title = {A Modular Approach to Fault-Tolerant Broadcasts and Related Problems}, + year = {1994}, + source = {http://www.ncstrl.org:8900/ncstrl/servlet/search?formname=detail\&id=oai%3Ancstrlh%3Acornellcs%3ACORNELLCS%3ATR94-1425}, + publisher = {Cornell University}, + address = {Ithaca, NY, USA}, +} + +@inproceedings{Ver09:spinning, + author = {Veronese, Giuliana Santos and Correia, Miguel and Bessani, Alysson Neves and Lung, Lau Cheuk}, + title = {Spin One's Wheels? Byzantine Fault Tolerance with a Spinning Primary}, + booktitle = {SRDS}, + year = {2009}, + numpages = {10} +} + +@inproceedings{Cle09:aardvark, + author = {Clement, Allen and Wong, Edmund and Alvisi, Lorenzo and Dahlin, Mike and Marchetti, Mirco}, + title = {Making Byzantine fault tolerant systems tolerate Byzantine faults}, + booktitle = {NSDI}, + year = {2009}, + pages = {153--168}, + numpages = {16} +} + +@inproceedings{Aiyer05:barB, + author = {Aiyer, Amitanand S. and Alvisi, Lorenzo and Clement, Allen and Dahlin, Mike and Martin, Jean-Philippe and Porth, Carl}, + title = {BAR fault tolerance for cooperative services}, + booktitle = {SOSP}, + year = {2005}, + pages = {45--58}, + numpages = {14} +} + +@inproceedings{Cach01:crypto, + author = {Cachin, Christian and Kursawe, Klaus and Petzold, Frank and Shoup, Victor}, + title = {Secure and Efficient Asynchronous Broadcast Protocols}, + booktitle = {CRYPTO}, + year = {2001}, + pages = {524--541}, + numpages = {18} +} + +@article{Moniz11:ritas, + author = {Moniz, Henrique and Neves, Nuno Ferreria and Correia, Miguel and Verissimo, Paulo}, + title = {RITAS: Services for Randomized Intrusion Tolerance}, + journal = {IEEE Trans. Dependable Secur. Comput.}, + volume = {8}, + number = {1}, + month = jan, + year = {2011}, + pages = {122--136}, + numpages = {15} +} + +@inproceedings{MHS11:jabc, + author = {Milosevic, Zarko and Hutle, Martin and Schiper, Andre}, + title = {On the Reduction of Atomic Broadcast to Consensus with Byzantine Faults}, + booktitle = {SRDS}, + year = {2011}, + pages = {235--244}, + numpages = {10} +} + +@incollection{DHSZ03, + author={Driscoll, Kevin and Hall, Brendan and Sivencrona, Håkan and Zumsteg, Phil}, + title={Byzantine Fault Tolerance, from Theory to Reality}, + year={2003}, + booktitle={Computer Safety, Reliability, and Security}, + volume={2788}, + pages={235--248} +} + +@inproceedings{RMES:dsn07, + author = {Olivier R{\"u}tti and + Sergio Mena and + Richard Ekwall and + Andr{\'e} Schiper}, + title = {On the Cost of Modularity in Atomic Broadcast}, + booktitle = {DSN}, + year = {2007}, + pages = {635-644} +} + +@article{Ben:jc92, + author = {Charles H. Bennett and + Fran\c{c}ois Bessette and + Gilles Brassard and + Louis Salvail and + John A. Smolin}, + title = {Experimental Quantum Cryptography}, + journal = {J. Cryptology}, + volume = {5}, + number = {1}, + year = {1992}, + pages = {3-28} +} + +@inproceedings{Aiyer:disc08, + author = {Aiyer, Amitanand S. and Alvisi, Lorenzo and Bazzi, Rida A. and Clement, Allen}, + title = {Matrix Signatures: From MACs to Digital Signatures in Distributed Systems}, + booktitle = {DISC}, + year = {2008}, + pages = {16--31}, + numpages = {16} +} + +@inproceedings{Biel13:dsn, + author = {Biely, Martin and Delgado, Pamela and Milosevic, Zarko and Schiper, Andr{\'e}}, + title = {Distal: A Framework for Implementing Fault-tolerant Distributed Algorithms}, + note = {To appear in DSN, 2013}, + year = 2013 +} + +@inproceedings{BS10:icdcn, + author = {Borran, Fatemeh and Schiper, Andr{\'e}}, + title = {A leader-free Byzantine consensus algorithm}, + booktitle = {ICDCN}, + year = {2010}, + pages = {67--78}, + numpages = {12} +} + +@article{Cor06:cj, + author = {Correia, Miguel and Neves, Nuno Ferreira and Ver\'{\i}ssimo, Paulo}, + title = {From Consensus to Atomic Broadcast: Time-Free Byzantine-Resistant Protocols without Signatures}, + journal = {Comput. J.}, + volume = {49}, + number = {1}, + year = {2006}, + pages = {82--96}, + numpages = {15} +} + +@inproceedings{RMS10:dsn, + author = {Olivier R{\"u}tti and + Zarko Milosevic and + Andr{\'e} Schiper}, + title = {Generic construction of consensus algorithms for benign + and Byzantine faults}, + booktitle = {DSN}, + year = {2010}, + pages = {343-352} +} + + + +@inproceedings{HKJR:usenix10, + author = {Hunt, Patrick and Konar, Mahadev and Junqueira, Flavio P. and Reed, Benjamin}, + title = {ZooKeeper: wait-free coordination for internet-scale systems}, + OPTbooktitle = {Proceedings of the 2010 USENIX conference on USENIX annual technical conference}, + booktitle = {USENIXATC}, + year = {2010}, + OPTlocation = {Boston, MA}, + pages = {11}, + numpages = {1}, + OPTurl = {http://dl.acm.org/citation.cfm?id=1855840.1855851}, + acmid = {1855851}, + OPTpublisher = {USENIX Association}, + OPTaddress = {Berkeley, CA, USA}, +} + +@inproceedings{Bur:osdi06, + author = {Burrows, Mike}, + title = {The Chubby lock service for loosely-coupled distributed systems}, + booktitle = {OSDI}, + year = {2006}, + pages = {335--350}, + numpages = {16}, +} + +@INPROCEEDINGS{Mao09:hotdep, + author = {Yanhua Mao and Flavio P. Junqueira and Keith Marzullo}, + title = {Towards low latency state machine replication for uncivil wide-area networks}, + booktitle = {HotDep}, + year = {2009} +} + +@inproceedings{Chun07:a2m, + author = {Chun, Byung-Gon and Maniatis, Petros and Shenker, Scott and Kubiatowicz, John}, + title = {Attested append-only memory: making adversaries stick to their word}, + booktitle = {SOSP}, + year = {2007}, + pages = {189--204}, + numpages = {16} +} + +@TECHREPORT{MBS:epfltr, + author = {Zarko Milosevic and Martin Biely and Andr\'e Schiper}, + title = {Bounded {D}elay in {B}yzantine {T}olerant {S}tate {M}achine {R}eplication}, + year = 2013, + month = april, + institution = {EPFL}, + number = {185962}, +} + +@book{BH09:datacenter, + author = {Barroso, Luiz Andre and Hoelzle, Urs}, + title = {The Datacenter as a Computer: An Introduction to the Design of Warehouse-Scale Machines}, + year = {2009}, + isbn = {159829556X, 9781598295566}, + edition = {1st}, + publisher = {Morgan and Claypool Publishers}, +} + +@inproceedings{Kir11:csiirw, + author = {Kirsch, Jonathan and Goose, Stuart and Amir, Yair and Skare, Paul}, + title = {Toward survivable SCADA}, + booktitle = {CSIIRW}, + year = {2011}, + pages = {21:1--21:1}, + articleno = {21}, + numpages = {1} +} + +@inproceedings{Ongaro14:raft, + author = {Ongaro, Diego and Ousterhout, John}, + title = {In Search of an Understandable Consensus Algorithm}, + booktitle = {Proceedings of the 2014 USENIX Conference on USENIX Annual Technical Conference}, + series = {USENIX ATC'14}, + year = {2014}, + isbn = {978-1-931971-10-2}, + location = {Philadelphia, PA}, + pages = {305--320}, + numpages = {16}, + url = {http://dl.acm.org/citation.cfm?id=2643634.2643666}, + acmid = {2643666}, + publisher = {USENIX Association}, + address = {Berkeley, CA, USA}, +} + +@article{GLR17:red-belly-bc, + author = {Tyler Crain and + Vincent Gramoli and + Mikel Larrea and + Michel Raynal}, + title = {Leader/Randomization/Signature-free Byzantine Consensus for Consortium + Blockchains}, + journal = {CoRR}, + volume = {abs/1702.03068}, + year = {2017}, + url = {http://arxiv.org/abs/1702.03068}, + archivePrefix = {arXiv}, + eprint = {1702.03068}, + timestamp = {Wed, 07 Jun 2017 14:41:08 +0200}, + biburl = {http://dblp.org/rec/bib/journals/corr/CrainGLR17}, + bibsource = {dblp computer science bibliography, http://dblp.org} +} + + +@misc{Nak2012:bitcoin, + added-at = {2014-04-17T08:33:06.000+0200}, + author = {Nakamoto, Satoshi}, + biburl = {https://www.bibsonomy.org/bibtex/23db66df0fc9fa2b5033f096a901f1c36/ngnn}, + interhash = {423c2cdff70ba0cd0bca55ebb164d770}, + intrahash = {3db66df0fc9fa2b5033f096a901f1c36}, + keywords = {imported}, + timestamp = {2014-04-17T08:33:06.000+0200}, + title = {Bitcoin: A peer-to-peer electronic cash system}, + url = {http://www.bitcoin.org/bitcoin.pdf}, + year = 2009 +} + +@misc{But2014:ethereum, + author = {Vitalik Buterin}, + title = {Ethereum: A next-generation smart contract and decentralized application platform}, + year = {2014}, + howpublished = {\url{https://github.com/ethereum/wiki/wiki/White-Paper}}, + note = {Accessed: 2018-07-11}, + url = {https://github.com/ethereum/wiki/wiki/White-Paper}, +} + +@inproceedings{Dem1987:gossip, + author = {Demers, Alan and Greene, Dan and Hauser, Carl and Irish, Wes and Larson, John and Shenker, Scott and Sturgis, Howard and Swinehart, Dan and Terry, Doug}, + title = {Epidemic Algorithms for Replicated Database Maintenance}, + booktitle = {Proceedings of the Sixth Annual ACM Symposium on Principles of Distributed Computing}, + series = {PODC '87}, + year = {1987}, + isbn = {0-89791-239-X}, + location = {Vancouver, British Columbia, Canada}, + pages = {1--12}, + numpages = {12}, + url = {http://doi.acm.org/10.1145/41840.41841}, + doi = {10.1145/41840.41841}, + acmid = {41841}, + publisher = {ACM}, + address = {New York, NY, USA}, +} + +@article{Gue2018:sbft, + author = {Guy Golan{-}Gueta and + Ittai Abraham and + Shelly Grossman and + Dahlia Malkhi and + Benny Pinkas and + Michael K. Reiter and + Dragos{-}Adrian Seredinschi and + Orr Tamir and + Alin Tomescu}, + title = {{SBFT:} a Scalable Decentralized Trust Infrastructure for Blockchains}, + journal = {CoRR}, + volume = {abs/1804.01626}, + year = {2018}, + url = {http://arxiv.org/abs/1804.01626}, + archivePrefix = {arXiv}, + eprint = {1804.01626}, + timestamp = {Tue, 01 May 2018 19:46:29 +0200}, + biburl = {https://dblp.org/rec/bib/journals/corr/abs-1804-01626}, + bibsource = {dblp computer science bibliography, https://dblp.org} +} + +@inproceedings{BLS2001:crypto, + author = {Boneh, Dan and Lynn, Ben and Shacham, Hovav}, + title = {Short Signatures from the Weil Pairing}, + booktitle = {Proceedings of the 7th International Conference on the Theory and Application of Cryptology and Information Security: Advances in Cryptology}, + series = {ASIACRYPT '01}, + year = {2001}, + isbn = {3-540-42987-5}, + pages = {514--532}, + numpages = {19}, + url = {http://dl.acm.org/citation.cfm?id=647097.717005}, + acmid = {717005}, + publisher = {Springer-Verlag}, + address = {Berlin, Heidelberg}, +} + + diff --git a/spec/consensus/consensus-paper/paper.tex b/spec/consensus/consensus-paper/paper.tex new file mode 100644 index 000000000..22f8b405f --- /dev/null +++ b/spec/consensus/consensus-paper/paper.tex @@ -0,0 +1,153 @@ +%\documentclass[conference]{IEEEtran} +\documentclass[conference,onecolumn,draft,a4paper]{IEEEtran} +% Add the compsoc option for Computer Society conferences. +% +% If IEEEtran.cls has not been installed into the LaTeX system files, +% manually specify the path to it like: +% \documentclass[conference]{../sty/IEEEtran} + + + +% *** GRAPHICS RELATED PACKAGES *** +% +\ifCLASSINFOpdf +\else +\fi + +% correct bad hyphenation here +\hyphenation{op-tical net-works semi-conduc-tor} + +%\usepackage[caption=false,font=footnotesize]{subfig} +\usepackage{tikz} +\usetikzlibrary{decorations,shapes,backgrounds,calc} +\tikzstyle{msg}=[->,black,>=latex] +\tikzstyle{rubber}=[|<->|] +\tikzstyle{announce}=[draw=blue,fill=blue,shape=diamond,right,minimum + height=2mm,minimum width=1.6667mm,inner sep=0pt] +\tikzstyle{decide}=[draw=red,fill=red,shape=isosceles triangle,right,minimum + height=2mm,minimum width=1.6667mm,inner sep=0pt,shape border rotate=90] +\tikzstyle{cast}=[draw=green!50!black,fill=green!50!black,shape=circle,left,minimum + height=2mm,minimum width=1.6667mm,inner sep=0pt] + + +\usepackage{multirow} +\usepackage{graphicx} +\usepackage{epstopdf} +\usepackage{amssymb} +\usepackage{rounddiag} +\graphicspath{{../}} + +\usepackage{technote} +\usepackage{homodel} +\usepackage{enumerate} +%%\usepackage{ulem}\normalem + +% to center caption +\usepackage{caption} + +\newcommand{\textstretch}{1.4} +\newcommand{\algostretch}{1} +\newcommand{\eqnstretch}{0.5} + +\newconstruct{\FOREACH}{\textbf{for each}}{\textbf{do}}{\ENDFOREACH}{} + +%\newconstruct{\ON}{\textbf{on}}{\textbf{do}}{\ENDON}{\textbf{end on}} +\newcommand\With{\textbf{while}} +\newcommand\From{\textbf{from}} +\newcommand\Broadcast{\textbf{broadcast}} +\newcommand\PBroadcast{send} +\newcommand\UpCall{\textbf{UpCall}} +\newcommand\DownCall{\textbf{DownCall}} +\newcommand \Call{\textbf{Call}} +\newident{noop} +\newconstruct{\UPON}{\textbf{upon}}{\textbf{do}}{\ENDUPON}{} + + + +\newcommand{\abcast}{\mathsf{to\mbox{\sf-}broadcast}} +\newcommand{\adeliver}{\mathsf{to\mbox{\sf-}deliver}} + +\newcommand{\ABCAgreement}{\emph{TO-Agreement}} +\newcommand{\ABCIntegrity}{\emph{TO-Integrity}} +\newcommand{\ABCValidity}{\emph{TO-Validity}} +\newcommand{\ABCTotalOrder}{\emph{TO-Order}} +\newcommand{\ABCBoundedDelivery}{\emph{TO-Bounded Delivery}} + + +\newcommand{\tabc}{\mathit{atab\mbox{\sf-}cast}} +\newcommand{\anno}{\mathit{atab\mbox{\sf-}announce}} +\newcommand{\abort}{\mathit{atab\mbox{\sf-}abort}} +\newcommand{\tadel}{\mathit{atab\mbox{\sf-}deliver}} + +\newcommand{\ATABAgreement}{\emph{ATAB-Agreement}} +\newcommand{\ATABAbort}{\emph{ATAB-Abort}} +\newcommand{\ATABIntegrity}{\emph{ATAB-Integrity}} +\newcommand{\ATABValidity}{\emph{ATAB-Validity}} +\newcommand{\ATABAnnounce}{\emph{ATAB-Announcement}} +\newcommand{\ATABTermination}{\emph{ATAB-Termination}} +%\newcommand{\ATABFastAnnounce}{\emph{ATAB-Fast-Announcement}} + +%% Command for observations. +\newtheorem{observation}{Observation} + + +%% HO ALGORITHM DEFINITIONS +\newconstruct{\FUNCTION}{\textbf{Function}}{\textbf{:}}{\ENDFUNCTION}{} + +%% Uncomment the following four lines to remove remarks and visible traces of +%% modifications in the document +%%\renewcommand{\sout}[1]{\relaxx} +%%\renewcommand{\uline}[1]{#1} +%% \renewcommand{\uwave}[1]{#1} + \renewcommand{\note}[2][default]{\relax} + + +%% The following commands can be used to generate TR or Conference version of the paper +\newcommand{\tr}[1]{} +\renewcommand{\tr}[1]{#1} +\newcommand{\onlypaper}[1]{#1} +%\renewcommand{\onlypaper}[1]{} +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%\pagestyle{plain} +%\pagestyle{empty} + +%% IEEE tweaks +%\setlength{\IEEEilabelindent}{.5\parindent} +%\setlength{\IEEEiednormlabelsep}{.5\parindent} + +\begin{document} +% +% paper title +% can use linebreaks \\ within to get better formatting as desired +\title{The latest gossip on BFT consensus\vspace{-0.7\baselineskip}} + + + +\author{\IEEEauthorblockN{\large Ethan Buchman, Jae Kwon and Zarko Milosevic\\} + \IEEEauthorblockN{\large Tendermint}\\ + %\\\vspace{-0.5\baselineskip} + \IEEEauthorblockN{September 24, 2018} +} + +% make the title area +\maketitle +\vspace*{0.5em} + +\begin{abstract} +This paper presents Tendermint, a new protocol for ordering events in a distributed network under adversarial conditions. More commonly known as Byzantine Fault Tolerant (BFT) consensus or atomic broadcast, the problem has attracted significant attention in recent years due to the widespread success of blockchain-based digital currencies, such as Bitcoin and Ethereum, which successfully solved the problem in a public setting without a central authority. Tendermint modernizes classic academic work on the subject and simplifies the design of the BFT algorithm by relying on a peer-to-peer gossip protocol among nodes. +\end{abstract} + +%\noindent \textbf{Keywords:} Blockchain, Byzantine Fault Tolerance, State Machine %Replication + +\input{intro} +\input{definitions} +\input{consensus} +\input{proof} +\input{conclusion} + +\bibliographystyle{IEEEtran} +\bibliography{lit} + +%\appendix + +\end{document} diff --git a/spec/consensus/consensus-paper/proof.tex b/spec/consensus/consensus-paper/proof.tex new file mode 100644 index 000000000..1c84d9b11 --- /dev/null +++ b/spec/consensus/consensus-paper/proof.tex @@ -0,0 +1,280 @@ +\section{Proof of Tendermint consensus algorithm} \label{sec:proof} + +\begin{lemma} \label{lemma:majority-intersection} For all $f\geq 0$, any two +sets of processes with voting power at least equal to $2f+1$ have at least one +correct process in common. \end{lemma} + +\begin{proof} As the total voting power is equal to $n=3f+1$, we have $2(2f+1) + = n+f+1$. This means that the intersection of two sets with the voting + power equal to $2f+1$ contains at least $f+1$ voting power in common, \ie, + at least one correct process (as the total voting power of faulty processes + is $f$). The result follows directly from this. \end{proof} + +\begin{lemma} \label{lemma:locked-decision_value-prevote-v} If $f+1$ correct +processes lock value $v$ in round $r_0$ ($lockedValue = v$ and $lockedRound = +r_0$), then in all rounds $r > r_0$, they send $\Prevote$ for $id(v)$ or +$\nil$. \end{lemma} + +\begin{proof} We prove the result by induction on $r$. + +\emph{Base step $r = r_0 + 1:$} Let's denote with $C$ the set of correct +processes with voting power equal to $f+1$. By the rules at +line~\ref{line:tab:recvProposal} and line~\ref{line:tab:acceptProposal}, the +processes from the set $C$ can't accept $\Proposal$ for any value different +from $v$ in round $r$, and therefore can't send a $\li{\Prevote,height_p, +r,id(v')}$ message, if $v' \neq v$. Therefore, the Lemma holds for the base +step. + +\emph{Induction step from $r_1$ to $r_1+1$:} We assume that no process from the +set $C$ has sent $\Prevote$ for values different than $id(v)$ or $\nil$ until +round $r_1 + 1$. We now prove that the Lemma also holds for round $r_1 + 1$. As +processes from the set $C$ send $\Prevote$ for $id(v)$ or $\nil$ in rounds $r_0 +\le r \le r_1$, by Lemma~\ref{lemma:majority-intersection} there is no value +$v' \neq v$ for which it is possible to receive $2f+1$ $\Prevote$ messages in +those rounds (i). Therefore, we have for all processes from the set $C$, +$lockedValue = v$ and $lockedRound \ge r_0$. Let's assume by a contradiction +that a process $q$ from the set $C$ sends $\Prevote$ in round $r_1 + 1$ for +value $id(v')$, where $v' \neq v$. This is possible only by +line~\ref{line:tab:prevote-higher-proposal}. Note that this implies that $q$ +received $2f+1$ $\li{\Prevote,h_q, r,id(v')}$ messages, where $r > r_0$ and $r +< r_1 +1$ (see line~\ref{line:tab:cond-prevote-higher-proposal}). A +contradiction with (i) and Lemma~\ref{lemma:majority-intersection}. +\end{proof} + +\begin{lemma} \label{lemma:agreement} Algorithm~\ref{alg:tendermint} satisfies +Agreement. \end{lemma} + +\begin{proof} Let round $r_0$ be the first round of height $h$ such that some + correct process $p$ decides $v$. We now prove that if some correct process + $q$ decides $v'$ in some round $r \ge r_0$, then $v = v'$. + +In case $r = r_0$, $q$ has received at least $2f+1$ +$\li{\Precommit,h_p,r_0,id(v')}$ messages at line~\ref{line:tab:onDecideRule}, +while $p$ has received at least $2f+1$ $\li{\Precommit,h_p,r_0,id(v)}$ +messages. By Lemma~\ref{lemma:majority-intersection} two sets of messages of +voting power $2f+1$ intersect in at least one correct process. As a correct +process sends a single $\Precommit$ message in a round, then $v=v'$. + +We prove the case $r > r_0$ by contradiction. By the +rule~\ref{line:tab:onDecideRule}, $p$ has received at least $2f+1$ voting-power +equivalent of $\li{\Precommit,h_p,r_0,id(v)}$ messages, i.e., at least $f+1$ +voting-power equivalent correct processes have locked value $v$ in round $r_0$ and have +sent those messages (i). Let denote this set of messages with $C$. On the +other side, $q$ has received at least $2f+1$ voting power equivalent of +$\li{\Precommit,h_q, r,id(v')}$ messages. As the voting power of all faulty +processes is at most $f$, some correct process $c$ has sent one of those +messages. By the rule at line~\ref{line:tab:recvPrevote}, $c$ has locked value +$v'$ in round $r$ before sending $\li{\Precommit,h_q, r,id(v')}$. Therefore $c$ +has received $2f+1$ $\Prevote$ messages for $id(v')$ in round $r > r_0$ (see +line~\ref{line:tab:recvPrevote}). By Lemma~\ref{lemma:majority-intersection}, a +process from the set $C$ has sent $\Prevote$ message for $id(v')$ in round $r$. +A contradiction with (i) and Lemma~\ref{lemma:locked-decision_value-prevote-v}. +\end{proof} + +\begin{lemma} \label{lemma:agreement} Algorithm~\ref{alg:tendermint} satisfies +Validity. \end{lemma} + +\begin{proof} Trivially follows from the rule at line +\ref{line:tab:validDecisionValue} which ensures that only valid values can be +decided. \end{proof} + +\begin{lemma} \label{lemma:round-synchronisation} If we assume that: +\begin{enumerate} + \item a correct process $p$ is the first correct process to + enter a round $r>0$ at time $t > GST$ (for every correct process + $c$, $round_c \le r$ at time $t$) + \item the proposer of round $r$ is + a correct process $q$ + \item for every correct process $c$, + $lockedRound_c \le validRound_q$ at time $t$ + \item $\timeoutPropose(r) + > 2\Delta + \timeoutPrecommit(r-1)$, $\timeoutPrevote(r) > 2\Delta$ and + $\timeoutPrecommit(r) > 2\Delta$, +\end{enumerate} +then all correct processes decide in round $r$ before $t + 4\Delta + + \timeoutPrecommit(r-1)$. +\end{lemma} + +\begin{proof} As $p$ is the first correct process to enter round $r$, it + executed the line~\ref{line:tab:nextRound} after $\timeoutPrecommit(r-1)$ + expired. Therefore, $p$ received $2f+1$ $\Precommit$ messages in the round + $r-1$ before time $t$. By the \emph{Gossip communication} property, all + correct processes will receive those messages the latest at time $t + + \Delta$. Correct processes that are in rounds $< r-1$ at time $t$ will + enter round $r-1$ (see the rule at line~\ref{line:tab:nextRound2}) and + trigger $\timeoutPrecommit(r-1)$ (see rule~\ref{line:tab:startTimeoutPrecommit}) + by time $t+\Delta$. Therefore, all correct processes will start round $r$ + by time $t+\Delta+\timeoutPrecommit(r-1)$ (i). + +In the worst case, the process $q$ is the last correct process to enter round +$r$, so $q$ starts round $r$ and sends $\Proposal$ message for some value $v$ +at time $t + \Delta + \timeoutPrecommit(r-1)$. Therefore, all correct processes +receive the $\Proposal$ message from $q$ the latest by time $t + 2\Delta + +\timeoutPrecommit(r-1)$. Therefore, if $\timeoutPropose(r) > 2\Delta + +\timeoutPrecommit(r-1)$, all correct processes will receive $\Proposal$ message +before $\timeoutPropose(r)$ expires. + +By (3) and the rules at line~\ref{line:tab:recvProposal} and +\ref{line:tab:acceptProposal}, all correct processes will accept the +$\Proposal$ message for value $v$ and will send a $\Prevote$ message for +$id(v)$ by time $t + 2\Delta + \timeoutPrecommit(r-1)$. Note that by the +\emph{Gossip communication} property, the $\Prevote$ messages needed to trigger +the rule at line~\ref{line:tab:acceptProposal} are received before time $t + +\Delta$. + +By time $t + 3\Delta + \timeoutPrecommit(r-1)$, all correct processes will receive +$\Proposal$ for $v$ and $2f+1$ corresponding $\Prevote$ messages for $id(v)$. +By the rule at line~\ref{line:tab:recvPrevote}, all correct processes will send +a $\Precommit$ message (see line~\ref{line:tab:precommit-v}) for $id(v)$ by +time $t + 3\Delta + \timeoutPrecommit(r-1)$. Therefore, by time $t + 4\Delta + +\timeoutPrecommit(r-1)$, all correct processes will have received the $\Proposal$ +for $v$ and $2f+1$ $\Precommit$ messages for $id(v)$, so they decide at +line~\ref{line:tab:decide} on $v$. + +This scenario holds if every correct process $q$ sends a $\Precommit$ message +before $\timeoutPrevote(r)$ expires, and if $\timeoutPrecommit(r)$ does not expire +before $t + 4\Delta + \timeoutPrecommit(r-1)$. Let's assume that a correct process +$c_1$ is the first correct process to trigger $\timeoutPrevote(r)$ (see the rule +at line~\ref{line:tab:recvAny2/3Prevote}) at time $t_1 > t$. This implies that +before time $t_1$, $c_1$ received a $\Proposal$ ($step_{c_1}$ must be +$\prevote$ by the rule at line~\ref{line:tab:recvAny2/3Prevote}) and a set of +$2f+1$ $\Prevote$ messages. By time $t_1 + \Delta$, all correct processes will +receive those messages. Note that even if some correct process was in the +smaller round before time $t_1$, at time $t_1 + \Delta$ it will start round $r$ +after receiving those messages (see the rule at +line~\ref{line:tab:skipRounds}). Therefore, all correct processes will send +their $\Prevote$ message for $id(v)$ by time $t_1 + \Delta$, and all correct +processes will receive those messages the by time $t_1 + 2\Delta$. Therefore, +as $\timeoutPrevote(r) > 2\Delta$, this ensures that all correct processes receive +$\Prevote$ messages from all correct processes before their respective local +$\timeoutPrevote(r)$ expire. + +On the other hand, $\timeoutPrecommit(r)$ is triggered in a correct process $c_2$ +after it receives any set of $2f+1$ $\Precommit$ messages for the first time. +Let's denote with $t_2 > t$ the earliest point in time $\timeoutPrecommit(r)$ is +triggered in some correct process $c_2$. This implies that $c_2$ has received +at least $f+1$ $\Precommit$ messages for $id(v)$ from correct processes, i.e., +those processes have received $\Proposal$ for $v$ and $2f+1$ $\Prevote$ +messages for $id(v)$ before time $t_2$. By the \emph{Gossip communication} +property, all correct processes will receive those messages by time $t_2 + +\Delta$, and will send $\Precommit$ messages for $id(v)$. Note that even if +some correct processes were at time $t_2$ in a round smaller than $r$, by the +rule at line~\ref{line:tab:skipRounds} they will enter round $r$ by time $t_2 + +\Delta$. Therefore, by time $t_2 + 2\Delta$, all correct processes will +receive $\Proposal$ for $v$ and $2f+1$ $\Precommit$ messages for $id(v)$. So if +$\timeoutPrecommit(r) > 2\Delta$, all correct processes will decide before the +timeout expires. \end{proof} + + +\begin{lemma} \label{lemma:validValue} If a correct process $p$ locks a value + $v$ at time $t_0 > GST$ in some round $r$ ($lockedValue = v$ and + $lockedRound = r$) and $\timeoutPrecommit(r) > 2\Delta$, then all correct + processes set $validValue$ to $v$ and $validRound$ to $r$ before starting + round $r+1$. \end{lemma} + +\begin{proof} In order to prove this Lemma, we need to prove that if the + process $p$ locks a value $v$ at time $t_0$, then no correct process will + leave round $r$ before time $t_0 + \Delta$ (unless it has already set + $validValue$ to $v$ and $validRound$ to $r$). It is sufficient to prove + this, since by the \emph{Gossip communication} property the messages that + $p$ received at time $t_0$ and that triggered rule at + line~\ref{line:tab:recvPrevote} will be received by time $t_0 + \Delta$ by + all correct processes, so all correct processes that are still in round $r$ + will set $validValue$ to $v$ and $validRound$ to $r$ (by the rule at + line~\ref{line:tab:recvPrevote}). To prove this, we need to compute the + earliest point in time a correct process could leave round $r$ without + updating $validValue$ to $v$ and $validRound$ to $r$ (we denote this time + with $t_1$). The Lemma is correct if $t_0 + \Delta < t_1$. + +If the process $p$ locks a value $v$ at time $t_0$, this implies that $p$ +received the valid $\Proposal$ message for $v$ and $2f+1$ +$\li{\Prevote,h,r,id(v)}$ at time $t_0$. At least $f+1$ of those messages are +sent by correct processes. Let's denote this set of correct processes as $C$. By +Lemma~\ref{lemma:majority-intersection} any set of $2f+1$ $\Prevote$ messages +in round $r$ contains at least a single message from the set $C$. + +Let's denote as time $t$ the earliest point in time a correct process, $c_1$, triggered +$\timeoutPrevote(r)$. This implies that $c_1$ received $2f+1$ $\Prevote$ messages +(see the rule at line \ref{line:tab:recvAny2/3Prevote}), where at least one of +those messages was sent by a process $c_2$ from the set $C$. Therefore, process +$c_2$ had received $\Proposal$ message before time $t$. By the \emph{Gossip +communication} property, all correct processes will receive $\Proposal$ and +$2f+1$ $\Prevote$ messages for round $r$ by time $t+\Delta$. The latest point +in time $p$ will trigger $\timeoutPrevote(r)$ is $t+\Delta$\footnote{Note that +even if $p$ was in smaller round at time $t$ it will start round $r$ by time +$t+\Delta$.}. So the latest point in time $p$ can lock the value $v$ in +round $r$ is $t_0 = t+\Delta+\timeoutPrevote(r)$ (as at this point +$\timeoutPrevote(r)$ expires, so a process sends $\Precommit$ $\nil$ and updates +$step$ to $\precommit$, see line \ref{line:tab:onTimeoutPrevote}). + +Note that according to the Algorithm \ref{alg:tendermint}, a correct process +can not send a $\Precommit$ message before receiving $2f+1$ $\Prevote$ +messages. Therefore, no correct process can send a $\Precommit$ message in +round $r$ before time $t$. If a correct process sends a $\Precommit$ message +for $\nil$, it implies that it has waited for the full duration of +$\timeoutPrevote(r)$ (see line +\ref{line:tab:precommit-nil-onTimeout})\footnote{The other case in which a +correct process $\Precommit$ for $\nil$ is after receiving $2f+1$ $Prevote$ for +$\nil$ messages, see the line \ref{line:tab:precommit-v-1}. By +Lemma~\ref{lemma:majority-intersection}, this is not possible in round $r$.}. +Therefore, no correct process can send $\Precommit$ for $\nil$ before time $t + +\timeoutPrevote(r)$ (*). + +A correct process $q$ that enters round $r+1$ must wait (i) $\timeoutPrecommit(r)$ +(see line \ref{line:tab:nextRound}) or (ii) receiving $f+1$ messages from the +round $r+1$ (see the line \ref{line:tab:skipRounds}). In the former case, $q$ +receives $2f+1$ $\Precommit$ messages before starting $\timeoutPrecommit(r)$. If +at least a single $\Precommit$ message from a correct process (at least $f+1$ +voting power equivalent of those messages is sent by correct processes) is for +$\nil$, then $q$ cannot start round $r+1$ before time $t_1 = t + +\timeoutPrevote(r) + \timeoutPrecommit(r)$ (see (*)). Therefore in this case we have: +$t_0 + \Delta < t_1$, i.e., $t+2\Delta+\timeoutPrevote(r) < t + \timeoutPrevote(r) + +\timeoutPrecommit(r)$, and this is true whenever $\timeoutPrecommit(r) > 2\Delta$, so +Lemma holds in this case. + +If in the set of $2f+1$ $\Precommit$ messages $q$ receives, there is at least a +single $\Precommit$ for $id(v)$ message from a correct process $c$, then $q$ +can start the round $r+1$ the earliest at time $t_1 = t+\timeoutPrecommit(r)$. In +this case, by the \emph{Gossip communication} property, all correct processes +will receive $\Proposal$ and $2f+1$ $\Prevote$ messages (that $c$ received +before time $t$) the latest at time $t+\Delta$. Therefore, $q$ will set +$validValue$ to $v$ and $validRound$ to $r$ the latest at time $t+\Delta$. As +$t+\Delta < t+\timeoutPrecommit(r)$, whenever $\timeoutPrecommit(r) > \Delta$, the +Lemma holds also in this case. + +In case (ii), $q$ received at least a single message from a correct process $c$ +from the round $r+1$. The earliest point in time $c$ could have started round +$r+1$ is $t+\timeoutPrecommit(r)$ in case it received a $\Precommit$ message for +$v$ from some correct process in the set of $2f+1$ $\Precommit$ messages it +received. The same reasoning as above holds also in this case, so $q$ set +$validValue$ to $v$ and $validRound$ to $r$ the latest by time $t+\Delta$. As +$t+\Delta < t+\timeoutPrecommit(r)$, whenever $\timeoutPrecommit(r) > \Delta$, the +Lemma holds also in this case. \end{proof} + +\begin{lemma} \label{lemma:agreement} Algorithm~\ref{alg:tendermint} satisfies +Termination. \end{lemma} + +\begin{proof} Lemma~\ref{lemma:round-synchronisation} defines a scenario in + which all correct processes decide. We now prove that within a bounded + duration after GST such a scenario will unfold. Let's assume that at time + $GST$ the highest round started by a correct process is $r_0$, and that + there exists a correct process $p$ such that the following holds: for every + correct process $c$, $lockedRound_c \le validRound_p$. Furthermore, we + assume that $p$ will be the proposer in some round $r_1 > r$ (this is + ensured by the $\coord$ function). + +We have two cases to consider. In the first case, for all rounds $r \ge r_0$ +and $r < r_1$, no correct process locks a value (set $lockedRound$ to $r$). So +in round $r_1$ we have the scenario from the +Lemma~\ref{lemma:round-synchronisation}, so all correct processes decides in +round $r_1$. + +In the second case, a correct process locks a value $v$ in round $r_2$, where +$r_2 \ge r_0$ and $r_2 < r_1$. Let's assume that $r_2$ is the highest round +before $r_1$ in which some correct process $q$ locks a value. By Lemma +\ref{lemma:validValue} at the end of round $r_2$ the following holds for all +correct processes $c$: $validValue_c = lockedValue_q$ and $validRound_c = r_2$. +Then in round $r_1$, the conditions for the +Lemma~\ref{lemma:round-synchronisation} holds, so all correct processes decide. +\end{proof} + diff --git a/spec/consensus/consensus-paper/rounddiag.sty b/spec/consensus/consensus-paper/rounddiag.sty new file mode 100644 index 000000000..a6ca5d883 --- /dev/null +++ b/spec/consensus/consensus-paper/rounddiag.sty @@ -0,0 +1,62 @@ +% ROUNDDIAG STYLE +% for LaTeX version 2e +% by -- 2008 Martin Hutle +% +% This style file is free software; you can redistribute it and/or +% modify it under the terms of the GNU Lesser General Public +% License as published by the Free Software Foundation; either +% version 2 of the License, or (at your option) any later version. +% +% This style file is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +% Lesser General Public License for more details. +% +% You should have received a copy of the GNU Lesser General Public +% License along with this style file; if not, write to the +% Free Software Foundation, Inc., 59 Temple Place - Suite 330, +% Boston, MA 02111-1307, USA. +% +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{rounddiag} +\typeout{Document Style `rounddiag' - provides simple round diagrams} +% +\RequirePackage{ifthen} +\RequirePackage{calc} +\RequirePackage{tikz} + +\def\rdstretch{3} + +\tikzstyle{msg}=[->,thick,>=latex] +\tikzstyle{rndline}=[dotted] +\tikzstyle{procline}=[dotted] + +\newenvironment{rounddiag}[2]{ +\begin{center} +\begin{tikzpicture} +\foreach \i in {1,...,#1}{ + \draw[procline] (0,#1-\i) node[xshift=-1em]{$p_{\i}$} -- (#2*\rdstretch+1,#1-\i); +} +\foreach \i in {0,...,#2}{ + \draw[rndline] (\i*\rdstretch+0.5,0) -- (\i*\rdstretch+0.5,#1-1); +} +\newcommand{\rdat}[2]{ + (##2*\rdstretch+0.5,#1-##1) +}% +\newcommand{\round}[2]{% + \def\rdround{##1} + \ifthenelse{\equal{##2}{}}{}{ + \node[yshift=-1em] at ({##1*\rdstretch+0.5-0.5*\rdstretch},0) {##2}; + } +}% +\newcommand{\rdmessage}[3]{\draw[msg] + (\rdround*\rdstretch-\rdstretch+0.5,#1-##1) -- node[yshift=1.2ex]{##3} + (\rdround*\rdstretch+0.5,#1-##2);}% +\newcommand{\rdalltoall}{% + \foreach \i in {1,...,#1}{ + \foreach \j in {1,...,#1}{ + { \rdmessage{\i}{\j}{}}}}}% +}{% +\end{tikzpicture} +\end{center} +} diff --git a/spec/consensus/consensus-paper/technote.sty b/spec/consensus/consensus-paper/technote.sty new file mode 100644 index 000000000..5353f13cd --- /dev/null +++ b/spec/consensus/consensus-paper/technote.sty @@ -0,0 +1,118 @@ +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{technote}[2007/11/09] +\typeout{Template for quick notes with some useful definitions} + +\RequirePackage{ifthen} +\RequirePackage{calc} +\RequirePackage{amsmath,amssymb,amsthm} +\RequirePackage{epsfig} +\RequirePackage{algorithm} +\RequirePackage[noend]{algorithmicplus} + +\newboolean{technote@noedit} +\setboolean{technote@noedit}{false} +\DeclareOption{noedit}{\setboolean{technote@noedit}{true}} + +\newcounter{technote@lang} +\setcounter{technote@lang}{0} +\DeclareOption{german}{\setcounter{technote@lang}{1}} +\DeclareOption{french}{\setcounter{technote@lang}{2}} + +\DeclareOption{fullpage}{ +\oddsidemargin -10mm % Margin on odd side pages (default=0mm) +\evensidemargin -10mm % Margin on even side pages (default=0mm) +\topmargin -10mm % Top margin space (default=16mm) +\headheight \baselineskip % Height of headers (default=0mm) +\headsep \baselineskip % Separation spc btw header and text (d=0mm) +\footskip 30pt % Separation spc btw text and footer (d=30pt) +\textheight 230mm % Total text height (default=200mm) +\textwidth 180mm % Total text width (default=160mm) +} + +\renewcommand{\algorithmiccomment}[1]{\hfill/* #1 */} +\renewcommand{\algorithmiclnosize}{\scriptsize} + +\newboolean{technote@truenumbers} +\setboolean{technote@truenumbers}{false} +\DeclareOption{truenumbers}{\setboolean{technote@truenumbers}{true}} + +\ProcessOptions + +\newcommand{\N}{\ifthenelse{\boolean{technote@truenumbers}}% + {\mbox{\rm I\hspace{-.5em}N}}% + {\mathbb{N}}} + +\newcommand{\R}{\ifthenelse{\boolean{technote@truenumbers}}% + {\mbox{\rm I\hspace{-.2em}R}}% + {\mathbb{R}}} + +\newcommand{\Z}{\mathbb{Z}} + +\newcommand{\set}[1]{\left\{#1\right\}} +\newcommand{\mathsc}[1]{\mbox{\sc #1}} +\newcommand{\li}[1]{\langle#1\rangle} +\newcommand{\st}{\;s.t.\;} +\newcommand{\Real}{\R} +\newcommand{\Natural}{\N} +\newcommand{\Integer}{\Z} + +% edit commands +\newcommand{\newedit}[2]{ + \newcommand{#1}[2][default]{% + \ifthenelse{\boolean{technote@noedit}}{}{ + \par\vspace{2mm} + \noindent + \begin{tabular}{|l|}\hline + \parbox{\linewidth-\tabcolsep*2}{{\bf #2:}\hfill\ifthenelse{\equal{##1}{default}}{}{##1}}\\\hline + \parbox{\linewidth-\tabcolsep*2}{\rule{0pt}{5mm}##2\rule[-2mm]{0pt}{2mm}}\\\hline + \end{tabular} + \par\vspace{2mm} + } + } +} + +\newedit{\note}{Note} +\newedit{\comment}{Comment} +\newedit{\question}{Question} +\newedit{\content}{Content} +\newedit{\problem}{Problem} + +\newcommand{\mnote}[1]{\marginpar{\scriptsize\it + \begin{minipage}[t]{0.8 in} + \raggedright #1 + \end{minipage}}} + +\newcommand{\Insert}[1]{\underline{#1}\marginpar{$|$}} + +\newcommand{\Delete}[1]{\marginpar{$|$} +} + +% lemma, theorem, etc. +\newtheorem{lemma}{Lemma} +\newtheorem{proposition}{Proposition} +\newtheorem{theorem}{Theorem} +\newtheorem{corollary}{Corollary} +\newtheorem{assumption}{Assumption} +\newtheorem{definition}{Definition} + +\gdef\op|{\,|\;} +\gdef\op:{\,:\;} +\newcommand{\assign}{\leftarrow} +\newcommand{\inc}[1]{#1 \assign #1 + 1} +\newcommand{\isdef}{:=} + +\newcommand{\ident}[1]{\mathit{#1}} +\def\newident#1{\expandafter\def\csname #1\endcsname{\ident{#1}}} + +\newcommand{\eg}{{\it e.g.}} +\newcommand{\ie}{{\it i.e.}} +\newcommand{\apriori}{{\it apriori}} +\newcommand{\etal}{{\it et al.}} + +\newcommand\ps@technote{% + \renewcommand\@oddhead{\theheader}% + \let\@evenhead\@oddhead + \renewcommand\@evenfoot + {\hfil\normalfont\textrm{\thepage}\hfil}% + \let\@oddfoot\@evenfoot +} diff --git a/spec/consensus/consensus.md b/spec/consensus/consensus.md new file mode 100644 index 000000000..5d1526a17 --- /dev/null +++ b/spec/consensus/consensus.md @@ -0,0 +1,352 @@ +--- +order: 1 +--- +# Byzantine Consensus Algorithm + +## Terms + +- The network is composed of optionally connected _nodes_. Nodes + directly connected to a particular node are called _peers_. +- The consensus process in deciding the next block (at some _height_ + `H`) is composed of one or many _rounds_. +- `NewHeight`, `Propose`, `Prevote`, `Precommit`, and `Commit` + represent state machine states of a round. (aka `RoundStep` or + just "step"). +- A node is said to be _at_ a given height, round, and step, or at + `(H,R,S)`, or at `(H,R)` in short to omit the step. +- To _prevote_ or _precommit_ something means to broadcast a [prevote + vote](https://godoc.org/github.com/tendermint/tendermint/types#Vote) + or [first precommit + vote](https://godoc.org/github.com/tendermint/tendermint/types#FirstPrecommit) + for something. +- A vote _at_ `(H,R)` is a vote signed with the bytes for `H` and `R` + included in its [sign-bytes](../core/data_structures.md#vote). +- _+2/3_ is short for "more than 2/3" +- _1/3+_ is short for "1/3 or more" +- A set of +2/3 of prevotes for a particular block or `` at + `(H,R)` is called a _proof-of-lock-change_ or _PoLC_ for short. + +## State Machine Overview + +At each height of the blockchain a round-based protocol is run to +determine the next block. Each round is composed of three _steps_ +(`Propose`, `Prevote`, and `Precommit`), along with two special steps +`Commit` and `NewHeight`. + +In the optimal scenario, the order of steps is: + +```md +NewHeight -> (Propose -> Prevote -> Precommit)+ -> Commit -> NewHeight ->... +``` + +The sequence `(Propose -> Prevote -> Precommit)` is called a _round_. +There may be more than one round required to commit a block at a given +height. Examples for why more rounds may be required include: + +- The designated proposer was not online. +- The block proposed by the designated proposer was not valid. +- The block proposed by the designated proposer did not propagate + in time. +- The block proposed was valid, but +2/3 of prevotes for the proposed + block were not received in time for enough validator nodes by the + time they reached the `Precommit` step. Even though +2/3 of prevotes + are necessary to progress to the next step, at least one validator + may have voted `` or maliciously voted for something else. +- The block proposed was valid, and +2/3 of prevotes were received for + enough nodes, but +2/3 of precommits for the proposed block were not + received for enough validator nodes. + +Some of these problems are resolved by moving onto the next round & +proposer. Others are resolved by increasing certain round timeout +parameters over each successive round. + +## State Machine Diagram + +```md + +-------------------------------------+ + v |(Wait til `CommmitTime+timeoutCommit`) + +-----------+ +-----+-----+ + +----------> | Propose +--------------+ | NewHeight | + | +-----------+ | +-----------+ + | | ^ + |(Else, after timeoutPrecommit) v | ++-----+-----+ +-----------+ | +| Precommit | <------------------------+ Prevote | | ++-----+-----+ +-----------+ | + |(When +2/3 Precommits for block found) | + v | ++--------------------------------------------------------------------+ +| Commit | +| | +| * Set CommitTime = now; | +| * Wait for block, then stage/save/commit block; | ++--------------------------------------------------------------------+ +``` + +# Background Gossip + +A node may not have a corresponding validator private key, but it +nevertheless plays an active role in the consensus process by relaying +relevant meta-data, proposals, blocks, and votes to its peers. A node +that has the private keys of an active validator and is engaged in +signing votes is called a _validator-node_. All nodes (not just +validator-nodes) have an associated state (the current height, round, +and step) and work to make progress. + +Between two nodes there exists a `Connection`, and multiplexed on top of +this connection are fairly throttled `Channel`s of information. An +epidemic gossip protocol is implemented among some of these channels to +bring peers up to speed on the most recent state of consensus. For +example, + +- Nodes gossip `PartSet` parts of the current round's proposer's + proposed block. A LibSwift inspired algorithm is used to quickly + broadcast blocks across the gossip network. +- Nodes gossip prevote/precommit votes. A node `NODE_A` that is ahead + of `NODE_B` can send `NODE_B` prevotes or precommits for `NODE_B`'s + current (or future) round to enable it to progress forward. +- Nodes gossip prevotes for the proposed PoLC (proof-of-lock-change) + round if one is proposed. +- Nodes gossip to nodes lagging in blockchain height with block + [commits](https://godoc.org/github.com/tendermint/tendermint/types#Commit) + for older blocks. +- Nodes opportunistically gossip `ReceivedVote` messages to hint peers what + votes it already has. +- Nodes broadcast their current state to all neighboring peers. (but + is not gossiped further) + +There's more, but let's not get ahead of ourselves here. + +## Proposals + +A proposal is signed and published by the designated proposer at each +round. The proposer is chosen by a deterministic and non-choking round +robin selection algorithm that selects proposers in proportion to their +voting power (see +[implementation](https://github.com/tendermint/tendermint/blob/master/types/validator_set.go)). + +A proposal at `(H,R)` is composed of a block and an optional latest +`PoLC-Round < R` which is included iff the proposer knows of one. This +hints the network to allow nodes to unlock (when safe) to ensure the +liveness property. + +## State Machine Spec + +### Propose Step (height:H,round:R) + +Upon entering `Propose`: + +- The designated proposer proposes a block at `(H,R)`. + +The `Propose` step ends: + +- After `timeoutProposeR` after entering `Propose`. --> goto + `Prevote(H,R)` +- After receiving proposal block and all prevotes at `PoLC-Round`. --> + goto `Prevote(H,R)` +- After [common exit conditions](#common-exit-conditions) + +### Prevote Step (height:H,round:R) + +Upon entering `Prevote`, each validator broadcasts its prevote vote. + +- First, if the validator is locked on a block since `LastLockRound` + but now has a PoLC for something else at round `PoLC-Round` where + `LastLockRound < PoLC-Round < R`, then it unlocks. +- If the validator is still locked on a block, it prevotes that. +- Else, if the proposed block from `Propose(H,R)` is good, it + prevotes that. +- Else, if the proposal is invalid or wasn't received on time, it + prevotes ``. + +The `Prevote` step ends: + +- After +2/3 prevotes for a particular block or ``. -->; goto + `Precommit(H,R)` +- After `timeoutPrevote` after receiving any +2/3 prevotes. --> goto + `Precommit(H,R)` +- After [common exit conditions](#common-exit-conditions) + +### Precommit Step (height:H,round:R) + +Upon entering `Precommit`, each validator broadcasts its precommit vote. + +- If the validator has a PoLC at `(H,R)` for a particular block `B`, it + (re)locks (or changes lock to) and precommits `B` and sets + `LastLockRound = R`. +- Else, if the validator has a PoLC at `(H,R)` for ``, it unlocks + and precommits ``. +- Else, it keeps the lock unchanged and precommits ``. + +A precommit for `` means "I didn’t see a PoLC for this round, but I +did get +2/3 prevotes and waited a bit". + +The Precommit step ends: + +- After +2/3 precommits for ``. --> goto `Propose(H,R+1)` +- After `timeoutPrecommit` after receiving any +2/3 precommits. --> goto + `Propose(H,R+1)` +- After [common exit conditions](#common-exit-conditions) + +### Common exit conditions + +- After +2/3 precommits for a particular block. --> goto + `Commit(H)` +- After any +2/3 prevotes received at `(H,R+x)`. --> goto + `Prevote(H,R+x)` +- After any +2/3 precommits received at `(H,R+x)`. --> goto + `Precommit(H,R+x)` + +### Commit Step (height:H) + +- Set `CommitTime = now()` +- Wait until block is received. --> goto `NewHeight(H+1)` + +### NewHeight Step (height:H) + +- Move `Precommits` to `LastCommit` and increment height. +- Set `StartTime = CommitTime+timeoutCommit` +- Wait until `StartTime` to receive straggler commits. --> goto + `Propose(H,0)` + +## Proofs + +### Proof of Safety + +Assume that at most -1/3 of the voting power of validators is byzantine. +If a validator commits block `B` at round `R`, it's because it saw +2/3 +of precommits at round `R`. This implies that 1/3+ of honest nodes are +still locked at round `R' > R`. These locked validators will remain +locked until they see a PoLC at `R' > R`, but this won't happen because +1/3+ are locked and honest, so at most -2/3 are available to vote for +anything other than `B`. + +### Proof of Liveness + +If 1/3+ honest validators are locked on two different blocks from +different rounds, a proposers' `PoLC-Round` will eventually cause nodes +locked from the earlier round to unlock. Eventually, the designated +proposer will be one that is aware of a PoLC at the later round. Also, +`timeoutProposalR` increments with round `R`, while the size of a +proposal are capped, so eventually the network is able to "fully gossip" +the whole proposal (e.g. the block & PoLC). + +### Proof of Fork Accountability + +Define the JSet (justification-vote-set) at height `H` of a validator +`V1` to be all the votes signed by the validator at `H` along with +justification PoLC prevotes for each lock change. For example, if `V1` +signed the following precommits: `Precommit(B1 @ round 0)`, +`Precommit( @ round 1)`, `Precommit(B2 @ round 4)` (note that no +precommits were signed for rounds 2 and 3, and that's ok), +`Precommit(B1 @ round 0)` must be justified by a PoLC at round 0, and +`Precommit(B2 @ round 4)` must be justified by a PoLC at round 4; but +the precommit for `` at round 1 is not a lock-change by definition +so the JSet for `V1` need not include any prevotes at round 1, 2, or 3 +(unless `V1` happened to have prevoted for those rounds). + +Further, define the JSet at height `H` of a set of validators `VSet` to +be the union of the JSets for each validator in `VSet`. For a given +commit by honest validators at round `R` for block `B` we can construct +a JSet to justify the commit for `B` at `R`. We say that a JSet +_justifies_ a commit at `(H,R)` if all the committers (validators in the +commit-set) are each justified in the JSet with no duplicitous vote +signatures (by the committers). + +- **Lemma**: When a fork is detected by the existence of two + conflicting [commits](../core/data_structures.md#commit), the + union of the JSets for both commits (if they can be compiled) must + include double-signing by at least 1/3+ of the validator set. + **Proof**: The commit cannot be at the same round, because that + would immediately imply double-signing by 1/3+. Take the union of + the JSets of both commits. If there is no double-signing by at least + 1/3+ of the validator set in the union, then no honest validator + could have precommitted any different block after the first commit. + Yet, +2/3 did. Reductio ad absurdum. + +As a corollary, when there is a fork, an external process can determine +the blame by requiring each validator to justify all of its round votes. +Either we will find 1/3+ who cannot justify at least one of their votes, +and/or, we will find 1/3+ who had double-signed. + +### Alternative algorithm + +Alternatively, we can take the JSet of a commit to be the "full commit". +That is, if light clients and validators do not consider a block to be +committed unless the JSet of the commit is also known, then we get the +desirable property that if there ever is a fork (e.g. there are two +conflicting "full commits"), then 1/3+ of the validators are immediately +punishable for double-signing. + +There are many ways to ensure that the gossip network efficiently share +the JSet of a commit. One solution is to add a new message type that +tells peers that this node has (or does not have) a +2/3 majority for B +(or) at (H,R), and a bitarray of which votes contributed towards that +majority. Peers can react by responding with appropriate votes. + +We will implement such an algorithm for the next iteration of the +Tendermint consensus protocol. + +Other potential improvements include adding more data in votes such as +the last known PoLC round that caused a lock change, and the last voted +round/step (or, we may require that validators not skip any votes). This +may make JSet verification/gossip logic easier to implement. + +### Censorship Attacks + +Due to the definition of a block +[commit](https://github.com/tendermint/tendermint/blob/master/docs/tendermint-core/validators.md), any 1/3+ coalition of +validators can halt the blockchain by not broadcasting their votes. Such +a coalition can also censor particular transactions by rejecting blocks +that include these transactions, though this would result in a +significant proportion of block proposals to be rejected, which would +slow down the rate of block commits of the blockchain, reducing its +utility and value. The malicious coalition might also broadcast votes in +a trickle so as to grind blockchain block commits to a near halt, or +engage in any combination of these attacks. + +If a global active adversary were also involved, it can partition the +network in such a way that it may appear that the wrong subset of +validators were responsible for the slowdown. This is not just a +limitation of Tendermint, but rather a limitation of all consensus +protocols whose network is potentially controlled by an active +adversary. + +### Overcoming Forks and Censorship Attacks + +For these types of attacks, a subset of the validators through external +means should coordinate to sign a reorg-proposal that chooses a fork +(and any evidence thereof) and the initial subset of validators with +their signatures. Validators who sign such a reorg-proposal forego its +collateral on all other forks. Clients should verify the signatures on +the reorg-proposal, verify any evidence, and make a judgement or prompt +the end-user for a decision. For example, a phone wallet app may prompt +the user with a security warning, while a refrigerator may accept any +reorg-proposal signed by +1/2 of the original validators. + +No non-synchronous Byzantine fault-tolerant algorithm can come to +consensus when 1/3+ of validators are dishonest, yet a fork assumes that +1/3+ of validators have already been dishonest by double-signing or +lock-changing without justification. So, signing the reorg-proposal is a +coordination problem that cannot be solved by any non-synchronous +protocol (i.e. automatically, and without making assumptions about the +reliability of the underlying network). It must be provided by means +external to the weakly-synchronous Tendermint consensus algorithm. For +now, we leave the problem of reorg-proposal coordination to human +coordination via internet media. Validators must take care to ensure +that there are no significant network partitions, to avoid situations +where two conflicting reorg-proposals are signed. + +Assuming that the external coordination medium and protocol is robust, +it follows that forks are less of a concern than [censorship +attacks](#censorship-attacks). + +### Canonical vs subjective commit + +We distinguish between "canonical" and "subjective" commits. A subjective commit is what +each validator sees locally when they decide to commit a block. The canonical commit is +what is included by the proposer of the next block in the `LastCommit` field of +the block. This is what makes it canonical and ensures every validator agrees on the canonical commit, +even if it is different from the +2/3 votes a validator has seen, which caused the validator to +commit the respective block. Each block contains a canonical +2/3 commit for the previous +block. diff --git a/spec/consensus/creating-proposal.md b/spec/consensus/creating-proposal.md new file mode 100644 index 000000000..cb43c8ebb --- /dev/null +++ b/spec/consensus/creating-proposal.md @@ -0,0 +1,43 @@ +--- +order: 2 +--- +# Creating a proposal + +A block consists of a header, transactions, votes (the commit), +and a list of evidence of malfeasance (ie. signing conflicting votes). + +We include no more than 1/10th of the maximum block size +(`ConsensusParams.Block.MaxBytes`) of evidence with each block. + +## Reaping transactions from the mempool + +When we reap transactions from the mempool, we calculate maximum data +size by subtracting maximum header size (`MaxHeaderBytes`), the maximum +amino overhead for a block (`MaxAminoOverheadForBlock`), the size of +the last commit (if present) and evidence (if present). While reaping +we account for amino overhead for each transaction. + +```go +func MaxDataBytes(maxBytes int64, valsCount, evidenceCount int) int64 { + return maxBytes - + MaxOverheadForBlock - + MaxHeaderBytes - + int64(valsCount)*MaxVoteBytes - + int64(evidenceCount)*MaxEvidenceBytes +} +``` + +## Validating transactions in the mempool + +Before we accept a transaction in the mempool, we check if it's size is no more +than {MaxDataSize}. {MaxDataSize} is calculated using the same formula as +above, except we subtract the max number of evidence, {MaxNum} by the maximum size of evidence + +```go +func MaxDataBytesUnknownEvidence(maxBytes int64, valsCount int) int64 { + return maxBytes - + MaxOverheadForBlock - + MaxHeaderBytes - + (maxNumEvidence * MaxEvidenceBytes) +} +``` diff --git a/spec/consensus/evidence.md b/spec/consensus/evidence.md new file mode 100644 index 000000000..84d51a01b --- /dev/null +++ b/spec/consensus/evidence.md @@ -0,0 +1,199 @@ +# Evidence + +Evidence is an important component of Tendermint's security model. Whilst the core +consensus protocol provides correctness gaurantees for state machine replication +that can tolerate less than 1/3 failures, the evidence system looks to detect and +gossip byzantine faults whose combined power is greater than or equal to 1/3. It is worth noting that +the evidence system is designed purely to detect possible attacks, gossip them, +commit them on chain and inform the application running on top of Tendermint. +Evidence in itself does not punish "bad actors", this is left to the discretion +of the application. A common form of punishment is slashing where the validators +that were caught violating the protocol have all or a portion of their voting +power removed. Evidence, given the assumption that 1/3+ of the network is still +byzantine, is susceptible to censorship and should therefore be considered added +security on a "best effort" basis. + +This document walks through the various forms of evidence, how they are detected, +gossiped, verified and committed. + +> NOTE: Evidence here is internal to tendermint and should not be confused with +> application evidence + +## Detection + +### Equivocation + +Equivocation is the most fundamental of byzantine faults. Simply put, to prevent +replication of state across all nodes, a validator tries to convince some subset +of nodes to commit one block whilst convincing another subset to commit a +different block. This is achieved by double voting (hence +`DuplicateVoteEvidence`). A successful duplicate vote attack requires greater +than 1/3 voting power and a (temporary) network partition between the aforementioned +subsets. This is because in consensus, votes are gossiped around. When a node +observes two conflicting votes from the same peer, it will use the two votes of +evidence and begin gossiping this evidence to other nodes. [Verification](#duplicatevoteevidence) is addressed further down. + +```go +type DuplicateVoteEvidence struct { + VoteA Vote + VoteB Vote + + // and abci specific fields +} +``` + +### Light Client Attacks + +Light clients also comply with the 1/3+ security model, however, by using a +different, more lightweight verification method they are subject to a +different kind of 1/3+ attack whereby the byzantine validators could sign an +alternative light block that the light client will think is valid. Detection, +explained in greater detail +[here](../light-client/detection/detection_003_reviewed.md), involves comparison +with multiple other nodes in the hope that at least one is "honest". An "honest" +node will return a challenging light block for the light client to validate. If +this challenging light block also meets the +[validation criteria](../light-client/verification/verification_001_published.md) +then the light client sends the "forged" light block to the node. +[Verification](#lightclientattackevidence) is addressed further down. + +```go +type LightClientAttackEvidence struct { + ConflictingBlock LightBlock + CommonHeight int64 + + // and abci specific fields +} +``` + +## Verification + +If a node receives evidence, it will first try to verify it, then persist it. +Evidence of byzantine behavior should only be committed once (uniqueness) and +should be committed within a certain period from the point that it occurred +(timely). Timelines is defined by the `EvidenceParams`: `MaxAgeNumBlocks` and +`MaxAgeDuration`. In Proof of Stake chains where validators are bonded, evidence +age should be less than the unbonding period so validators still can be +punished. Given these two propoerties the following initial checks are made. + +1. Has the evidence expired? This is done by taking the height of the `Vote` + within `DuplicateVoteEvidence` or `CommonHeight` within + `LightClientAttakEvidence`. The evidence height is then used to retrieve the + header and thus the time of the block that corresponds to the evidence. If + `CurrentHeight - MaxAgeNumBlocks > EvidenceHeight` && `CurrentTime - + MaxAgeDuration > EvidenceTime`, the evidence is considered expired and + ignored. + +2. Has the evidence already been committed? The evidence pool tracks the hash of + all committed evidence and uses this to determine uniqueness. If a new + evidence has the same hash as a committed one, the new evidence will be + ignored. + +### DuplicateVoteEvidence + +Valid `DuplicateVoteEvidence` must adhere to the following rules: + +- Validator Address, Height, Round and Type must be the same for both votes + +- BlockID must be different for both votes (BlockID can be for a nil block) + +- Validator must have been in the validator set at that height + +- Vote signature must be correctly signed. This also uses `ChainID` so we know + that the fault occurred on this chain + +### LightClientAttackEvidence + +Valid Light Client Attack Evidence must adhere to the following rules: + +- If the header of the light block is invalid, thus indicating a lunatic attack, + the node must check that they can use `verifySkipping` from their header at + the common height to the conflicting header + +- If the header is valid, then the validator sets are the same and this is + either a form of equivocation or amnesia. We therefore check that 2/3 of the + validator set also signed the conflicting header. + +- The nodes own header at the same height as the conflicting header must have a + different hash to the conflicting header. + +- If the nodes latest header is less in height to the conflicting header, then + the node must check that the conflicting block has a time that is less than + this latest header (This is a forward lunatic attack). + +## Gossiping + +If a node verifies evidence it then broadcasts it to all peers, continously sending +the same evidence once every 10 seconds until the evidence is seen on chain or +expires. + +## Commiting on Chain + +Evidence takes strict priority over regular transactions, thus a block is filled +with evidence first and transactions take up the remainder of the space. To +mitigate the threat of an already punished node from spamming the network with +more evidence, the size of the evidence in a block can be capped by +`EvidenceParams.MaxBytes`. Nodes receiving blocks with evidence will validate +the evidence before sending `Prevote` and `Precommit` votes. The evidence pool +will usually cache verifications so that this process is much quicker. + +## Sending Evidence to the Application + +After evidence is committed, the block is then processed by the block executor +which delivers the evidence to the application via `EndBlock`. Evidence is +stripped of the actual proof, split up per faulty validator and only the +validator, height, time and evidence type is sent. + +```proto +enum EvidenceType { + UNKNOWN = 0; + DUPLICATE_VOTE = 1; + LIGHT_CLIENT_ATTACK = 2; +} + +message Evidence { + EvidenceType type = 1; + // The offending validator + Validator validator = 2 [(gogoproto.nullable) = false]; + // The height when the offense occurred + int64 height = 3; + // The corresponding time where the offense occurred + google.protobuf.Timestamp time = 4 [ + (gogoproto.nullable) = false, (gogoproto.stdtime) = true]; + // Total voting power of the validator set in case the ABCI application does + // not store historical validators. + // https://github.com/tendermint/tendermint/issues/4581 + int64 total_voting_power = 5; +} +``` + +`DuplicateVoteEvidence` and `LightClientAttackEvidence` are self-contained in +the sense that the evidence can be used to derive the `abci.Evidence` that is +sent to the application. Because of this, extra fields are necessary: + +```go +type DuplicateVoteEvidence struct { + VoteA *Vote + VoteB *Vote + + // abci specific information + TotalVotingPower int64 + ValidatorPower int64 + Timestamp time.Time +} + +type LightClientAttackEvidence struct { + ConflictingBlock *LightBlock + CommonHeight int64 + + // abci specific information + ByzantineValidators []*Validator + TotalVotingPower int64 + Timestamp time.Time +} +``` + +These ABCI specific fields don't affect validity of the evidence itself but must +be consistent amongst nodes and agreed upon on chain. If evidence with the +incorrect abci information is sent, a node will create new evidence from it and +replace the ABCI fields with the correct information. diff --git a/spec/consensus/light-client/README.md b/spec/consensus/light-client/README.md new file mode 100644 index 000000000..44b9e0c76 --- /dev/null +++ b/spec/consensus/light-client/README.md @@ -0,0 +1,9 @@ +--- +order: 1 +parent: + title: Light Client + order: false +--- +# Tendermint Light Client Protocol + +Deprecated, please see [light-client](../../light-client/README.md). diff --git a/spec/consensus/light-client/accountability.md b/spec/consensus/light-client/accountability.md new file mode 100644 index 000000000..5cf46b0b4 --- /dev/null +++ b/spec/consensus/light-client/accountability.md @@ -0,0 +1,3 @@ +# Fork accountability + +Deprecated, please see [light-client/accountability](../../light-client/accountability.md). diff --git a/spec/consensus/light-client/assets/light-node-image.png b/spec/consensus/light-client/assets/light-node-image.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b93c6e415ebc39bc2313c885faacb35aa7af9a GIT binary patch literal 122270 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ɸa2^db4^;{u=6(Fa8oE-0a}M)8Mf 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

#_$y479`m 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 yG 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 NF6my(J#Y2ke*p{XTJ-<` literal 0 HcmV?d00001 diff --git a/spec/consensus/light-client/detection.md b/spec/consensus/light-client/detection.md new file mode 100644 index 000000000..5c87562ba --- /dev/null +++ b/spec/consensus/light-client/detection.md @@ -0,0 +1,3 @@ +# Detection + +Deprecated, please see [light-client/detection](../../light-client/detection.md). diff --git a/spec/consensus/light-client/verification.md b/spec/consensus/light-client/verification.md new file mode 100644 index 000000000..1f0104a40 --- /dev/null +++ b/spec/consensus/light-client/verification.md @@ -0,0 +1,3 @@ +# Core Verification + +Deprecated, please see [light-client/accountability](../../light-client/verification.md). diff --git a/spec/consensus/proposer-selection.md b/spec/consensus/proposer-selection.md new file mode 100644 index 000000000..3cea3d5cd --- /dev/null +++ b/spec/consensus/proposer-selection.md @@ -0,0 +1,323 @@ +--- +order: 3 +--- + +# Proposer Selection Procedure + +This document specifies the Proposer Selection Procedure that is used in Tendermint to choose a round proposer. +As Tendermint is “leader-based protocol”, the proposer selection is critical for its correct functioning. + +At a given block height, the proposer selection algorithm runs with the same validator set at each round . +Between heights, an updated validator set may be specified by the application as part of the ABCIResponses' EndBlock. + +## Requirements for Proposer Selection + +This sections covers the requirements with Rx being mandatory and Ox optional requirements. +The following requirements must be met by the Proposer Selection procedure: + +### R1: Determinism + +Given a validator set `V`, and two honest validators `p` and `q`, for each height `h` and each round `r` the following must hold: + + `proposer_p(h,r) = proposer_q(h,r)` + +where `proposer_p(h,r)` is the proposer returned by the Proposer Selection Procedure at process `p`, at height `h` and round `r`. + +### R2: Fairness + +Given a validator set with total voting power P and a sequence S of elections. In any sub-sequence of S with length C*P, a validator v must be elected as proposer P/VP(v) times, i.e. with frequency: + + f(v) ~ VP(v) / P + +where C is a tolerance factor for validator set changes with following values: + +- C == 1 if there are no validator set changes +- C ~ k when there are validator changes + +*[this needs more work]* + +## Basic Algorithm + +At its core, the proposer selection procedure uses a weighted round-robin algorithm. + +A model that gives a good intuition on how/ why the selection algorithm works and it is fair is that of a priority queue. The validators move ahead in this queue according to their voting power (the higher the voting power the faster a validator moves towards the head of the queue). When the algorithm runs the following happens: + +- all validators move "ahead" according to their powers: for each validator, increase the priority by the voting power +- first in the queue becomes the proposer: select the validator with highest priority +- move the proposer back in the queue: decrease the proposer's priority by the total voting power + +Notation: + +- vset - the validator set +- n - the number of validators +- VP(i) - voting power of validator i +- A(i) - accumulated priority for validator i +- P - total voting power of set +- avg - average of all validator priorities +- prop - proposer + +Simple view at the Selection Algorithm: + +```md + def ProposerSelection (vset): + + // compute priorities and elect proposer + for each validator i in vset: + A(i) += VP(i) + prop = max(A) + A(prop) -= P +``` + +## Stable Set + +Consider the validator set: + +Validator | p1 | p2 +----------|----|--- +VP | 1 | 3 + +Assuming no validator changes, the following table shows the proposer priority computation over a few runs. Four runs of the selection procedure are shown, starting with the 5th the same values are computed. +Each row shows the priority queue and the process place in it. The proposer is the closest to the head, the rightmost validator. As priorities are updated, the validators move right in the queue. The proposer moves left as its priority is reduced after election. + +| Priority Run | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | Alg step | +|----------------|----|----|-------|----|-------|----|----|----|------------------| +| | | | p1,p2 | | | | | | Initialized to 0 | +| run 1 | | | | p1 | | p2 | | | A(i)+=VP(i) | +| | | p2 | | p1 | | | | | A(p2)-= P | +| run 2 | | | | | p1,p2 | | | | A(i)+=VP(i) | +| | p1 | | | | p2 | | | | A(p1)-= P | +| run 3 | | p1 | | | | | | p2 | A(i)+=VP(i) | +| | | p1 | | p2 | | | | | A(p2)-= P | +| run 4 | | | p1 | | | | p2 | | A(i)+=VP(i) | +| | | | p1,p2 | | | | | | A(p2)-= P | + +It can be shown that: + +- At the end of each run k+1 the sum of the priorities is the same as at end of run k. If a new set's priorities are initialized to 0 then the sum of priorities will be 0 at each run while there are no changes. +- The max distance between priorites is (n-1) *P.*[formal proof not finished]* + +## Validator Set Changes + +Between proposer selection runs the validator set may change. Some changes have implications on the proposer election. + +### Voting Power Change + +Consider again the earlier example and assume that the voting power of p1 is changed to 4: + +Validator | p1 | p2 +----------|----|--- +VP | 4 | 3 + +Let's also assume that before this change the proposer priorites were as shown in first row (last run). As it can be seen, the selection could run again, without changes, as before. + +| Priority Run | -2 | -1 | 0 | 1 | 2 | Comment | +|----------------|----|----|---|----|----|-------------------| +| last run | | p2 | | p1 | | __update VP(p1)__ | +| next run | | | | | p2 | A(i)+=VP(i) | +| | p1 | | | | p2 | A(p1)-= P | + +However, when a validator changes power from a high to a low value, some other validator remain far back in the queue for a long time. This scenario is considered again in the Proposer Priority Range section. + +As before: + +- At the end of each run k+1 the sum of the priorities is the same as at run k. +- The max distance between priorites is (n-1) * P. + +### Validator Removal + +Consider a new example with set: + +Validator | p1 | p2 | p3 +----------|----|----|--- +VP | 1 | 2 | 3 + +Let's assume that after the last run the proposer priorities were as shown in first row with their sum being 0. After p2 is removed, at the end of next proposer selection run (penultimate row) the sum of priorities is -2 (minus the priority of the removed process). + +The procedure could continue without modifications. However, after a sufficiently large number of modifications in validator set, the priority values would migrate towards maximum or minimum allowed values causing truncations due to overflow detection. +For this reason, the selection procedure adds another __new step__ that centers the current priority values such that the priority sum remains close to 0. + +| Priority Run | -3 | -2 | -1 | 0 | 1 | 2 | 4 | Comment | +|----------------|----|----|----|---|----|----|---|-----------------------| +| last run | p3 | | | | p1 | p2 | | __remove p2__ | +| nextrun | | | | | | | | | +| __new step__ | | p3 | | | | p1 | | A(i) -= avg, avg = -1 | +| | | | | | p3 | p1 | | A(i)+=VP(i) | +| | | | p1 | | p3 | | | A(p1)-= P | + +The modified selection algorithm is: + +```md + def ProposerSelection (vset): + + // center priorities around zero + avg = sum(A(i) for i in vset)/len(vset) + for each validator i in vset: + A(i) -= avg + + // compute priorities and elect proposer + for each validator i in vset: + A(i) += VP(i) + prop = max(A) + A(prop) -= P +``` + +Observations: + +- The sum of priorities is now close to 0. Due to integer division the sum is an integer in (-n, n), where n is the number of validators. + +### New Validator + +When a new validator is added, same problem as the one described for removal appears, the sum of priorities in the new set is not zero. This is fixed with the centering step introduced above. + +One other issue that needs to be addressed is the following. A validator V that has just been elected is moved to the end of the queue. If the validator set is large and/ or other validators have significantly higher power, V will have to wait many runs to be elected. If V removes and re-adds itself to the set, it would make a significant (albeit unfair) "jump" ahead in the queue. + +In order to prevent this, when a new validator is added, its initial priority is set to: + +```md + A(V) = -1.125 * P +``` + +where P is the total voting power of the set including V. + +Curent implementation uses the penalty factor of 1.125 because it provides a small punishment that is efficient to calculate. See [here](https://github.com/tendermint/tendermint/pull/2785#discussion_r235038971) for more details. + +If we consider the validator set where p3 has just been added: + +Validator | p1 | p2 | p3 +----------|----|----|--- +VP | 1 | 3 | 8 + +then p3 will start with proposer priority: + +```md + A(p3) = -1.125 * (1 + 3 + 8) ~ -13 +``` + +Note that since current computation uses integer division there is penalty loss when sum of the voting power is less than 8. + +In the next run, p3 will still be ahead in the queue, elected as proposer and moved back in the queue. + +| Priority Run | -13 | -9 | -5 | -2 | -1 | 0 | 1 | 2 | 5 | 6 | 7 | Alg step | +|----------------|-----|----|----|----|----|---|---|----|----|----|----|-----------------------| +| last run | | | | p2 | | | | p1 | | | | __add p3__ | +| | p3 | | | p2 | | | | p1 | | | | A(p3) = -4 | +| next run | | p3 | | | | | | p2 | | p1 | | A(i) -= avg, avg = -4 | +| | | | | | p3 | | | | p2 | | p1 | A(i)+=VP(i) | +| | | | p1 | | p3 | | | | p2 | | | A(p1)-=P | + +## Proposer Priority Range + +With the introduction of centering, some interesting cases occur. Low power validators that bind early in a set that includes high power validator(s) benefit from subsequent additions to the set. This is because these early validators run through more right shift operations during centering, operations that increase their priority. + +As an example, consider the set where p2 is added after p1, with priority -1.125 * 80k = -90k. After the selection procedure runs once: + +Validator | p1 | p2 | Comment +----------|------|------|------------------ +VP | 80k | 10 | +A | 0 | -90k | __added p2__ +A | -45k | 45k | __run selection__ + +Then execute the following steps: + +1. Add a new validator p3: + + Validator | p1 | p2 | p3 + ----------|-----|----|--- + VP | 80k | 10 | 10 + +2. Run selection once. The notation '..p'/'p..' means very small deviations compared to column priority. + + | Priority Run | -90k.. | -60k | -45k | -15k | 0 | 45k | 75k | 155k | Comment | + |---------------|--------|------|------|------|---|-----|-----|------|--------------| + | last run | p3 | | p2 | | | p1 | | | __added p3__ | + | next run + | *right_shift*| | p3 | | p2 | | | p1 | | A(i) -= avg,avg=-30k + | | | ..p3| | ..p2| | | | p1 | A(i)+=VP(i) + | | | ..p3| | ..p2| | | p1.. | | A(p1)-=P, P=80k+20 + +3. Remove p1 and run selection once: + + Validator | p3 | p2 | Comment + ----------|--------|-------|------------------ + VP | 10 | 10 | + A | -60k | -15k | + A | -22.5k | 22.5k | __run selection__ + +At this point, while the total voting power is 20, the distance between priorities is 45k. It will take 4500 runs for p3 to catch up with p2. + +In order to prevent these types of scenarios, the selection algorithm performs scaling of priorities such that the difference between min and max values is smaller than two times the total voting power. + +The modified selection algorithm is: + +```md + def ProposerSelection (vset): + + // scale the priority values + diff = max(A)-min(A) + threshold = 2 * P + if diff > threshold: + scale = diff/threshold + for each validator i in vset: + A(i) = A(i)/scale + + // center priorities around zero + avg = sum(A(i) for i in vset)/len(vset) + for each validator i in vset: + A(i) -= avg + + // compute priorities and elect proposer + for each validator i in vset: + A(i) += VP(i) + prop = max(A) + A(prop) -= P +``` + +Observations: + +- With this modification, the maximum distance between priorites becomes 2 * P. + +Note also that even during steady state the priority range may increase beyond 2 * P. The scaling introduced here helps to keep the range bounded. + +## Wrinkles + +### Validator Power Overflow Conditions + +The validator voting power is a positive number stored as an int64. When a validator is added the `1.125 * P` computation must not overflow. As a consequence the code handling validator updates (add and update) checks for overflow conditions making sure the total voting power is never larger than the largest int64 `MAX`, with the property that `1.125 * MAX` is still in the bounds of int64. Fatal error is return when overflow condition is detected. + +### Proposer Priority Overflow/ Underflow Handling + +The proposer priority is stored as an int64. The selection algorithm performs additions and subtractions to these values and in the case of overflows and underflows it limits the values to: + +```go + MaxInt64 = 1 << 63 - 1 + MinInt64 = -1 << 63 +``` + +## Requirement Fulfillment Claims + +__[R1]__ + +The proposer algorithm is deterministic giving consistent results across executions with same transactions and validator set modifications. +[WIP - needs more detail] + +__[R2]__ + +Given a set of processes with the total voting power P, during a sequence of elections of length P, the number of times any process is selected as proposer is equal to its voting power. The sequence of the P proposers then repeats. If we consider the validator set: + +Validator | p1 | p2 +----------|----|--- +VP | 1 | 3 + +With no other changes to the validator set, the current implementation of proposer selection generates the sequence: +`p2, p1, p2, p2, p2, p1, p2, p2,...` or [`p2, p1, p2, p2`]* +A sequence that starts with any circular permutation of the [`p2, p1, p2, p2`] sub-sequence would also provide the same degree of fairness. In fact these circular permutations show in the sliding window (over the generated sequence) of size equal to the length of the sub-sequence. + +Assigning priorities to each validator based on the voting power and updating them at each run ensures the fairness of the proposer selection. In addition, every time a validator is elected as proposer its priority is decreased with the total voting power. + +Intuitively, a process v jumps ahead in the queue at most (max(A) - min(A))/VP(v) times until it reaches the head and is elected. The frequency is then: + +```md + f(v) ~ VP(v)/(max(A)-min(A)) = 1/k * VP(v)/P +``` + +For current implementation, this means v should be proposer at least VP(v) times out of k * P runs, with scaling factor k=2. diff --git a/spec/consensus/readme.md b/spec/consensus/readme.md new file mode 100644 index 000000000..aa79ba192 --- /dev/null +++ b/spec/consensus/readme.md @@ -0,0 +1,32 @@ +--- +order: 1 +parent: + title: Consensus + order: 4 +--- + +# Consensus + +Specification of the Tendermint consensus protocol. + +## Contents + +- [Consensus Paper](./consensus-paper) - Latex paper on + [arxiv](https://arxiv.org/abs/1807.04938) describing the + core Tendermint consensus state machine with proofs of safety and termination. +- [BFT Time](./bft-time.md) - How the timestamp in a Tendermint + block header is computed in a Byzantine Fault Tolerant manner +- [Creating Proposal](./creating-proposal.md) - How a proposer + creates a block proposal for consensus +- [Light Client Protocol](./light-client) - A protocol for light weight consensus + verification and syncing to the latest state +- [Signing](./signing.md) - Rules for cryptographic signatures + produced by validators. +- [Write Ahead Log](./wal.md) - Write ahead log used by the + consensus state machine to recover from crashes. + +The protocol used to gossip consensus messages between peers, which is critical +for liveness, is described in the [reactors section](../reactors/consensus/consensus.md). + +There is also a [stale markdown description](consensus.md) of the consensus state machine +(TODO update this). diff --git a/spec/consensus/signing.md b/spec/consensus/signing.md new file mode 100644 index 000000000..907a5a01a --- /dev/null +++ b/spec/consensus/signing.md @@ -0,0 +1,229 @@ +# Validator Signing + +Here we specify the rules for validating a proposal and vote before signing. +First we include some general notes on validating data structures common to both types. +We then provide specific validation rules for each. Finally, we include validation rules to prevent double-sigining. + +## SignedMsgType + +The `SignedMsgType` is a single byte that refers to the type of the message +being signed. It is defined in Go as follows: + +```go +// SignedMsgType is a type of signed message in the consensus. +type SignedMsgType byte + +const ( + // Votes + PrevoteType SignedMsgType = 0x01 + PrecommitType SignedMsgType = 0x02 + + // Proposals + ProposalType SignedMsgType = 0x20 +) +``` + +All signed messages must correspond to one of these types. + +## Timestamp + +Timestamp validation is subtle and there are currently no bounds placed on the +timestamp included in a proposal or vote. It is expected that validators will honestly +report their local clock time. The median of all timestamps +included in a commit is used as the timestamp for the next block height. + +Timestamps are expected to be strictly monotonic for a given validator, though +this is not currently enforced. + +## ChainID + +ChainID is an unstructured string with a max length of 50-bytes. +In the future, the ChainID may become structured, and may take on longer lengths. +For now, it is recommended that signers be configured for a particular ChainID, +and to only sign votes and proposals corresponding to that ChainID. + +## BlockID + +BlockID is the structure used to represent the block: + +```go +type BlockID struct { + Hash []byte + PartsHeader PartSetHeader +} + +type PartSetHeader struct { + Hash []byte + Total int +} +``` + +To be included in a valid vote or proposal, BlockID must either represent a `nil` block, or a complete one. +We introduce two methods, `BlockID.IsZero()` and `BlockID.IsComplete()` for these cases, respectively. + +`BlockID.IsZero()` returns true for BlockID `b` if each of the following +are true: + +```go +b.Hash == nil +b.PartsHeader.Total == 0 +b.PartsHeader.Hash == nil +``` + +`BlockID.IsComplete()` returns true for BlockID `b` if each of the following +are true: + +```go +len(b.Hash) == 32 +b.PartsHeader.Total > 0 +len(b.PartsHeader.Hash) == 32 +``` + +## Proposals + +The structure of a proposal for signing looks like: + +```go +type CanonicalProposal struct { + Type SignedMsgType // type alias for byte + Height int64 `binary:"fixed64"` + Round int64 `binary:"fixed64"` + POLRound int64 `binary:"fixed64"` + BlockID BlockID + Timestamp time.Time + ChainID string +} +``` + +A proposal is valid if each of the following lines evaluates to true for proposal `p`: + +```go +p.Type == 0x20 +p.Height > 0 +p.Round >= 0 +p.POLRound >= -1 +p.BlockID.IsComplete() +``` + +In other words, a proposal is valid for signing if it contains the type of a Proposal +(0x20), has a positive, non-zero height, a +non-negative round, a POLRound not less than -1, and a complete BlockID. + +## Votes + +The structure of a vote for signing looks like: + +```go +type CanonicalVote struct { + Type SignedMsgType // type alias for byte + Height int64 `binary:"fixed64"` + Round int64 `binary:"fixed64"` + BlockID BlockID + Timestamp time.Time + ChainID string +} +``` + +A vote is valid if each of the following lines evaluates to true for vote `v`: + +```go +v.Type == 0x1 || v.Type == 0x2 +v.Height > 0 +v.Round >= 0 +v.BlockID.IsZero() || v.BlockID.IsComplete() +``` + +In other words, a vote is valid for signing if it contains the type of a Prevote +or Precommit (0x1 or 0x2, respectively), has a positive, non-zero height, a +non-negative round, and an empty or valid BlockID. + +## Invalid Votes and Proposals + +Votes and proposals which do not satisfy the above rules are considered invalid. +Peers gossipping invalid votes and proposals may be disconnected from other peers on the network. +Note, however, that there is not currently any explicit mechanism to punish validators signing votes or proposals that fail +these basic validation rules. + +## Double Signing + +Signers must be careful not to sign conflicting messages, also known as "double signing" or "equivocating". +Tendermint has mechanisms to publish evidence of validators that signed conflicting votes, so they can be punished +by the application. Note Tendermint does not currently handle evidence of conflciting proposals, though it may in the future. + +### State + +To prevent such double signing, signers must track the height, round, and type of the last message signed. +Assume the signer keeps the following state, `s`: + +```go +type LastSigned struct { + Height int64 + Round int64 + Type SignedMsgType // byte +} +``` + +After signing a vote or proposal `m`, the signer sets: + +```go +s.Height = m.Height +s.Round = m.Round +s.Type = m.Type +``` + +### Proposals + +A signer should only sign a proposal `p` if any of the following lines are true: + +```go +p.Height > s.Height +p.Height == s.Height && p.Round > s.Round +``` + +In other words, a proposal should only be signed if it's at a higher height, or a higher round for the same height. +Once a proposal or vote has been signed for a given height and round, a proposal should never be signed for the same height and round. + +### Votes + +A signer should only sign a vote `v` if any of the following lines are true: + +```go +v.Height > s.Height +v.Height == s.Height && v.Round > s.Round +v.Height == s.Height && v.Round == s.Round && v.Step == 0x1 && s.Step == 0x20 +v.Height == s.Height && v.Round == s.Round && v.Step == 0x2 && s.Step != 0x2 +``` + +In other words, a vote should only be signed if it's: + +- at a higher height +- at a higher round for the same height +- a prevote for the same height and round where we haven't signed a prevote or precommit (but have signed a proposal) +- a precommit for the same height and round where we haven't signed a precommit (but have signed a proposal and/or a prevote) + +This means that once a validator signs a prevote for a given height and round, the only other message it can sign for that height and round is a precommit. +And once a validator signs a precommit for a given height and round, it must not sign any other message for that same height and round. + +Note this includes votes for `nil`, ie. where `BlockID.IsZero()` is true. If a +signer has already signed a vote where `BlockID.IsZero()` is true, it cannot +sign another vote with the same type for the same height and round where +`BlockID.IsComplete()` is true. Thus only a single vote of a particular type +(ie. 0x01 or 0x02) can be signed for the same height and round. + +### Other Rules + +According to the rules of Tendermint consensus, once a validator precommits for +a block, they become "locked" on that block, which means they can't prevote for +another block unless they see sufficient justification (ie. a polka from a +higher round). For more details, see the [consensus +spec](https://arxiv.org/abs/1807.04938). + +Violating this rule is known as "amnesia". In contrast to equivocation, +which is easy to detect, amnesia is difficult to detect without access to votes +from all the validators, as this is what constitutes the justification for +"unlocking". Hence, amnesia is not punished within the protocol, and cannot +easily be prevented by a signer. If enough validators simultaneously commit an +amnesia attack, they may cause a fork of the blockchain, at which point an +off-chain protocol must be engaged to collect votes from all the validators and +determine who misbehaved. For more details, see [fork +detection](https://github.com/tendermint/tendermint/pull/3978). diff --git a/spec/consensus/wal.md b/spec/consensus/wal.md new file mode 100644 index 000000000..95d1bad12 --- /dev/null +++ b/spec/consensus/wal.md @@ -0,0 +1,32 @@ +# WAL + +Consensus module writes every message to the WAL (write-ahead log). + +It also issues fsync syscall through +[File#Sync](https://golang.org/pkg/os/#File.Sync) for messages signed by this +node (to prevent double signing). + +Under the hood, it uses +[autofile.Group](https://godoc.org/github.com/tendermint/tmlibs/autofile#Group), +which rotates files when those get too big (> 10MB). + +The total maximum size is 1GB. We only need the latest block and the block before it, +but if the former is dragging on across many rounds, we want all those rounds. + +## Replay + +Consensus module will replay all the messages of the last height written to WAL +before a crash (if such occurs). + +The private validator may try to sign messages during replay because it runs +somewhat autonomously and does not know about replay process. + +For example, if we got all the way to precommit in the WAL and then crash, +after we replay the proposal message, the private validator will try to sign a +prevote. But it will fail. That's ok because we’ll see the prevote later in the +WAL. Then it will go to precommit, and that time it will work because the +private validator contains the `LastSignBytes` and then we’ll replay the +precommit from the WAL. + +Make sure to read about [WAL corruption](https://github.com/tendermint/tendermint/blob/master/docs/tendermint-core/running-in-production.md#wal-corruption) +and recovery strategies. diff --git a/spec/core/data_structures.md b/spec/core/data_structures.md new file mode 100644 index 000000000..87bea298f --- /dev/null +++ b/spec/core/data_structures.md @@ -0,0 +1,456 @@ +# Data Structures + +Here we describe the data structures in the Tendermint blockchain and the rules for validating them. + +The Tendermint blockchains consists of a short list of data types: + +- [Data Structures](#data-structures) + - [Block](#block) + - [Execution](#execution) + - [Header](#header) + - [Version](#version) + - [BlockID](#blockid) + - [PartSetHeader](#partsetheader) + - [Part](#part) + - [Time](#time) + - [Data](#data) + - [Commit](#commit) + - [CommitSig](#commitsig) + - [BlockIDFlag](#blockidflag) + - [Vote](#vote) + - [CanonicalVote](#canonicalvote) + - [Proposal](#proposal) + - [SignedMsgType](#signedmsgtype) + - [Signature](#signature) + - [EvidenceList](#evidencelist) + - [Evidence](#evidence) + - [DuplicateVoteEvidence](#duplicatevoteevidence) + - [LightClientAttackEvidence](#lightclientattackevidence) + - [LightBlock](#lightblock) + - [SignedHeader](#signedheader) + - [ValidatorSet](#validatorset) + - [Validator](#validator) + - [Address](#address) + - [ConsensusParams](#consensusparams) + - [BlockParams](#blockparams) + - [EvidenceParams](#evidenceparams) + - [ValidatorParams](#validatorparams) + - [VersionParams](#versionparams) + - [Proof](#proof) + + +## Block + +A block consists of a header, transactions, votes (the commit), +and a list of evidence of malfeasance (ie. signing conflicting votes). + +| Name | Type | Description | Validation | +|--------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| +| Header | [Header](#header) | Header corresponding to the block. This field contains information used throughout consensus and other areas of the protocol. To find out what it contains, visit [header] (#header) | Must adhere to the validation rules of [header](#header) | +| Data | [Data](#data) | Data contains a list of transactions. The contents of the transaction is unknown to Tendermint. | This field can be empty or populated, but no validation is performed. Applications can perform validation on individual transactions prior to block creation using [checkTx](../abci/abci.md#checktx). +| Evidence | [EvidenceList](#evidence_list) | Evidence contains a list of infractions committed by validators. | Can be empty, but when populated the validations rules from [evidenceList](#evidence_list) apply | +| LastCommit | [Commit](#commit) | `LastCommit` includes one vote for every validator. All votes must either be for the previous block, nil or absent. If a vote is for the previous block it must have a valid signature from the corresponding validator. The sum of the voting power of the validators that voted must be greater than 2/3 of the total voting power of the complete validator set. The number of votes in a commit is limited to 10000 (see `types.MaxVotesCount`). | Must be empty for the initial height and must adhere to the validation rules of [commit](#commit). | + +## Execution + +Once a block is validated, it can be executed against the state. + +The state follows this recursive equation: + +```go +state(initialHeight) = InitialState +state(h+1) <- Execute(state(h), ABCIApp, block(h)) +``` + +where `InitialState` includes the initial consensus parameters and validator set, +and `ABCIApp` is an ABCI application that can return results and changes to the validator +set (TODO). Execute is defined as: + +```go +func Execute(s State, app ABCIApp, block Block) State { + // Fuction ApplyBlock executes block of transactions against the app and returns the new root hash of the app state, + // modifications to the validator set and the changes of the consensus parameters. + AppHash, ValidatorChanges, ConsensusParamChanges := app.ApplyBlock(block) + + nextConsensusParams := UpdateConsensusParams(state.ConsensusParams, ConsensusParamChanges) + return State{ + ChainID: state.ChainID, + InitialHeight: state.InitialHeight, + LastResults: abciResponses.DeliverTxResults, + AppHash: AppHash, + InitialHeight: state.InitialHeight, + LastValidators: state.Validators, + Validators: state.NextValidators, + NextValidators: UpdateValidators(state.NextValidators, ValidatorChanges), + ConsensusParams: nextConsensusParams, + Version: { + Consensus: { + AppVersion: nextConsensusParams.Version.AppVersion, + }, + }, + } +} +``` + +Validating a new block is first done prior to the `prevote`, `precommit` & `finalizeCommit` stages. + +The steps to validate a new block are: + +- Check the validity rules of the block and its fields. +- Check the versions (Block & App) are the same as in local state. +- Check the chainID's match. +- Check the height is correct. +- Check the `LastBlockID` corresponds to BlockID currently in state. +- Check the hashes in the header match those in state. +- Verify the LastCommit against state, this step is skipped for the initial height. + - This is where checking the signatures correspond to the correct block will be made. +- Make sure the proposer is part of the validator set. +- Validate bock time. + - Make sure the new blocks time is after the previous blocks time. + - Calculate the medianTime and check it against the blocks time. + - If the blocks height is the initial height then check if it matches the genesis time. +- Validate the evidence in the block. Note: Evidence can be empty + +## Header + +A block header contains metadata about the block and about the consensus, as well as commitments to +the data in the current block, the previous block, and the results returned by the application: + +| Name | Type | Description | Validation | +|-------------------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Version | [Version](#version) | Version defines the application and protocol version being used. | Must adhere to the validation rules of [Version](#version) | +| ChainID | String | ChainID is the ID of the chain. This must be unique to your chain. | ChainID must be less than 50 bytes. | +| Height | uint64 | Height is the height for this header. | Must be > 0, >= initialHeight, and == previous Height+1 | +| Time | [Time](#time) | The timestamp is equal to the weighted median of validators present in the last commit. Read more on time in the [BFT-time section](../consensus/bft-time.md). Note: the timestamp of a vote must be greater by at least one millisecond than that of the block being voted on. | Time must be >= previous header timestamp + consensus parameters TimeIotaMs. The timestamp of the first block must be equal to the genesis time (since there's no votes to compute the median). | +| LastBlockID | [BlockID](#blockid) | BlockID of the previous block. | Must adhere to the validation rules of [blockID](#blockid). The first block has `block.Header.LastBlockID == BlockID{}`. | +| LastCommitHash | slice of bytes (`[]byte`) | MerkleRoot of the lastCommit's signatures. The signatures represent the validators that committed to the last block. The first block has an empty slices of bytes for the hash. | Must be of length 32 | +| DataHash | slice of bytes (`[]byte`) | MerkleRoot of the hash of transactions. **Note**: The transactions are hashed before being included in the merkle tree, the leaves of the Merkle tree are the hashes, not the transactions themselves. | Must be of length 32 | +| ValidatorHash | slice of bytes (`[]byte`) | MerkleRoot of the current validator set. The validators are first sorted by voting power (descending), then by address (ascending) prior to computing the MerkleRoot. | Must be of length 32 | +| NextValidatorHash | slice of bytes (`[]byte`) | MerkleRoot of the next validator set. The validators are first sorted by voting power (descending), then by address (ascending) prior to computing the MerkleRoot. | Must be of length 32 | +| ConsensusHash | slice of bytes (`[]byte`) | Hash of the protobuf encoded consensus parameters. | Must be of length 32 | +| AppHash | slice of bytes (`[]byte`) | Arbitrary byte array returned by the application after executing and commiting the previous block. It serves as the basis for validating any merkle proofs that comes from the ABCI application and represents the state of the actual application rather than the state of the blockchain itself. The first block's `block.Header.AppHash` is given by `ResponseInitChain.app_hash`. | This hash is determined by the application, Tendermint can not perform validation on it. | +| LastResultHash | slice of bytes (`[]byte`) | `LastResultsHash` is the root hash of a Merkle tree built from `ResponseDeliverTx` responses (`Log`,`Info`, `Codespace` and `Events` fields are ignored). | Must be of length 32. The first block has `block.Header.ResultsHash == MerkleRoot(nil)`, i.e. the hash of an empty input, for RFC-6962 conformance. | +| EvidenceHash | slice of bytes (`[]byte`) | MerkleRoot of the evidence of Byzantine behaviour included in this block. | Must be of length 32 | +| ProposerAddress | slice of bytes (`[]byte`) | Address of the original proposer of the block. Validator must be in the current validatorSet. | Must be of length 20 | + +## Version + +NOTE: that this is more specifically the consensus version and doesn't include information like the +P2P Version. (TODO: we should write a comprehensive document about +versioning that this can refer to) + +| Name | type | Description | Validation | +|-------|--------|-----------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------| +| Block | uint64 | This number represents the version of the block protocol and must be the same throughout an operational network | Must be equal to protocol version being used in a network (`block.Version.Block == state.Version.Consensus.Block`) | +| App | uint64 | App version is decided on by the application. Read [here](../abci/abci.md#info) | `block.Version.App == state.Version.Consensus.App` | + +## BlockID + +The `BlockID` contains two distinct Merkle roots of the block. The `BlockID` includes these two hashes, as well as the number of parts (ie. `len(MakeParts(block))`) + +| Name | Type | Description | Validation | +|---------------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------| +| Hash | slice of bytes (`[]byte`) | MerkleRoot of all the fields in the header (ie. `MerkleRoot(header)`. | hash must be of length 32 | +| PartSetHeader | [PartSetHeader](#PartSetHeader) | Used for secure gossiping of the block during consensus, is the MerkleRoot of the complete serialized block cut into parts (ie. `MerkleRoot(MakeParts(block))`). | Must adhere to the validation rules of [PartSetHeader](#PartSetHeader) | + +See [MerkleRoot](./encoding.md#MerkleRoot) for details. + +## PartSetHeader + +| Name | Type | Description | Validation | +|-------|---------------------------|-----------------------------------|----------------------| +| Total | int32 | Total amount of parts for a block | Must be > 0 | +| Hash | slice of bytes (`[]byte`) | MerkleRoot of a serialized block | Must be of length 32 | + +## Part + +Part defines a part of a block. In Tendermint blocks are broken into `parts` for gossip. + +| Name | Type | Description | Validation | +|-------|-----------------|-----------------------------------|----------------------| +| index | int32 | Total amount of parts for a block | Must be > 0 | +| bytes | bytes | MerkleRoot of a serialized block | Must be of length 32 | +| proof | [Proof](#proof) | MerkleRoot of a serialized block | Must be of length 32 | + +## Time + +Tendermint uses the [Google.Protobuf.Timestamp](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp) +format, which uses two integers, one 64 bit integer for Seconds and a 32 bit integer for Nanoseconds. + +## Data + +Data is just a wrapper for a list of transactions, where transactions are arbitrary byte arrays: + +| Name | Type | Description | Validation | +|------|----------------------------|------------------------|-----------------------------------------------------------------------------| +| Txs | Matrix of bytes ([][]byte) | Slice of transactions. | Validation does not occur on this field, this data is unknown to Tendermint | + +## Commit + +Commit is a simple wrapper for a list of signatures, with one for each validator. It also contains the relevant BlockID, height and round: + +| Name | Type | Description | Validation | +|------------|----------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| +| Height | uint64 | Height at which this commit was created. | Must be > 0 | +| Round | int32 | Round that the commit corresponds to. | Must be > 0 | +| BlockID | [BlockID](#blockid) | The blockID of the corresponding block. | Must adhere to the validation rules of [BlockID](#blockid). | +| Signatures | Array of [CommitSig](#commitsig) | Array of commit signatures that correspond to current validator set. | Length of signatures must be > 0 and adhere to the validation of each individual [Commitsig](#commitsig) | + +## CommitSig + +`CommitSig` represents a signature of a validator, who has voted either for nil, +a particular `BlockID` or was absent. It's a part of the `Commit` and can be used +to reconstruct the vote set given the validator set. + +| Name | Type | Description | Validation | +|------------------|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------| +| BlockIDFlag | [BlockIDFlag](#blockidflag) | Represents the validators participation in consensus: Either voted for the block that received the majority, voted for another block, voted nil or did not vote | Must be one of the fields in the [BlockIDFlag](#blockidflag) enum | +| ValidatorAddress | [Address](#address) | Address of the validator | Must be of length 20 | +| Timestamp | [Time](#time) | This field will vary from `CommitSig` to `CommitSig`. It represents the timestamp of the validator. | [Time](#time) | +| Signature | [Signature](#signature) | Signature corresponding to the validators participation in consensus. | The length of the signature must be > 0 and < than 64 | + +NOTE: `ValidatorAddress` and `Timestamp` fields may be removed in the future +(see [ADR-25](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-025-commit.md)). + +## BlockIDFlag + +BlockIDFlag represents which BlockID the [signature](#commitsig) is for. + +```go +enum BlockIDFlag { + BLOCK_ID_FLAG_UNKNOWN = 0; + BLOCK_ID_FLAG_ABSENT = 1; // signatures for other blocks are also considered absent + BLOCK_ID_FLAG_COMMIT = 2; + BLOCK_ID_FLAG_NIL = 3; +} +``` + +## Vote + +A vote is a signed message from a validator for a particular block. +The vote includes information about the validator signing it. When stored in the blockchain or propagated over the network, votes are encoded in Protobuf. + +| Name | Type | Description | Validation | +|------------------|---------------------------------|---------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------| +| Type | [SignedMsgType](#signedmsgtype) | Either prevote or precommit. [SignedMsgType](#signedmsgtype) | A Vote is valid if its corresponding fields are included in the enum [signedMsgType](#signedmsgtype) | +| Height | uint64 | Height for which this vote was created for | Must be > 0 | +| Round | int32 | Round that the commit corresponds to. | Must be > 0 | +| BlockID | [BlockID](#blockid) | The blockID of the corresponding block. | [BlockID](#blockid) | +| Timestamp | [Time](#Time) | Timestamp represents the time at which a validator signed. | [Time](#time) | +| ValidatorAddress | slice of bytes (`[]byte`) | Address of the validator | Length must be equal to 20 | +| ValidatorIndex | int32 | Index at a specific block height that corresponds to the Index of the validator in the set. | must be > 0 | +| Signature | slice of bytes (`[]byte`) | Signature by the validator if they participated in consensus for the associated bock. | Length of signature must be > 0 and < 64 | + +## CanonicalVote + +CanonicalVote is for validator signing. This type will not be present in a block. Votes are represented via `CanonicalVote` and also encoded using protobuf via `type.SignBytes` which includes the `ChainID`, and uses a different ordering of +the fields. + +```proto +message CanonicalVote { + SignedMsgType type = 1; + fixed64 height = 2; + sfixed64 round = 3; + CanonicalBlockID block_id = 4; + google.protobuf.Timestamp timestamp = 5; + string chain_id = 6; +} +``` + +For signing, votes are represented via [`CanonicalVote`](#canonicalvote) and also encoded using protobuf via +`type.SignBytes` which includes the `ChainID`, and uses a different ordering of +the fields. + +We define a method `Verify` that returns `true` if the signature verifies against the pubkey for the `SignBytes` +using the given ChainID: + +```go +func (vote *Vote) Verify(chainID string, pubKey crypto.PubKey) error { + if !bytes.Equal(pubKey.Address(), vote.ValidatorAddress) { + return ErrVoteInvalidValidatorAddress + } + + if !pubKey.VerifyBytes(types.VoteSignBytes(chainID), vote.Signature) { + return ErrVoteInvalidSignature + } + return nil +} +``` + +## Proposal + +Proposal contains height and round for which this proposal is made, BlockID as a unique identifier +of proposed block, timestamp, and POLRound (a so-called Proof-of-Lock (POL) round) that is needed for +termination of the consensus. If POLRound >= 0, then BlockID corresponds to the block that +is locked in POLRound. The message is signed by the validator private key. + +| Name | Type | Description | Validation | +|-----------|---------------------------------|---------------------------------------------------------------------------------------|---------------------------------------------------------| +| Type | [SignedMsgType](#signedmsgtype) | Represents a Proposal [SignedMsgType](#signedmsgtype) | Must be `ProposalType` [signedMsgType](#signedmsgtype) | +| Height | uint64 | Height for which this vote was created for | Must be > 0 | +| Round | int32 | Round that the commit corresponds to. | Must be > 0 | +| POLRound | int64 | Proof of lock | Must be > 0 | +| BlockID | [BlockID](#blockid) | The blockID of the corresponding block. | [BlockID](#blockid) | +| Timestamp | [Time](#Time) | Timestamp represents the time at which a validator signed. | [Time](#time) | +| Signature | slice of bytes (`[]byte`) | Signature by the validator if they participated in consensus for the associated bock. | Length of signature must be > 0 and < 64 | + +## SignedMsgType + +Signed message type represents a signed messages in consensus. + +```proto +enum SignedMsgType { + + SIGNED_MSG_TYPE_UNKNOWN = 0; + // Votes + SIGNED_MSG_TYPE_PREVOTE = 1; + SIGNED_MSG_TYPE_PRECOMMIT = 2; + + // Proposal + SIGNED_MSG_TYPE_PROPOSAL = 32; +} +``` + +## Signature + +Signatures in Tendermint are raw bytes representing the underlying signature. + +See the [signature spec](./encoding.md#key-types) for more. + +## EvidenceList + +EvidenceList is a simple wrapper for a list of evidence: + +| Name | Type | Description | Validation | +|----------|--------------------------------|----------------------------------------|-----------------------------------------------------------------| +| Evidence | Array of [Evidence](#evidence) | List of verified [evidence](#evidence) | Validation adheres to individual types of [Evidence](#evidence) | + +## Evidence + +Evidence in Tendermint is used to indicate breaches in the consensus by a validator. + +More information on how evidence works in Tendermint can be found [here](../consensus/evidence.md) + +### DuplicateVoteEvidence + +`DuplicateVoteEvidence` represents a validator that has voted for two different blocks +in the same round of the same height. Votes are lexicographically sorted on `BlockID`. + +| Name | Type | Description | Validation | +|------------------|---------------|--------------------------------------------------------------------|-----------------------------------------------------| +| VoteA | [Vote](#vote) | One of the votes submitted by a validator when they equivocated | VoteA must adhere to [Vote](#vote) validation rules | +| VoteB | [Vote](#vote) | The second vote submitted by a validator when they equivocated | VoteB must adhere to [Vote](#vote) validation rules | +| TotalVotingPower | int64 | The total power of the validator set at the height of equivocation | Must be equal to nodes own copy of the data | +| ValidatorPower | int64 | Power of the equivocating validator at the height | Must be equal to the nodes own copy of the data | +| Timestamp | [Time](#Time) | Time of the block where the equivocation occurred | Must be equal to the nodes own copy of the data | + +### LightClientAttackEvidence + +`LightClientAttackEvidence` is a generalized evidence that captures all forms of known attacks on +a light client such that a full node can verify, propose and commit the evidence on-chain for +punishment of the malicious validators. There are three forms of attacks: Lunatic, Equivocation +and Amnesia. These attacks are exhaustive. You can find a more detailed overview of this [here](../light-client/accountability#the_misbehavior_of_faulty_validators) + +| Name | Type | Description | Validation | +|----------------------|------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------| +| ConflictingBlock | [LightBlock](#LightBlock) | Read Below | Must adhere to the validation rules of [lightBlock](#lightblock) | +| CommonHeight | int64 | Read Below | must be > 0 | +| Byzantine Validators | Array of [Validators](#Validators) | validators that acted maliciously | Read Below | +| TotalVotingPower | int64 | The total power of the validator set at the height of the infraction | Must be equal to the nodes own copy of the data | +| Timestamp | [Time](#Time) | Time of the block where the infraction occurred | Must be equal to the nodes own copy of the data | + +## LightBlock + +LightBlock is the core data structure of the [light client](../light-client/README.md). It combines two data structures needed for verification ([signedHeader](#signedheader) & [validatorSet](#validatorset)). + +| Name | Type | Description | Validation | +|--------------|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------| +| SignedHeader | [SignedHeader](#signedheader) | The header and commit, these are used for verification purposes. To find out more visit [light client docs](../light-client/README.md) | Must not be nil and adhere to the validation rules of [signedHeader](#signedheader) | +| ValidatorSet | [ValidatorSet](#validatorset) | The validatorSet is used to help with verify that the validators in that committed the infraction were truly in the validator set. | Must not be nil and adhere to the validation rules of [validatorSet](#validatorset) | + +The `SignedHeader` and `ValidatorSet` are linked by the hash of the validator set(`SignedHeader.ValidatorsHash == ValidatorSet.Hash()`. + +## SignedHeader + +The SignedhHeader is the [header](#header) accompanied by the commit to prove it. + +| Name | Type | Description | Validation | +|--------|-------------------|-------------------|-----------------------------------------------------------------------------------| +| Header | [Header](#Header) | [Header](#header) | Header cannot be nil and must adhere to the [Header](#Header) validation criteria | +| Commit | [Commit](#commit) | [Commit](#commit) | Commit cannot be nil and must adhere to the [Commit](#commit) criteria | + +## ValidatorSet + +| Name | Type | Description | Validation | +|------------|----------------------------------|----------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| +| Validators | Array of [validator](#validator) | List of the active validators at a specific height | The list of validators can not be empty or nil and must adhere to the validation rules of [validator](#validator) | +| Proposer | [validator](#validator) | The block proposer for the corresponding block | The proposer cannot be nil and must adhere to the validation rules of [validator](#validator) | + +## Validator + +| Name | Type | Description | Validation | +|------------------|---------------------------|---------------------------------------------------------------------------------------------------|---------------------------------------------------| +| Address | [Address](#address) | Validators Address | Length must be of size 20 | +| Pubkey | slice of bytes (`[]byte`) | Validators Public Key | must be a length greater than 0 | +| VotingPower | int64 | Validators voting power | cannot be < 0 | +| ProposerPriority | int64 | Validators proposer priority. This is used to gauge when a validator is up next to propose blocks | No validation, value can be negative and positive | + +## Address + +Address is a type alias of a slice of bytes. The address is calculated by hashing the public key using sha256 and truncating it to only use the first 20 bytes of the slice. + +```go +const ( + TruncatedSize = 20 +) + +func SumTruncated(bz []byte) []byte { + hash := sha256.Sum256(bz) + return hash[:TruncatedSize] +} +``` + +## ConsensusParams + +| Name | Type | Description | Field Number | +|-----------|-------------------------------------|------------------------------------------------------------------------------|--------------| +| block | [BlockParams](#blockparams) | Parameters limiting the size of a block and time between consecutive blocks. | 1 | +| evidence | [EvidenceParams](#evidenceparams) | Parameters limiting the validity of evidence of byzantine behaviour. | 2 | +| validator | [ValidatorParams](#validatorparams) | Parameters limiting the types of public keys validators can use. | 3 | +| version | [BlockParams](#blockparams) | The ABCI application version. | 4 | + +### BlockParams + +| Name | Type | Description | Field Number | +|--------------|-------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| +| max_bytes | int64 | Max size of a block, in bytes. | 1 | +| max_gas | int64 | Max sum of `GasWanted` in a proposed block. NOTE: blocks that violate this may be committed if there are Byzantine proposers. It's the application's responsibility to handle this when processing a block! | 2 | + +### EvidenceParams + +| Name | Type | Description | Field Number | +|--------------------|------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| +| max_age_num_blocks | int64 | Max age of evidence, in blocks. | 1 | +| max_age_duration | [google.protobuf.Duration](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Duration) | Max age of evidence, in time. It should correspond with an app's "unbonding period" or other similar mechanism for handling [Nothing-At-Stake attacks](https://github.com/ethereum/wiki/wiki/Proof-of-Stake-FAQ#what-is-the-nothing-at-stake-problem-and-how-can-it-be-fixed). | 2 | +| max_bytes | int64 | maximum size in bytes of total evidence allowed to be entered into a block | 3 | + +### ValidatorParams + +| Name | Type | Description | Field Number | +|---------------|-----------------|-----------------------------------------------------------------------|--------------| +| pub_key_types | repeated string | List of accepted public key types. Uses same naming as `PubKey.Type`. | 1 | + +### VersionParams + +| Name | Type | Description | Field Number | +|-------------|--------|-------------------------------|--------------| +| app_version | uint64 | The ABCI application version. | 1 | + +## Proof + +| Name | Type | Description | Field Number | +|-----------|----------------|-----------------------------------------------|--------------| +| total | int64 | Total number of items. | 1 | +| index | int64 | Index item to prove. | 2 | +| leaf_hash | bytes | Hash of item value. | 3 | +| aunts | repeated bytes | Hashes from leaf's sibling to a root's child. | 4 | diff --git a/spec/core/encoding.md b/spec/core/encoding.md new file mode 100644 index 000000000..c137575d7 --- /dev/null +++ b/spec/core/encoding.md @@ -0,0 +1,300 @@ +# Encoding + +## Protocol Buffers + +Tendermint uses [Protocol Buffers](https://developers.google.com/protocol-buffers), specifically proto3, for all data structures. + +Please see the [Proto3 language guide](https://developers.google.com/protocol-buffers/docs/proto3) for more details. + +## Byte Arrays + +The encoding of a byte array is simply the raw-bytes prefixed with the length of +the array as a `UVarint` (what proto calls a `Varint`). + +For details on varints, see the [protobuf +spec](https://developers.google.com/protocol-buffers/docs/encoding#varints). + +For example, the byte-array `[0xA, 0xB]` would be encoded as `0x020A0B`, +while a byte-array containing 300 entires beginning with `[0xA, 0xB, ...]` would +be encoded as `0xAC020A0B...` where `0xAC02` is the UVarint encoding of 300. + +## Hashing + +Tendermint uses `SHA256` as its hash function. +Objects are always serialized before being hashed. +So `SHA256(obj)` is short for `SHA256(ProtoEncoding(obj))`. + +## Public Key Cryptography + +Tendermint uses Protobuf [Oneof](https://developers.google.com/protocol-buffers/docs/proto3#oneof) +to distinguish between different types public keys, and signatures. +Additionally, for each public key, Tendermint +defines an Address function that can be used as a more compact identifier in +place of the public key. Here we list the concrete types, their names, +and prefix bytes for public keys and signatures, as well as the address schemes +for each PubKey. Note for brevity we don't +include details of the private keys beyond their type and name. + +### Key Types + +Each type specifies it's own pubkey, address, and signature format. + +#### Ed25519 + +The address is the first 20-bytes of the SHA256 hash of the raw 32-byte public key: + +```go +address = SHA256(pubkey)[:20] +``` + +The signature is the raw 64-byte ED25519 signature. + +Tendermint adopted [zip215](https://zips.z.cash/zip-0215) for verification of ed25519 signatures. + +> Note: This change will be released in the next major release of Tendermint-Go (0.35). + +#### Secp256k1 + +The address is the first 20-bytes of the SHA256 hash of the raw 32-byte public key: + +```go +address = SHA256(pubkey)[:20] +``` + +## Other Common Types + +### BitArray + +The BitArray is used in some consensus messages to represent votes received from +validators, or parts received in a block. It is represented +with a struct containing the number of bits (`Bits`) and the bit-array itself +encoded in base64 (`Elems`). + +| Name | Type | +|-------|----------------------------| +| bits | int64 | +| elems | slice of int64 (`[]int64`) | + +Note BitArray receives a special JSON encoding in the form of `x` and `_` +representing `1` and `0`. Ie. the BitArray `10110` would be JSON encoded as +`"x_xx_"` + +### Part + +Part is used to break up blocks into pieces that can be gossiped in parallel +and securely verified using a Merkle tree of the parts. + +Part contains the index of the part (`Index`), the actual +underlying data of the part (`Bytes`), and a Merkle proof that the part is contained in +the set (`Proof`). + +| Name | Type | +|-------|---------------------------| +| index | uint32 | +| bytes | slice of bytes (`[]byte`) | +| proof | [proof](#merkle-proof) | + +See details of SimpleProof, below. + +### MakeParts + +Encode an object using Protobuf and slice it into parts. +Tendermint uses a part size of 65536 bytes, and allows a maximum of 1601 parts +(see `types.MaxBlockPartsCount`). This corresponds to the hard-coded block size +limit of 100MB. + +```go +func MakeParts(block Block) []Part +``` + +## Merkle Trees + +For an overview of Merkle trees, see +[wikipedia](https://en.wikipedia.org/wiki/Merkle_tree) + +We use the RFC 6962 specification of a merkle tree, with sha256 as the hash function. +Merkle trees are used throughout Tendermint to compute a cryptographic digest of a data structure. +The differences between RFC 6962 and the simplest form a merkle tree are that: + +1. leaf nodes and inner nodes have different hashes. + This is for "second pre-image resistance", to prevent the proof to an inner node being valid as the proof of a leaf. + The leaf nodes are `SHA256(0x00 || leaf_data)`, and inner nodes are `SHA256(0x01 || left_hash || right_hash)`. + +2. When the number of items isn't a power of two, the left half of the tree is as big as it could be. + (The largest power of two less than the number of items) This allows new leaves to be added with less + recomputation. For example: + +```md + Simple Tree with 6 items Simple Tree with 7 items + + * * + / \ / \ + / \ / \ + / \ / \ + / \ / \ + * * * * + / \ / \ / \ / \ + / \ / \ / \ / \ + / \ / \ / \ / \ + * * h4 h5 * * * h6 + / \ / \ / \ / \ / \ +h0 h1 h2 h3 h0 h1 h2 h3 h4 h5 +``` + +### MerkleRoot + +The function `MerkleRoot` is a simple recursive function defined as follows: + +```go +// SHA256([]byte{}) +func emptyHash() []byte { + return tmhash.Sum([]byte{}) +} + +// SHA256(0x00 || leaf) +func leafHash(leaf []byte) []byte { + return tmhash.Sum(append(0x00, leaf...)) +} + +// SHA256(0x01 || left || right) +func innerHash(left []byte, right []byte) []byte { + return tmhash.Sum(append(0x01, append(left, right...)...)) +} + +// largest power of 2 less than k +func getSplitPoint(k int) { ... } + +func MerkleRoot(items [][]byte) []byte{ + switch len(items) { + case 0: + return empthHash() + case 1: + return leafHash(items[0]) + default: + k := getSplitPoint(len(items)) + left := MerkleRoot(items[:k]) + right := MerkleRoot(items[k:]) + return innerHash(left, right) + } +} +``` + +Note: `MerkleRoot` operates on items which are arbitrary byte arrays, not +necessarily hashes. For items which need to be hashed first, we introduce the +`Hashes` function: + +```go +func Hashes(items [][]byte) [][]byte { + return SHA256 of each item +} +``` + +Note: we will abuse notion and invoke `MerkleRoot` with arguments of type `struct` or type `[]struct`. +For `struct` arguments, we compute a `[][]byte` containing the protobuf encoding of each +field in the struct, in the same order the fields appear in the struct. +For `[]struct` arguments, we compute a `[][]byte` by protobuf encoding the individual `struct` elements. + +### Merkle Proof + +Proof that a leaf is in a Merkle tree is composed as follows: + +| Name | Type | +|----------|----------------------------| +| total | int64 | +| index | int64 | +| leafHash | slice of bytes (`[]byte`) | +| aunts | Matrix of bytes ([][]byte) | + +Which is verified as follows: + +```golang +func (proof Proof) Verify(rootHash []byte, leaf []byte) bool { + assert(proof.LeafHash, leafHash(leaf) + + computedHash := computeHashFromAunts(proof.Index, proof.Total, proof.LeafHash, proof.Aunts) + return computedHash == rootHash +} + +func computeHashFromAunts(index, total int, leafHash []byte, innerHashes [][]byte) []byte{ + assert(index < total && index >= 0 && total > 0) + + if total == 1{ + assert(len(proof.Aunts) == 0) + return leafHash + } + + assert(len(innerHashes) > 0) + + numLeft := getSplitPoint(total) // largest power of 2 less than total + if index < numLeft { + leftHash := computeHashFromAunts(index, numLeft, leafHash, innerHashes[:len(innerHashes)-1]) + assert(leftHash != nil) + return innerHash(leftHash, innerHashes[len(innerHashes)-1]) + } + rightHash := computeHashFromAunts(index-numLeft, total-numLeft, leafHash, innerHashes[:len(innerHashes)-1]) + assert(rightHash != nil) + return innerHash(innerHashes[len(innerHashes)-1], rightHash) +} +``` + +The number of aunts is limited to 100 (`MaxAunts`) to protect the node against DOS attacks. +This limits the tree size to 2^100 leaves, which should be sufficient for any +conceivable purpose. + +### IAVL+ Tree + +Because Tendermint only uses a Simple Merkle Tree, application developers are expect to use their own Merkle tree in their applications. For example, the IAVL+ Tree - an immutable self-balancing binary tree for persisting application state is used by the [Cosmos SDK](https://github.com/cosmos/cosmos-sdk/blob/ae77f0080a724b159233bd9b289b2e91c0de21b5/docs/interfaces/lite/specification.md) + +## JSON + +Tendermint has its own JSON encoding in order to keep backwards compatibility with the previous RPC layer. + +Registered types are encoded as: + +```json +{ + "type": "", + "value": +} +``` + +For instance, an ED25519 PubKey would look like: + +```json +{ + "type": "tendermint/PubKeyEd25519", + "value": "uZ4h63OFWuQ36ZZ4Bd6NF+/w9fWUwrOncrQsackrsTk=" +} +``` + +Where the `"value"` is the base64 encoding of the raw pubkey bytes, and the +`"type"` is the type name for Ed25519 pubkeys. + +### Signed Messages + +Signed messages (eg. votes, proposals) in the consensus are encoded using protobuf. + +When signing, the elements of a message are re-ordered so the fixed-length fields +are first, making it easy to quickly check the type, height, and round. +The `ChainID` is also appended to the end. +We call this encoding the SignBytes. For instance, SignBytes for a vote is the protobuf encoding of the following struct: + +```protobuf +message CanonicalVote { + SignedMsgType type = 1; + sfixed64 height = 2; // canonicalization requires fixed size encoding here + sfixed64 round = 3; // canonicalization requires fixed size encoding here + CanonicalBlockID block_id = 4; + google.protobuf.Timestamp timestamp = 5; + string chain_id = 6; +} +``` + +The field ordering and the fixed sized encoding for the first three fields is optimized to ease parsing of SignBytes +in HSMs. It creates fixed offsets for relevant fields that need to be read in this context. + +> Note: All canonical messages are length prefixed. + +For more details, see the [signing spec](../consensus/signing.md). +Also, see the motivating discussion in +[#1622](https://github.com/tendermint/tendermint/issues/1622). diff --git a/spec/core/genesis.md b/spec/core/genesis.md new file mode 100644 index 000000000..1dc019e77 --- /dev/null +++ b/spec/core/genesis.md @@ -0,0 +1,34 @@ +# Genesis + +The genesis file is the starting point of a chain. An application will populate the `app_state` field in the genesis with their required fields. Tendermint is not able to validate this section because it is unaware what application state consists of. + +## Genesis Fields + +- `genesis_time`: The genesis time is the time the blockchain started or will start. If nodes are started before this time they will sit idle until the time specified. +- `chain_id`: The chainid is the chain identifier. Every chain should have a unique identifier. When conducting a fork based upgrade, we recommend changing the chainid to avoid network or consensus errors. +- `initial_height`: This field is the starting height of the blockchain. When conducting a chain restart to avoid restarting at height 1, the network is able to start at a specified height. +- `consensus_params` + - `block` + - `max_bytes`: The max amount of bytes a block can be. + - `max_gas`: The maximum amount of gas that a block can have. + - `time_iota_ms`: This parameter has no value anymore in Tendermint-core. + +- `evidence` + - `max_age_num_blocks`: After this preset amount of blocks has passed a single piece of evidence is considered invalid + - `max_age_duration`: After this preset amount of time has passed a single piece of evidence is considered invalid. + - `max_bytes`: The max amount of bytes of all evidence included in a block. + +> Note: For evidence to be considered invalid, evidence must be older than both `max_age_num_blocks` and `max_age_duration` + +- `validator` + - `pub_key_types`: Defines which curves are to be accepted as a valid validator consensus key. Tendermint supports ed25519, sr25519 and secp256k1. + +- `version` + - `app_version`: The version of the application. This is set by the application and is used to identify which version of the app a user should be using in order to operate a node. + +- `validators` + - This is an array of validators. This validator set is used as the starting validator set of the chain. This field can be empty, if the application sets the validator set in `InitChain`. + +- `app_hash`: The applications state root hash. This field does not need to be populated at the start of the chain, the application may provide the needed information via `Initchain`. + +- `app_state`: This section is filled in by the application and is unknown to Tendermint. diff --git a/spec/core/readme.md b/spec/core/readme.md new file mode 100644 index 000000000..46f95f1b7 --- /dev/null +++ b/spec/core/readme.md @@ -0,0 +1,13 @@ +--- +order: 1 +parent: + title: Core + order: 3 +--- + +This section describes the core types and functionality of the Tendermint protocol implementation. + +- [Core Data Structures](./data_structures.md) +- [Encoding](./encoding.md) +- [Genesis](./genesis.md) +- [State](./state.md) diff --git a/spec/core/state.md b/spec/core/state.md new file mode 100644 index 000000000..5138c0950 --- /dev/null +++ b/spec/core/state.md @@ -0,0 +1,121 @@ +# State + +The state contains information whose cryptographic digest is included in block headers, and thus is +necessary for validating new blocks. For instance, the validators set and the results of +transactions are never included in blocks, but their Merkle roots are: +the state keeps track of them. + +The `State` object itself is an implementation detail, since it is never +included in a block or gossiped over the network, and we never compute +its hash. The persistence or query interface of the `State` object +is an implementation detail and not included in the specification. +However, the types in the `State` object are part of the specification, since +the Merkle roots of the `State` objects are included in blocks and values are used during +validation. + +```go +type State struct { + ChainID string + InitialHeight int64 + + LastBlockHeight int64 + LastBlockID types.BlockID + LastBlockTime time.Time + + Version Version + LastResults []Result + AppHash []byte + + LastValidators ValidatorSet + Validators ValidatorSet + NextValidators ValidatorSet + + ConsensusParams ConsensusParams +} +``` + +The chain ID and initial height are taken from the genesis file, and not changed again. The +initial height will be `1` in the typical case, `0` is an invalid value. + +Note there is a hard-coded limit of 10000 validators. This is inherited from the +limit on the number of votes in a commit. + +Further information on [`Validator`'s](./data_structures.md#validator), +[`ValidatorSet`'s](./data_structures.md#validatorset) and +[`ConsensusParams`'s](./data_structures.md#consensusparams) can +be found in [data structures](./data_structures.md) + +## Execution + +State gets updated at the end of executing a block. Of specific interest is `ResponseEndBlock` and +`ResponseCommit` + +```go +type ResponseEndBlock struct { + ValidatorUpdates []ValidatorUpdate `protobuf:"bytes,1,rep,name=validator_updates,json=validatorUpdates,proto3" json:"validator_updates"` + ConsensusParamUpdates *types1.ConsensusParams `protobuf:"bytes,2,opt,name=consensus_param_updates,json=consensusParamUpdates,proto3" json:"consensus_param_updates,omitempty"` + Events []Event `protobuf:"bytes,3,rep,name=events,proto3" json:"events,omitempty"` +} +``` + +where + +```go +type ValidatorUpdate struct { + PubKey crypto.PublicKey `protobuf:"bytes,1,opt,name=pub_key,json=pubKey,proto3" json:"pub_key"` + Power int64 `protobuf:"varint,2,opt,name=power,proto3" json:"power,omitempty"` +} +``` + +and + +```go +type ResponseCommit struct { + // reserve 1 + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + RetainHeight int64 `protobuf:"varint,3,opt,name=retain_height,json=retainHeight,proto3" json:"retain_height,omitempty"` +} +``` + +`ValidatorUpdates` are used to add and remove validators to the current set as well as update +validator power. Setting validator power to 0 in `ValidatorUpdate` will cause the validator to be +removed. `ConsensusParams` are safely copied across (i.e. if a field is nil it gets ignored) and the +`Data` from the `ResponseCommit` is used as the `AppHash` + +## Version + +```go +type Version struct { + consensus Consensus + software string +} +``` + +[`Consensus`](./data_structures.md#version) contains the protocol version for the blockchain and the +application. + +## Block + +The total size of a block is limited in bytes by the `ConsensusParams.Block.MaxBytes`. +Proposed blocks must be less than this size, and will be considered invalid +otherwise. + +Blocks should additionally be limited by the amount of "gas" consumed by the +transactions in the block, though this is not yet implemented. + +## Evidence + +For evidence in a block to be valid, it must satisfy: + +```go +block.Header.Time-evidence.Time < ConsensusParams.Evidence.MaxAgeDuration && + block.Header.Height-evidence.Height < ConsensusParams.Evidence.MaxAgeNumBlocks +``` + +A block must not contain more than `ConsensusParams.Evidence.MaxBytes` of evidence. This is +implemented to mitigate spam attacks. + +## Validator + +Validators from genesis file and `ResponseEndBlock` must have pubkeys of type ∈ +`ConsensusParams.Validator.PubKeyTypes`. diff --git a/spec/ivy-proofs/Dockerfile b/spec/ivy-proofs/Dockerfile new file mode 100644 index 000000000..be60151fd --- /dev/null +++ b/spec/ivy-proofs/Dockerfile @@ -0,0 +1,37 @@ +# we need python2 support, which was dropped after buster: +FROM debian:buster + +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections +RUN apt-get update +RUN apt-get install -y apt-utils + +# Install and configure locale `en_US.UTF-8` +RUN apt-get install -y locales && \ + sed -i -e "s/# $en_US.*/en_US.UTF-8 UTF-8/" /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales && \ + update-locale LANG=en_US.UTF-8 +ENV LANG=en_US.UTF-8 + +RUN apt-get update +RUN apt-get install -y git python2 python-pip g++ cmake python-ply python-tk tix pkg-config libssl-dev python-setuptools + +# create a user: +RUN useradd -ms /bin/bash user +USER user +WORKDIR /home/user + +RUN git clone --recurse-submodules https://github.com/kenmcmil/ivy.git +WORKDIR /home/user/ivy/ +RUN git checkout 271ee38980699115508eb90a0dd01deeb750a94b + +RUN python2.7 build_submodules.py +RUN mkdir -p "/home/user/python/lib/python2.7/site-packages" +ENV PYTHONPATH="/home/user/python/lib/python2.7/site-packages" +# need to install pyparsing manually because otherwise wrong version found +RUN pip install pyparsing +RUN python2.7 setup.py install --prefix="/home/user/python/" +ENV PATH=$PATH:"/home/user/python/bin/" +WORKDIR /home/user/tendermint-proof/ + +ENTRYPOINT ["/home/user/tendermint-proof/check_proofs.sh"] + diff --git a/spec/ivy-proofs/README.md b/spec/ivy-proofs/README.md new file mode 100644 index 000000000..00a4bed25 --- /dev/null +++ b/spec/ivy-proofs/README.md @@ -0,0 +1,33 @@ +# Ivy Proofs + +```copyright +Copyright (c) 2020 Galois, Inc. +SPDX-License-Identifier: Apache-2.0 +``` + +## Contents + +This folder contains: + +* `tendermint.ivy`, a specification of Tendermint algorithm as described in *The latest gossip on BFT consensus* by E. Buchman, J. Kwon, Z. Milosevic. +* `abstract_tendermint.ivy`, a more abstract specification of Tendermint that is more verification-friendly. +* `classic_safety.ivy`, a proof that Tendermint satisfies the classic safety property of BFT consensus: if every two quorums have a well-behaved node in common, then no two well-behaved nodes ever disagree. +* `accountable_safety_1.ivy`, a proof that, assuming every quorum contains at least one well-behaved node, if two well-behaved nodes disagree, then there is evidence demonstrating at least f+1 nodes misbehaved. +* `accountable_safety_2.ivy`, a proof that, regardless of any assumption about quorums, well-behaved nodes cannot be framed by malicious nodes. In other words, malicious nodes can never construct evidence that incriminates a well-behaved node. +* `network_shim.ivy`, the network model and a convenience `shim` object to interface with the Tendermint specification. +* `domain_model.ivy`, a specification of the domain model underlying the Tendermint specification, i.e. rounds, value, quorums, etc. + +All specifications and proofs are written in [Ivy](https://github.com/kenmcmil/ivy). + +The license above applies to all files in this folder. + + +## Building and running + +The easiest way to check the proofs is to use [Docker](https://www.docker.com/). + +1. Install [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/). +2. Build a Docker image: `docker-compose build` +3. Run the proofs inside the Docker container: `docker-compose run +tendermint-proof`. This will check all the proofs with the `ivy_check` +command and write the output of `ivy_check` to a subdirectory of `./output/' diff --git a/spec/ivy-proofs/abstract_tendermint.ivy b/spec/ivy-proofs/abstract_tendermint.ivy new file mode 100644 index 000000000..4a160be2a --- /dev/null +++ b/spec/ivy-proofs/abstract_tendermint.ivy @@ -0,0 +1,178 @@ +#lang ivy1.7 +# --- +# layout: page +# title: Abstract specification of Tendermint in Ivy +# --- + +# Here we define an abstract version of the Tendermint specification. We use +# two main forms of abstraction: a) We abstract over how information is +# transmitted (there is no network). b) We abstract functions using relations. +# For example, we abstract over a node's current round, instead only tracking +# with a relation which rounds the node has left. We do something similar for +# the `lockedRound` variable. This is in order to avoid using a function from +# node to round, and it allows us to emit verification conditions that are +# efficiently solvable by Z3. + +# This specification also defines the observations that are used to adjudicate +# misbehavior. Well-behaved nodes faithfully observe every message that they +# use to take a step, while Byzantine nodes can fake observations about +# themselves (including withholding observations). Misbehavior is defined using +# the collection of all observations made (in reality, those observations must +# be collected first, but we do not model this process). + +include domain_model + +module abstract_tendermint = { + +# Protocol state +# ############## + + relation left_round(N:node, R:round) + relation prevoted(N:node, R:round, V:value) + relation precommitted(N:node, R:round, V:value) + relation decided(N:node, R:round, V:value) + relation locked(N:node, R:round, V:value) + +# Accountability relations +# ######################## + + relation observed_prevoted(N:node, R:round, V:value) + relation observed_precommitted(N:node, R:round, V:value) + +# relations that are defined in terms of the previous two: + relation observed_equivocation(N:node) + relation observed_unlawful_prevote(N:node) + relation agreement + relation accountability_violation + + object defs = { # we hide those definitions and use them only when needed + private { + definition [observed_equivocation_def] observed_equivocation(N) = exists V1,V2,R . + V1 ~= V2 & (observed_precommitted(N,R,V1) & observed_precommitted(N,R,V2) | observed_prevoted(N,R,V1) & observed_prevoted(N,R,V2)) + + definition [observed_unlawful_prevote_def] observed_unlawful_prevote(N) = exists V1,V2,R1,R2 . + V1 ~= value.nil & V2 ~= value.nil & V1 ~= V2 & R1 < R2 & observed_precommitted(N,R1,V1) & observed_prevoted(N,R2,V2) + & forall Q,R . R1 <= R & R < R2 & nset.is_quorum(Q) -> exists N2 . nset.member(N2,Q) & ~observed_prevoted(N2,R,V2) + + definition [agreement_def] agreement = forall N1,N2,R1,R2,V1,V2 . well_behaved(N1) & well_behaved(N2) & decided(N1,R1,V1) & decided(N2,R2,V2) -> V1 = V2 + + definition [accountability_violation_def] accountability_violation = exists Q1,Q2 . nset.is_quorum(Q1) & nset.is_quorum(Q2) & (forall N . nset.member(N,Q1) & nset.member(N,Q2) -> observed_equivocation(N) | observed_unlawful_prevote(N)) + } + } + +# Protocol transitions +# #################### + + after init { + left_round(N,R) := R < 0; + prevoted(N,R,V) := false; + precommitted(N,R,V) := false; + decided(N,R,V) := false; + locked(N,R,V) := false; + + observed_prevoted(N,R,V) := false; + observed_precommitted(N,R,V) := false; + } + +# Actions are named after the corresponding line numbers in the Tendermint +# arXiv paper. + + action l_11(n:node, r:round) = { # start round r + require ~left_round(n,r); + left_round(n,R) := R < r; + } + + action l_22(n:node, rp:round, v:value) = { + require ~left_round(n,rp); + require ~prevoted(n,rp,V) & ~precommitted(n,rp,V); + require (forall R,V . locked(n,R,V) -> V = v) | v = value.nil; + prevoted(n, rp, v) := true; + left_round(n, R) := R < rp; # leave all lower rounds. + + observed_prevoted(n, rp, v) := observed_prevoted(n, rp, v) | well_behaved(n); # the node observes itself + } + + action l_28(n:node, rp:round, v:value, vr:round, q:nset) = { + require ~left_round(n,rp) & ~prevoted(n,rp,V); + require ~prevoted(n,rp,V) & ~precommitted(n,rp,V); + require vr < rp; + require nset.is_quorum(q) & (forall N . nset.member(N,q) -> (prevoted(N,vr,v) | ~well_behaved(N))); + var proposal:value; + if value.valid(v) & ((forall R0,V0 . locked(n,R0,V0) -> R0 <= vr) | (forall R,V . locked(n,R,V) -> V = v)) { + proposal := v; + } + else { + proposal := value.nil; + }; + prevoted(n, rp, proposal) := true; + left_round(n, R) := R < rp; # leave all lower rounds + + observed_prevoted(N, vr, v) := observed_prevoted(N, vr, v) | (well_behaved(n) & nset.member(N,q)); # the node observes the prevotes of quorum q + observed_prevoted(n, rp, proposal) := observed_prevoted(n, rp, proposal) | well_behaved(n); # the node observes itself + } + + action l_36(n:node, rp:round, v:value, q:nset) = { + require v ~= value.nil; + require ~left_round(n,rp); + require exists V . prevoted(n,rp,V); + require ~precommitted(n,rp,V); + require nset.is_quorum(q) & (forall N . nset.member(N,q) -> (prevoted(N,rp,v) | ~well_behaved(N))); + precommitted(n, rp, v) := true; + left_round(n, R) := R < rp; # leave all lower rounds + locked(n,R,V) := R <= rp & V = v; + + observed_prevoted(N, rp, v) := observed_prevoted(N, rp, v) | (well_behaved(n) & nset.member(N,q)); # the node observes the prevotes of quorum q + observed_precommitted(n, rp, v) := observed_precommitted(n, rp, v) | well_behaved(n); # the node observes itself + } + + action l_44(n:node, rp:round, q:nset) = { + require ~left_round(n,rp); + require ~precommitted(n,rp,V); + require nset.is_quorum(q) & (forall N .nset.member(N,q) -> (prevoted(N,rp,value.nil) | ~well_behaved(N))); + precommitted(n, rp, value.nil) := true; + left_round(n, R) := R < rp; # leave all lower rounds + + observed_prevoted(N, rp, value.nil) := observed_prevoted(N, rp, value.nil) | (well_behaved(n) & nset.member(N,q)); # the node observes the prevotes of quorum q + observed_precommitted(n, rp, value.nil) := observed_precommitted(n, rp, value.nil) | well_behaved(n); # the node observes itself + } + + action l_57(n:node, rp:round) = { + require ~left_round(n,rp); + require ~prevoted(n,rp,V); + prevoted(n, rp, value.nil) := true; + left_round(n, R) := R < rp; # leave all lower rounds + + observed_prevoted(n, rp, value.nil) := observed_prevoted(n, rp, value.nil) | well_behaved(n); # the node observes itself + } + + action l_61(n:node, rp:round) = { + require ~left_round(n,rp); + require ~precommitted(n,rp,V); + precommitted(n, rp, value.nil) := true; + left_round(n, R) := R < rp; # leave all lower rounds + + observed_precommitted(n, rp, value.nil) := observed_precommitted(n, rp, value.nil) | well_behaved(n); # the node observes itself + } + + action decide(n:node, r:round, v:value, q:nset) = { + require v ~= value.nil; + require nset.is_quorum(q) & (forall N . nset.member(N, q) -> (precommitted(N, r, v) | ~well_behaved(N))); + decided(n, r, v) := true; + + observed_precommitted(N, r, v) := observed_precommitted(N, r, v) | (well_behaved(n) & nset.member(N,q)); # the node observes the precommits of quorum q + + } + + action misbehave = { +# Byzantine nodes can claim they observed whatever they want about themselves, +# but they cannot remove observations. Note that we use assume because we don't +# want those to be checked; we just want them to be true (that's the model of +# Byzantine behavior). + observed_prevoted(N,R,V) := *; + assume (old observed_prevoted(N,R,V)) -> observed_prevoted(N,R,V); + assume well_behaved(N) -> old observed_prevoted(N,R,V) = observed_prevoted(N,R,V); + observed_precommitted(N,R,V) := *; + assume (old observed_precommitted(N,R,V)) -> observed_precommitted(N,R,V); + assume well_behaved(N) -> old observed_precommitted(N,R,V) = observed_precommitted(N,R,V); + } +} diff --git a/spec/ivy-proofs/accountable_safety_1.ivy b/spec/ivy-proofs/accountable_safety_1.ivy new file mode 100644 index 000000000..02bdf1add --- /dev/null +++ b/spec/ivy-proofs/accountable_safety_1.ivy @@ -0,0 +1,143 @@ +#lang ivy1.7 +# --- +# layout: page +# title: Proof of Classic Safety +# --- + +include tendermint +include abstract_tendermint + +# Here we prove the first accountability property: if two well-behaved nodes +# disagree, then there are two quorums Q1 and Q2 such that all members of the +# intersection of Q1 and Q2 have violated the accountability properties. + +# The proof is done in two steps: first we prove the abstract specification +# satisfies the property, and then we show by refinement that this property +# also holds in the concrete specification. + +# To see what is checked in the refinement proof, use `ivy_show isolate=accountable_safety_1 accountable_safety_1.ivy` +# To see what is checked in the abstract correctness proof, use `ivy_show isolate=abstract_accountable_safety_1 accountable_safety_1.ivy` +# To check the whole proof, use `ivy_check accountable_safety_1.ivy`. + + +# Proof of the accountability property in the abstract specification +# ================================================================== + +# We prove with tactics (see `lemma_1` and `lemma_2`) that, if some basic +# invariants hold (see `invs` below), then the accountability property holds. + +isolate abstract_accountable_safety = { + + instantiate abstract_tendermint + +# The main property +# ----------------- + +# If there is disagreement, then there is evidence that a third of the nodes +# have violated the protocol: + invariant [accountability] agreement | accountability_violation + proof { + apply lemma_1.thm # this reduces to goal to three subgoals: p1, p2, and p3 (see their definition below) + proof [p1] { + assume invs.inv1 + } + proof [p2] { + assume invs.inv2 + } + proof [p3] { + assume invs.inv3 + } + } + +# The invariants +# -------------- + + isolate invs = { + + # well-behaved nodes observe their own actions faithfully: + invariant [inv1] well_behaved(N) -> (observed_precommitted(N,R,V) = precommitted(N,R,V)) + # if a value is precommitted by a well-behaved node, then a quorum is observed to prevote it: + invariant [inv2] (exists N . well_behaved(N) & precommitted(N,R,V)) & V ~= value.nil -> exists Q . nset.is_quorum(Q) & forall N2 . nset.member(N2,Q) -> observed_prevoted(N2,R,V) + # if a value is decided by a well-behaved node, then a quorum is observed to precommit it: + invariant [inv3] (exists N . well_behaved(N) & decided(N,R,V)) -> 0 <= R & V ~= value.nil & exists Q . nset.is_quorum(Q) & forall N2 . nset.member(N2,Q) -> observed_precommitted(N2,R,V) + private { + invariant (precommitted(N,R,V) | prevoted(N,R,V)) -> 0 <= R + invariant R < 0 -> left_round(N,R) + } + + } with this, nset, round, accountable_bft.max_2f_byzantine + +# The theorems proved with tactics +# -------------------------------- + +# Using complete induction on rounds, we prove that, assuming that the +# invariants inv1, inv2, and inv3 hold, the accountability property holds. + +# For technical reasons, we separate the proof in two steps + isolate lemma_1 = { + + specification { + theorem [thm] { + property [p1] forall N,R,V . well_behaved(N) -> (observed_precommitted(N,R,V) = precommitted(N,R,V)) + property [p2] forall R,V . (exists N . well_behaved(N) & precommitted(N,R,V)) & V ~= value.nil -> exists Q . nset.is_quorum(Q) & forall N2 . nset.member(N2,Q) -> observed_prevoted(N2,R,V) + property [p3] forall R,V. (exists N . well_behaved(N) & decided(N,R,V)) -> 0 <= R & V ~= value.nil & exists Q . nset.is_quorum(Q) & forall N2 . nset.member(N2,Q) -> observed_precommitted(N2,R,V) + #------------------------------------------------------------------------------------------------------------------------------------------- + property agreement | accountability_violation + } + proof { + assume inductive_property # the theorem follows from what we prove by induction below + } + } + + implementation { + # complete induction is not built-in, so we introduce it with an axiom. Note that this only holds for a type where 0 is the smallest element + axiom [complete_induction] { + relation p(X:round) + { # base case + property p(0) + } + { # inductive step: show that if the property is true for all X lower or equal to x and y=x+1, then the property is true of y + individual a:round + individual b:round + property (forall X. 0 <= X & X <= a -> p(X)) & round.succ(a,b) -> p(b) + } + #-------------------------- + property forall X . 0 <= X -> p(X) + } + + # The main lemma: if inv1 and inv2 below hold and a quorum is observed to + # precommit V1 at R1 and another quorum is observed to precommit V2~=V1 at + # R2>=R1, then the intersection of two quorums (i.e. f+1 nodes) is observed to + # violate the protocol. We prove this by complete induction on R2. + theorem [inductive_property] { + property [p1] forall N,R,V . well_behaved(N) -> (observed_precommitted(N,R,V) = precommitted(N,R,V)) + property [p2] forall R,V . (exists N . well_behaved(N) & precommitted(N,R,V)) -> V = value.nil | exists Q . nset.is_quorum(Q) & forall N2 . nset.member(N2,Q) -> observed_prevoted(N2,R,V) + #----------------------------------------------------------------------------------------------------------------------- + property forall R2. 0 <= R2 -> ((exists V2,Q1,R1,V1,Q1 . V1 ~= value.nil & V2 ~= value.nil & V1 ~= V2 & 0 <= R1 & R1 <= R2 & nset.is_quorum(Q1) & (forall N . nset.member(N,Q1) -> observed_precommitted(N,R1,V1)) & (exists Q2 . nset.is_quorum(Q2) & forall N . nset.member(N,Q2) -> observed_prevoted(N,R2,V2))) -> accountability_violation) + } + proof { + apply complete_induction # the two subgoals (base case and inductive case) are then discharged automatically + # NOTE: this can take a long time depending on the SMT random seed (to try a different seed, use `ivy_check seed=$RANDOM` + } + } + } with this, round, nset, accountable_bft.max_2f_byzantine, defs.observed_equivocation_def, defs.observed_unlawful_prevote_def, defs.accountability_violation_def, defs.agreement_def + +} with round + +# The final proof +# =============== + +isolate accountable_safety_1 = { + +# First we instantiate the concrete protocol: + instantiate tendermint(abstract_accountable_safety) + +# We then define what we mean by agreement + relation agreement + definition [agreement_def] agreement = forall N1,N2. well_behaved(N1) & well_behaved(N2) & server.decision(N1) ~= value.nil & server.decision(N2) ~= value.nil -> server.decision(N1) = server.decision(N2) + + invariant abstract_accountable_safety.agreement -> agreement + + invariant [accountability] agreement | abstract_accountable_safety.accountability_violation + +} with value, round, proposers, shim, abstract_accountable_safety, abstract_accountable_safety.defs.agreement_def, accountable_safety_1.agreement_def diff --git a/spec/ivy-proofs/accountable_safety_2.ivy b/spec/ivy-proofs/accountable_safety_2.ivy new file mode 100644 index 000000000..7fb928909 --- /dev/null +++ b/spec/ivy-proofs/accountable_safety_2.ivy @@ -0,0 +1,52 @@ +#lang ivy1.7 + +include tendermint +include abstract_tendermint + +# Here we prove the second accountability property: no well-behaved node is +# ever observed to violate the accountability properties. + +# The proof is done in two steps: first we prove the the abstract specification +# satisfies the property, and then we show by refinement that this property +# also holds in the concrete specification. + +# To see what is checked in the refinement proof, use `ivy_show isolate=accountable_safety_2 accountable_safety_2.ivy` +# To see what is checked in the abstract correctness proof, use `ivy_show isolate=abstract_accountable_safety_2 accountable_safety_2.ivy` +# To check the whole proof, use `ivy_check complete=fo accountable_safety_2.ivy`. + +# Proof that the property holds in the abstract specification +# ============================================================ + +isolate abstract_accountable_safety_2 = { + + instantiate abstract_tendermint + +# the main property: + invariant [wb_never_punished] well_behaved(N) -> ~(observed_equivocation(N) | observed_unlawful_prevote(N)) + +# the main invariant for proving wb_not_punished: + invariant well_behaved(N) & precommitted(N,R,V) & ~locked(N,R,V) & V ~= value.nil -> exists R2,V2 . V2 ~= value.nil & R < R2 & precommitted(N,R2,V2) & locked(N,R2,V2) + + invariant (exists N . well_behaved(N) & precommitted(N,R,V) & V ~= value.nil) -> exists Q . nset.is_quorum(Q) & forall N . nset.member(N,Q) -> observed_prevoted(N,R,V) + + invariant well_behaved(N) -> (observed_prevoted(N,R,V) <-> prevoted(N,R,V)) + invariant well_behaved(N) -> (observed_precommitted(N,R,V) <-> precommitted(N,R,V)) + +# nodes stop prevoting or precommitting in lower rounds when doing so in a higher round: + invariant well_behaved(N) & prevoted(N,R2,V2) & R1 < R2 -> left_round(N,R1) + invariant well_behaved(N) & locked(N,R2,V2) & R1 < R2 -> left_round(N,R1) + + invariant [precommit_unique_per_round] well_behaved(N) & precommitted(N,R,V1) & precommitted(N,R,V2) -> V1 = V2 + +} with nset, round, abstract_accountable_safety_2.defs.observed_equivocation_def, abstract_accountable_safety_2.defs.observed_unlawful_prevote_def + +# Proof that the property holds in the concrete specification +# =========================================================== + +isolate accountable_safety_2 = { + + instantiate tendermint(abstract_accountable_safety_2) + + invariant well_behaved(N) -> ~(abstract_accountable_safety_2.observed_equivocation(N) | abstract_accountable_safety_2.observed_unlawful_prevote(N)) + +} with round, value, shim, abstract_accountable_safety_2, abstract_accountable_safety_2.defs.observed_equivocation_def, abstract_accountable_safety_2.defs.observed_unlawful_prevote_def diff --git a/spec/ivy-proofs/check_proofs.sh b/spec/ivy-proofs/check_proofs.sh new file mode 100755 index 000000000..6afd1a962 --- /dev/null +++ b/spec/ivy-proofs/check_proofs.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# returns non-zero error code if any proof fails + +success=0 +log_dir=$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 6) +cmd="ivy_check seed=$RANDOM" +mkdir -p output/$log_dir + +echo "Checking classic safety:" +res=$($cmd classic_safety.ivy | tee "output/$log_dir/classic_safety.txt" | tail -n 1) +if [ "$res" = "OK" ]; then + echo "OK" +else + echo "FAILED" + success=1 +fi + +echo "Checking accountable safety 1:" +res=$($cmd accountable_safety_1.ivy | tee "output/$log_dir/accountable_safety_1.txt" | tail -n 1) +if [ "$res" = "OK" ]; then + echo "OK" +else + echo "FAILED" + success=1 +fi + +echo "Checking accountable safety 2:" +res=$($cmd complete=fo accountable_safety_2.ivy | tee "output/$log_dir/accountable_safety_2.txt" | tail -n 1) +if [ "$res" = "OK" ]; then + echo "OK" +else + echo "FAILED" + success=1 +fi + +echo +echo "See ivy_check output in the output/ folder" +exit $success diff --git a/spec/ivy-proofs/classic_safety.ivy b/spec/ivy-proofs/classic_safety.ivy new file mode 100644 index 000000000..b422a2c17 --- /dev/null +++ b/spec/ivy-proofs/classic_safety.ivy @@ -0,0 +1,85 @@ +#lang ivy1.7 +# --- +# layout: page +# title: Proof of Classic Safety +# --- + +include tendermint +include abstract_tendermint + +# Here we prove the classic safety property: assuming that every two quorums +# have a well-behaved node in common, no two well-behaved nodes ever disagree. + +# The proof is done in two steps: first we prove the the abstract specification +# satisfies the property, and then we show by refinement that this property +# also holds in the concrete specification. + +# To see what is checked in the refinement proof, use `ivy_show isolate=classic_safety classic_safety.ivy` +# To see what is checked in the abstract correctness proof, use `ivy_show isolate=abstract_classic_safety classic_safety.ivy` + +# To check the whole proof, use `ivy_check classic_safety.ivy`. + +# Note that all the verification conditions sent to Z3 for this proof are in +# EPR. + +# Classic safety in the abstract model +# ==================================== + +# We start by proving that classic safety holds in the abstract model. + +isolate abstract_classic_safety = { + + instantiate abstract_tendermint + + invariant [classic_safety] classic_bft.quorum_intersection & decided(N1,R1,V1) & decided(N2,R2,V2) -> V1 = V2 + +# The notion of choosable value +# ----------------------------- + + relation choosable(R:round, V:value) + definition choosable(R,V) = exists Q . nset.is_quorum(Q) & forall N . well_behaved(N) & nset.member(N,Q) -> ~left_round(N,R) | precommitted(N,R,V) + +# Main invariants +# --------------- + +# `classic_safety` is inductive relative to those invariants + + invariant [decision_is_quorum_precommit] (exists N1 . decided(N1,R,V)) -> exists Q. nset.is_quorum(Q) & forall N2. well_behaved(N2) & nset.member(N2, Q) -> precommitted(N2,R,V) + + invariant [precommitted_is_quorum_prevote] V ~= value.nil & (exists N1 . precommitted(N1,R,V)) -> exists Q. nset.is_quorum(Q) & forall N2. well_behaved(N2) & nset.member(N2, Q) -> prevoted(N2,R,V) + + invariant [prevote_unique_per_round] prevoted(N,R,V1) & prevoted(N,R,V2) -> V1 = V2 + +# This is the core invariant: as long as a precommitted value is still choosable, it remains protected by a lock and prevents any new value from being prevoted: + invariant [locks] classic_bft.quorum_intersection & V ~= value.nil & precommitted(N,R,V) & choosable(R,V) -> locked(N,R,V) & forall R2,V2 . R < R2 & prevoted(N,R2,V2) -> V2 = V | V2 = value.nil + +# Supporting invariants +# --------------------- + +# The main invariants are inductive relative to those + + invariant decided(N,R,V) -> V ~= value.nil + + invariant left_round(N,R2) & R1 < R2 -> left_round(N,R1) # if a node left round R2>R1, then it also left R1: + + invariant prevoted(N,R2,V2) & R1 < R2 -> left_round(N,R1) + invariant precommitted(N,R2,V2) & R1 < R2 -> left_round(N,R1) + +} with round, nset, classic_bft.quorum_intersection_def + +# The refinement proof +# ==================== + +# Now, thanks to the refinement relation that we establish in +# `concrete_tendermint.ivy`, we prove that classic safety transfers to the +# concrete specification: +isolate classic_safety = { + + # We instantiate the `tendermint` module providing `abstract_classic_safety` as abstract model. + instantiate tendermint(abstract_classic_safety) + + # We prove that if every two quorums have a well-behaved node in common, + # then well-behaved nodes never disagree: + invariant [classic_safety] classic_bft.quorum_intersection & server.decision(N1) ~= value.nil & server.decision(N2) ~= value.nil -> server.decision(N1) = server.decision(N2) + +} with value, round, proposers, shim, abstract_classic_safety # here we list all the specifications that we rely on for this proof diff --git a/spec/ivy-proofs/count_lines.sh b/spec/ivy-proofs/count_lines.sh new file mode 100755 index 000000000..b2c457e21 --- /dev/null +++ b/spec/ivy-proofs/count_lines.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +r='^\s*$\|^\s*\#\|^\s*\}\s*$\|^\s*{\s*$' # removes comments and blank lines and lines that contain only { or } +N1=`cat tendermint.ivy domain_model.ivy network_shim.ivy | grep -v $r'\|.*invariant.*' | wc -l` +N2=`cat abstract_tendermint.ivy | grep "observed_" | wc -l` # the observed_* variables specify the observations of the nodes +SPEC_LINES=`expr $N1 + $N2` +echo "spec lines: $SPEC_LINES" +N3=`cat abstract_tendermint.ivy | grep -v $r'\|.*observed_.*' | wc -l` +N4=`cat accountable_safety_1.ivy | grep -v $r | wc -l` +PROOF_LINES=`expr $N3 + $N4` +echo "proof lines: $PROOF_LINES" +RATIO=`bc <<< "scale=2;$PROOF_LINES / $SPEC_LINES"` +echo "proof-to-code ratio for the accountable-safety property: $RATIO" diff --git a/spec/ivy-proofs/docker-compose.yml b/spec/ivy-proofs/docker-compose.yml new file mode 100644 index 000000000..1d4a8ffe1 --- /dev/null +++ b/spec/ivy-proofs/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + tendermint-proof: + build: . + volumes: + - ./:/home/user/tendermint-proof:ro + - ./output:/home/user/tendermint-proof/output:rw + diff --git a/spec/ivy-proofs/domain_model.ivy b/spec/ivy-proofs/domain_model.ivy new file mode 100644 index 000000000..0f12f7288 --- /dev/null +++ b/spec/ivy-proofs/domain_model.ivy @@ -0,0 +1,143 @@ +#lang ivy1.7 + +include order # this is a file from the standard library (`ivy/ivy/include/1.7/order.ivy`) + +isolate round = { + type this + individual minus_one:this + relation succ(R1:round, R2:round) + action incr(i:this) returns (j:this) + specification { +# to simplify verification, we treat rounds as an abstract totally ordered set with a successor relation. + instantiate totally_ordered(this) + property minus_one < 0 + property succ(X,Z) -> (X < Z & ~(X < Y & Y < Z)) + after incr { + ensure succ(i,j) + } + } + implementation { +# here we prove that the abstraction is sound. + interpret this -> int # rounds are integers in the Tendermint specification. + definition minus_one = 0-1 + definition succ(R1,R2) = R2 = R1 + 1 + implement incr { + j := i+1; + } + } +} + +instance node : iterable # nodes are a set with an order, that can be iterated over (see order.ivy in the standard library) + +relation well_behaved(N:node) # whether a node is well-behaved or not. NOTE: Used only in the proof and the Byzantine model; Nodes do know know who is well-behaved and who is not. + +isolate proposers = { + # each round has a unique proposer in Tendermint. In order to avoid a + # function from round to node (which makes verification more difficult), we + # abstract over this function using a relation. + relation is_proposer(N:node, R:round) + export action get_proposer(r:round) returns (n:node) + specification { + property is_proposer(N1,R) & is_proposer(N2,R) -> N1 = N2 + after get_proposer { + ensure is_proposer(n,r); + } + } + implementation { + function f(R:round):node + definition f(r:round) = <<>> + definition is_proposer(N,R) = N = f(R) + implement get_proposer { + n := f(r); + } + } +} + +isolate value = { # the type of values + type this + relation valid(V:value) + individual nil:value + specification { + property ~valid(nil) + } + implementation { + interpret value -> bv[2] + definition nil = <<< -1 >>> # let's say nil is -1 + definition valid(V) = V ~= nil + } +} + +object nset = { # the type of node sets + type this # a set of N=3f+i nodes for 0 + #include + namespace hash_space { + template + class hash > { + public: + size_t operator()(const std::set &s) const { + hash h; + size_t res = 0; + for (const T &e : s) + res += h(e); + return res; + } + }; + } + >>> + interpret nset -> <<< std::set<`node`> >>> + definition member(n:node, s:nset) = <<< `s`.find(`n`) != `s`.end() >>> + definition is_quorum(s:nset) = <<< 3*`s`.size() > 2*`node.size` >>> + definition is_blocking(s:nset) = <<< 3*`s`.size() > `node.size` >>> + implement empty { + <<< + >>> + } + implement insert { + <<< + `t` = `s`; + `t`.insert(`n`); + >>> + } + <<< encode `nset` + + std::ostream &operator <<(std::ostream &s, const `nset` &a) { + s << "{"; + for (auto iter = a.begin(); iter != a.end(); iter++) { + if (iter != a.begin()) s << ", "; + s << *iter; + } + s << "}"; + return s; + } + + template <> + `nset` _arg<`nset`>(std::vector &args, unsigned idx, long long bound) { + throw std::invalid_argument("Not implemented"); // no syntax for nset values in the REPL + } + + >>> + } +} + +object classic_bft = { + relation quorum_intersection + private { + definition [quorum_intersection_def] quorum_intersection = forall Q1,Q2. exists N. well_behaved(N) & nset.member(N, Q1) & nset.member(N, Q2) # every two quorums have a well-behaved node in common + } +} + +trusted isolate accountable_bft = { + # this is our baseline assumption about quorums: + private { + property [max_2f_byzantine] exists N . well_behaved(N) & nset.member(N,Q) # every quorum has a well-behaved member + } +} diff --git a/spec/ivy-proofs/network_shim.ivy b/spec/ivy-proofs/network_shim.ivy new file mode 100644 index 000000000..ebc3a04fc --- /dev/null +++ b/spec/ivy-proofs/network_shim.ivy @@ -0,0 +1,133 @@ +#lang ivy1.7 +# --- +# layout: page +# title: Network model and network shim +# --- + +# Here we define a network module, which is our model of the network, and a +# shim module that sits on top of the network and which, upon receiving a +# message, calls the appropriate protocol handler. + +include domain_model + +# Here we define an enumeration type for identifying the 3 different types of +# messages that nodes send. +object msg_kind = { # TODO: merge with step_t + type this = {proposal, prevote, precommit} +} + +# Here we define the type of messages `msg`. Its members are structs with the fields described below. +object msg = { + type this = struct { + m_kind : msg_kind, + m_src : node, + m_round : round, + m_value : value, + m_vround : round + } +} + +# This is our model of the network: +isolate net = { + + export action recv(dst:node,v:msg) + action send(src:node,dst:node,v:msg) + # Note that the `recv` action is exported, meaning that it can be called + # non-deterministically by the environment any time it is enabled. In other + # words, a packet that is in flight can be received at any time. In this + # sense, the network is fully asynchronous. Moreover, there is no + # requirement that a given message will be received at all. + + # The state of the network consists of all the packets that have been + # sent so far, along with their destination. + relation sent(V:msg, N:node) + + after init { + sent(V, N) := false + } + + before send { + sent(v,dst) := true + } + + before recv { + require sent(v,dst) # only sent messages can be received. + } +} + +# The network shim sits on top of the network and, upon receiving a message, +# calls the appropriate protocol handler. It also exposes a `broadcast` action +# that sends to all nodes. + +isolate shim = { + + # In order not repeat the same code for each handler, we use a handler + # module parameterized by the type of message it will handle. Below we + # instantiate this module for the 3 types of messages of Tendermint + module handler(p_kind) = { + action handle(dst:node,m:msg) + object spec = { + before handle { + assert sent(m,dst) & m.m_kind = p_kind + } + } + } + + instance proposal_handler : handler(msg_kind.proposal) + instance prevote_handler : handler(msg_kind.prevote) + instance precommit_handler : handler(msg_kind.precommit) + + relation sent(M:msg,N:node) + + action broadcast(src:node,m:msg) + action send(src:node,dst:node,m:msg) + + specification { + after init { + sent(M,D) := false; + } + before broadcast { + sent(m,D) := true + } + before send { + sent(m,dst) := true + } + } + + # Here we give an implementation of it that satisfies its specification: + implementation { + + implement net.recv(dst:node,m:msg) { + + if m.m_kind = msg_kind.proposal { + call proposal_handler.handle(dst,m) + } + else if m.m_kind = msg_kind.prevote { + call prevote_handler.handle(dst,m) + } + else if m.m_kind = msg_kind.precommit { + call precommit_handler.handle(dst,m) + } + } + + implement broadcast { # broadcast sends to all nodes, including the sender. + var iter := node.iter.create(0); + while ~iter.is_end + invariant net.sent(M,D) -> sent(M,D) + { + var n := iter.val; + call net.send(src,n,m); + iter := iter.next; + } + } + + implement send { + call net.send(src,dst,m) + } + + private { + invariant net.sent(M,D) -> sent(M,D) + } + } + +} with net, node # to prove that the shim implementation satisfies the shim specification, we rely on the specification of net and node. diff --git a/spec/ivy-proofs/output/.gitignore b/spec/ivy-proofs/output/.gitignore new file mode 100644 index 000000000..5e7d2734c --- /dev/null +++ b/spec/ivy-proofs/output/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/spec/ivy-proofs/tendermint.ivy b/spec/ivy-proofs/tendermint.ivy new file mode 100644 index 000000000..b7678bef9 --- /dev/null +++ b/spec/ivy-proofs/tendermint.ivy @@ -0,0 +1,420 @@ +#lang ivy1.7 +# --- +# layout: page +# title: Specification of Tendermint in Ivy +# --- + +# This specification closely follows the pseudo-code given in "The latest +# gossip on BFT consensus" by E. Buchman, J. Kwon, Z. Milosevic +# + +include domain_model +include network_shim + +# We model the Tendermint protocol as an Ivy object. Like in Object-Oriented +# Programming, the basic structuring unit in Ivy is the object. Objects have +# internal state and actions (i.e. methods in OO parlance) that modify their +# state. We model Tendermint as an object whose actions represent steps taken +# by individual nodes in the protocol. Actions in Ivy can have preconditions, +# and a valid execution is a sequence of actions whose preconditions are all +# satisfied in the state in which they are called. + +# For technical reasons, we define below a `tendermint` module instead of an +# object. Ivy modules are a little bit like classes in OO programs, and like +# classes they can be instantiated to obtain objects. To instantiate the +# `tendermint` module, we must provide an abstract-protocol object. This allows +# us to use different abstract-protocol objects for different parts of the +# proof, and to do so without too much notational burden (we could have used +# Ivy monitors, but then we would need to prefix every variable name by the +# name of the object containing it, which clutters things a bit compared to the +# approach we took). + +# The abstract-protocol object is called by the resulting tendermint object so +# as to run the abstract protocol alongside the concrete protocol. This allows +# us to transfer properties proved of the abstract protocol to the concrete +# protocol, as follows. First, we prove that running the abstract protocol in +# this way results in a valid execution of the abstract protocol. This is done +# by checking that all preconditions of the abstract actions are satisfied at +# their call sites. Second, we establish a relation between abstract state and +# concrete state (in the form of invariants of the resulting, two-object +# transition system) that allow us to transfer properties proved in the +# abstract protocol to the concrete protocol (for example, we prove that any +# decision made in the Tendermint protocol is also made in the abstract +# protocol; if the abstract protocol satisfies the agreement property, this +# allows us to conclude that the Tendermint protocol also does). + +# The abstract protocol object that we will use is always the same, and only +# the abstract properties that we prove about it change in the different +# instantiations of the `tendermint` module. Thus we provide common invariants +# that a) allow to prove that the abstract preconditions are met, and b) +# provide a refinement relation (see end of the module) relating the state of +# Tendermint to the state of the abstract protocol. + +# In the model, Byzantine nodes can send whatever messages they want, except +# that they cannot forge sender identities. This reflects the fact that, in +# practice, nodes use public key cryptography to sign their messages. + +# Finally, note that the observations that serve to adjudicate misbehavior are +# defined only in the abstract protocol (they happen in the abstract actions). + +module tendermint(abstract_protocol) = { + + # the initial value of a node: + function init_val(N:node): value + + # the three type of steps + object step_t = { + type this = {propose, prevote, precommit} + } # refer to those e.g. as step_t.propose + + object server(n:node) = { + + # the current round of a node + individual round_p: round + + individual step: step_t + + individual decision: value + + individual lockedValue: value + individual lockedRound: round + + individual validValue: value + individual validRound: round + + + relation done_l34(R:round) + relation done_l36(R:round, V:value) + relation done_l47(R:round) + + # variables for scheduling request + relation propose_timer_scheduled(R:round) + relation prevote_timer_scheduled(R:round) + relation precommit_timer_scheduled(R:round) + + relation _recved_proposal(Sender:node, R:round, V:value, VR:round) + relation _recved_prevote(Sender:node, R:round, V:value) + relation _recved_precommit(Sender:node, R:round, V:value) + + relation _has_started + + after init { + round_p := 0; + step := step_t.propose; + decision := value.nil; + + lockedValue := value.nil; + lockedRound := round.minus_one; + + validValue := value.nil; + validRound := round.minus_one; + + done_l34(R) := false; + done_l36(R, V) := false; + done_l47(R) := false; + + propose_timer_scheduled(R) := false; + prevote_timer_scheduled(R) := false; + precommit_timer_scheduled(R) := false; + + _recved_proposal(Sender, R, V, VR) := false; + _recved_prevote(Sender, R, V) := false; + _recved_precommit(Sender, R, V) := false; + + _has_started := false; + } + + action getValue returns (v:value) = { + v := init_val(n) + } + + export action start = { + require ~_has_started; + _has_started := true; + # line 10 + call startRound(0); + } + + # line 11-21 + action startRound(r:round) = { + # line 12 + round_p := r; + + # line 13 + step := step_t.propose; + + var proposal : value; + + # line 14 + if (proposers.get_proposer(r) = n) { + if validValue ~= value.nil { # line 15 + proposal := validValue; # line 16 + } else { + proposal := getValue(); # line 18 + }; + call broadcast_proposal(r, proposal, validRound); # line 19 + } else { + propose_timer_scheduled(r) := true; # line 21 + }; + + call abstract_protocol.l_11(n, r); + } + + # This action, as not exported, can only be called at specific call sites. + action broadcast_proposal(r:round, v:value, vr:round) = { + var m: msg; + m.m_kind := msg_kind.proposal; + m.m_src := n; + m.m_round := r; + m.m_value := v; + m.m_vround := vr; + call shim.broadcast(n,m); + } + + implement shim.proposal_handler.handle(msg:msg) { + _recved_proposal(msg.m_src, msg.m_round, msg.m_value, msg.m_vround) := true; + } + + # line 22-27 + export action l_22(v:value) = { + require _has_started; + require _recved_proposal(proposers.get_proposer(round_p), round_p, v, round.minus_one); + require step = step_t.propose; + + if (value.valid(v) & (lockedRound = round.minus_one | lockedValue = v)) { + call broadcast_prevote(round_p, v); # line 24 + call abstract_protocol.l_22(n, round_p, v); + } else { + call broadcast_prevote(round_p, value.nil); # line 26 + call abstract_protocol.l_22(n, round_p, value.nil); + }; + + # line 27 + step := step_t.prevote; + } + + # line 28-33 + export action l_28(r:round, v:value, vr:round, q:nset) = { + require _has_started; + require r = round_p; + require _recved_proposal(proposers.get_proposer(r), r, v, vr); + require nset.is_quorum(q); + require nset.member(N,q) -> _recved_prevote(N,vr,v); + require step = step_t.propose; + require vr >= 0 & vr < r; + + # line 29 + if (value.valid(v) & (lockedRound <= vr | lockedValue = v)) { + call broadcast_prevote(r, v); + } else { + call broadcast_prevote(r, value.nil); + }; + + call abstract_protocol.l_28(n,r,v,vr,q); + step := step_t.prevote; + } + + action broadcast_prevote(r:round, v:value) = { + var m: msg; + m.m_kind := msg_kind.prevote; + m.m_src := n; + m.m_round := r; + m.m_value := v; + call shim.broadcast(n,m); + } + + implement shim.prevote_handler.handle(msg:msg) { + _recved_prevote(msg.m_src, msg.m_round, msg.m_value) := true; + } + + # line 34-35 + export action l_34(r:round, q:nset) = { + require _has_started; + require round_p = r; + require nset.is_quorum(q); + require exists V . nset.member(N,q) -> _recved_prevote(N,r,V); + require step = step_t.prevote; + require ~done_l34(r); + done_l34(r) := true; + + prevote_timer_scheduled(r) := true; + } + + + # line 36-43 + export action l_36(r:round, v:value, q:nset) = { + require _has_started; + require r = round_p; + require exists VR . round.minus_one <= VR & VR < r & _recved_proposal(proposers.get_proposer(r), r, v, VR); + require nset.is_quorum(q); + require nset.member(N,q) -> _recved_prevote(N,r,v); + require value.valid(v); + require step = step_t.prevote | step = step_t.precommit; + + require ~done_l36(r,v); + done_l36(r, v) := true; + + if step = step_t.prevote { + lockedValue := v; # line 38 + lockedRound := r; # line 39 + call broadcast_precommit(r, v); # line 40 + step := step_t.precommit; # line 41 + call abstract_protocol.l_36(n, r, v, q); + }; + + validValue := v; # line 42 + validRound := r; # line 43 + } + + # line 44-46 + export action l_44(r:round, q:nset) = { + require _has_started; + require r = round_p; + require nset.is_quorum(q); + require nset.member(N,q) -> _recved_prevote(N,r,value.nil); + require step = step_t.prevote; + + call broadcast_precommit(r, value.nil); # line 45 + step := step_t.precommit; # line 46 + + call abstract_protocol.l_44(n, r, q); + } + + action broadcast_precommit(r:round, v:value) = { + var m: msg; + m.m_kind := msg_kind.precommit; + m.m_src := n; + m.m_round := r; + m.m_value := v; + call shim.broadcast(n,m); + } + + implement shim.precommit_handler.handle(msg:msg) { + _recved_precommit(msg.m_src, msg.m_round, msg.m_value) := true; + } + + + # line 47-48 + export action l_47(r:round, q:nset) = { + require _has_started; + require round_p = r; + require nset.is_quorum(q); + require nset.member(N,q) -> exists V . _recved_precommit(N,r,V); + require ~done_l47(r); + done_l47(r) := true; + + precommit_timer_scheduled(r) := true; + } + + + # line 49-54 + export action l_49_decide(r:round, v:value, q:nset) = { + require _has_started; + require exists VR . round.minus_one <= VR & VR < r & _recved_proposal(proposers.get_proposer(r), r, v, VR); + require nset.is_quorum(q); + require nset.member(N,q) -> _recved_precommit(N,r,v); + require decision = value.nil; + + if value.valid(v) { + decision := v; + # MORE for next height + call abstract_protocol.decide(n, r, v, q); + } + } + + # line 55-56 + export action l_55(r:round, b:nset) = { + require _has_started; + require nset.is_blocking(b); + require nset.member(N,b) -> exists VR . round.minus_one <= VR & VR < r & exists V . _recved_proposal(N,r,V,VR) | _recved_prevote(N,r,V) | _recved_precommit(N,r,V); + require r > round_p; + call startRound(r); # line 56 + } + + # line 57-60 + export action onTimeoutPropose(r:round) = { + require _has_started; + require propose_timer_scheduled(r); + require r = round_p; + require step = step_t.propose; + call broadcast_prevote(r,value.nil); + step := step_t.prevote; + + call abstract_protocol.l_57(n,r); + + propose_timer_scheduled(r) := false; + } + + # line 61-64 + export action onTimeoutPrevote(r:round) = { + require _has_started; + require prevote_timer_scheduled(r); + require r = round_p; + require step = step_t.prevote; + call broadcast_precommit(r,value.nil); + step := step_t.precommit; + + call abstract_protocol.l_61(n,r); + + prevote_timer_scheduled(r) := false; + } + + # line 65-67 + export action onTimeoutPrecommit(r:round) = { + require _has_started; + require precommit_timer_scheduled(r); + require r = round_p; + call startRound(round.incr(r)); + + precommit_timer_scheduled(r) := false; + } + +# The Byzantine actions +# --------------------- + +# Byzantine nodes can send whatever they want, but they cannot send +# messages on behalf of well-behaved nodes. In practice this is implemented +# using cryptography (e.g. public-key cryptography). + + export action byzantine_send(m:msg, dst:node) = { + require ~well_behaved(n); + require ~well_behaved(m.m_src); # cannot forge the identity of well-behaved nodes + call shim.send(n,dst,m); + } + +# Byzantine nodes can also report fake observations, as defined in the abstract protocol. + export action fake_observations = { + call abstract_protocol.misbehave + } + +# Invariants +# ---------- + +# We provide common invariants that a) allow to prove that the abstract +# preconditions are met, and b) provide a refinement relation. + + + specification { + + invariant 0 <= round_p + invariant abstract_protocol.left_round(n,R) <-> R < round_p + + invariant lockedRound ~= round.minus_one -> forall R,V . abstract_protocol.locked(n,R,V) <-> R <= lockedRound & lockedValue = V + invariant lockedRound = round.minus_one -> forall R,V . ~abstract_protocol.locked(n,R,V) + + invariant forall M:msg . well_behaved(M.m_src) & M.m_kind = msg_kind.prevote & shim.sent(M,N) -> abstract_protocol.prevoted(M.m_src,M.m_round,M.m_value) + invariant well_behaved(N) & _recved_prevote(N,R,V) -> abstract_protocol.prevoted(N,R,V) + invariant forall M:msg . well_behaved(M.m_src) & M.m_kind = msg_kind.precommit & shim.sent(M,N) -> abstract_protocol.precommitted(M.m_src,M.m_round,M.m_value) + invariant well_behaved(N) & _recved_precommit(N,R,V) -> abstract_protocol.precommitted(N,R,V) + + invariant (step = step_t.prevote | step = step_t.propose) -> ~abstract_protocol.precommitted(n,round_p,V) + invariant step = step_t.propose -> ~abstract_protocol.prevoted(n,round_p,V) + invariant step = step_t.prevote -> exists V . abstract_protocol.prevoted(n,round_p,V) + + invariant round_p < R -> ~(abstract_protocol.prevoted(n,R,V) | abstract_protocol.precommitted(n,R,V)) + invariant ~_has_started -> step = step_t.propose & ~(abstract_protocol.prevoted(n,R,V) | abstract_protocol.precommitted(n,R,V)) & round_p = 0 + + invariant decision ~= value.nil -> exists R . abstract_protocol.decided(n,R,decision) + } + } +} diff --git a/spec/ivy-proofs/tendermint_test.ivy b/spec/ivy-proofs/tendermint_test.ivy new file mode 100644 index 000000000..1299fc086 --- /dev/null +++ b/spec/ivy-proofs/tendermint_test.ivy @@ -0,0 +1,127 @@ +#lang ivy1.7 + +include tendermint +include abstract_tendermint + +isolate ghost_ = { + instantiate abstract_tendermint +} + +isolate protocol = { + instantiate tendermint(ghost_) # here we instantiate the parameter of the tendermint module with `ghost_`; however note that we don't extract any code for `ghost_` (it's not in the list of object in the extract, and it's thus sliced away). + implementation { + definition init_val(n:node) = <<< `n`%2 >>> + } + # attribute test = impl +} with ghost_, shim, value, round, proposers + +# Here we run a simple scenario that exhibits an execution in which nodes make +# a decision. We do this to rule out trivial modeling errors. + +# One option to check that this scenario is valid is to run it in Ivy's REPL. +# For this, first compile the scenario: +#```ivyc target=repl isolate=code trace=true tendermint_test.ivy +# Then, run the produced binary (e.g. for 4 nodes): +#``` ./tendermint_test 4 +# Finally, call the action: +#``` scenarios.scenario_1 +# Note that Ivy will check at runtime that all action preconditions are +# satisfied. For example, runing the scenario twice will cause a violation of +# the precondition of the `start` action, because a node cannot start twice +# (see `require ~_has_started` in action `start`). + +# Another possibility would be to run `ivy_check` on the scenario, but that +# does not seem to work at the moment. + +isolate scenarios = { + individual all:nset # will be used as parameter to actions requiring a quorum + + after init { + var iter := node.iter.create(0); + while ~iter.is_end + { + all := all.insert(iter.val); + iter := iter.next; + }; + assert nset.is_quorum(all); # we can also use asserts to make sure we are getting what we expect + } + + export action scenario_1 = { + # all nodes start: + var iter := node.iter.create(0); + while ~iter.is_end + { + call protocol.server.start(iter.val); + iter := iter.next; + }; + # all nodes receive the leader's proposal: + var m:msg; + m.m_kind := msg_kind.proposal; + m.m_src := 0; + m.m_round := 0; + m.m_value := 0; + m.m_vround := round.minus_one; + iter := node.iter.create(0); + while ~iter.is_end + { + call net.recv(iter.val,m); + iter := iter.next; + }; + # all nodes prevote: + iter := node.iter.create(0); + while ~iter.is_end + { + call protocol.server.l_22(iter.val,0); + iter := iter.next; + }; + # all nodes receive each other's prevote messages; + m.m_kind := msg_kind.prevote; + m.m_vround := 0; + iter := node.iter.create(0); + while ~iter.is_end + { + var iter2 := node.iter.create(0); # the sender + while ~iter2.is_end + { + m.m_src := iter2.val; + call net.recv(iter.val,m); + iter2 := iter2.next; + }; + iter := iter.next; + }; + # all nodes precommit: + iter := node.iter.create(0); + while ~iter.is_end + { + call protocol.server.l_36(iter.val,0,0,all); + iter := iter.next; + }; + # all nodes receive each other's pre-commits + m.m_kind := msg_kind.precommit; + iter := node.iter.create(0); + while ~iter.is_end + { + var iter2 := node.iter.create(0); # the sender + while ~iter2.is_end + { + m.m_src := iter2.val; + call net.recv(iter.val,m); + iter2 := iter2.next; + }; + iter := iter.next; + }; + # now all nodes can decide: + iter := node.iter.create(0); + while ~iter.is_end + { + call protocol.server.l_49_decide(iter.val,0,0,all); + iter := iter.next; + }; + } + + # TODO: add more scenarios + +} with round, node, proposers, value, nset, protocol, shim, net + +# extract code = protocol, shim, round, node +extract code = round, node, proposers, value, nset, protocol, shim, net, scenarios diff --git a/spec/light-client/README.md b/spec/light-client/README.md new file mode 100644 index 000000000..42f20d46c --- /dev/null +++ b/spec/light-client/README.md @@ -0,0 +1,205 @@ +--- +order: 1 +parent: + title: Light Client + order: 5 +--- + +# Light Client Specification + +This directory contains work-in-progress English and TLA+ specifications for the Light Client +protocol. Implementations of the light client can be found in +[Rust](https://github.com/informalsystems/tendermint-rs/tree/master/light-client) and +[Go](https://github.com/tendermint/tendermint/tree/master/light). + +Light clients are assumed to be initialized once from a trusted source +with a trusted header and validator set. The light client +protocol allows a client to then securely update its trusted state by requesting and +verifying a minimal set of data from a network of full nodes (at least one of which is correct). + +The light client is decomposed into two main components: + +- [Commit Verification](#Commit-Verification) - verify signed headers and associated validator + set changes from a single full node, called primary +- [Attack Detection](#Attack-Detection) - verify commits across multiple full nodes (called secondaries) and detect conflicts (ie. the existence of a lightclient attack) + +In case a lightclient attack is detected, the lightclient submits evidence to a full node which is responsible for "accountability", that is, punishing attackers: + +- [Accountability](#Accountability) - given evidence for an attack, compute a set of validators that are responsible for it. + +## Commit Verification + +The [English specification](verification/verification_001_published.md) describes the light client +commit verification problem in terms of the temporal properties +[LCV-DIST-SAFE.1](https://github.com/informalsystems/tendermint-rs/blob/master/docs/spec/lightclient/verification/verification_001_published.md#lcv-dist-safe1) and +[LCV-DIST-LIVE.1](https://github.com/informalsystems/tendermint-rs/blob/master/docs/spec/lightclient/verification/verification_001_published.md#lcv-dist-live1). +Commit verification is assumed to operate within the Tendermint Failure Model, where +2/3 of validators are correct for some time period and +validator sets can change arbitrarily at each height. + +A light client protocol is also provided, including all checks that +need to be performed on headers, commits, and validator sets +to satisfy the temporal properties - so a light client can continuously +synchronize with a blockchain. Clients can skip possibly +many intermediate headers by exploiting overlap in trusted and untrusted validator sets. +When there is not enough overlap, a bisection routine can be used to find a +minimal set of headers that do provide the required overlap. + +The [TLA+ specification ver. 001](verification/Lightclient_A_1.tla) +is a formal description of the +commit verification protocol executed by a client, including the safety and +termination, which can be model checked with Apalache. + +A more detailed TLA+ specification of +[Light client verification ver. 003](verification/Lightclient_003_draft.tla) +is currently under peer review. + +The `MC*.tla` files contain concrete parameters for the +[TLA+ specification](verification/Lightclient_A_1.tla), in order to do model checking. +For instance, [MC4_3_faulty.tla](verification/MC4_3_faulty.tla) contains the following parameters +for the nodes, heights, the trusting period, the clock drifts, +correctness of the primary node, and the ratio of the faulty processes: + +```tla +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 3 +TRUSTING_PERIOD == 1400 \* the trusting period in some time units +CLOCK_DRIFT = 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT = 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators +``` + +To run a complete set of experiments, clone [apalache](https://github.com/informalsystems/apalache) and [apalache-tests](https://github.com/informalsystems/apalache-tests) into a directory `$DIR` and run the following commands: + +```sh +$DIR/apalache-tests/scripts/mk-run.py --memlimit 28 002bmc-apalache-ok.csv $DIR/apalache . out +./out/run-all.sh +``` + +After the experiments have finished, you can collect the logs by executing the following command: + +```sh +cd ./out +$DIR/apalache-tests/scripts/parse-logs.py --human . +``` + +All lines in `results.csv` should report `Deadlock`, which means that the algorithm +has terminated and no invariant violation was found. + +Similar to [002bmc-apalache-ok.csv](verification/002bmc-apalache-ok.csv), +file [003bmc-apalache-error.csv](verification/003bmc-apalache-error.csv) specifies +the set of experiments that should result in counterexamples: + +```sh +$DIR/apalache-tests/scripts/mk-run.py --memlimit 28 003bmc-apalache-error.csv $DIR/apalache . out +./out/run-all.sh +``` + +All lines in `results.csv` should report `Error`. + +The following table summarizes the experimental results for Light client verification +version 001. The TLA+ properties can be found in the +[TLA+ specification](verification/Lightclient_A_1.tla). + The experiments were run in an AWS instance equipped with 32GB +RAM and a 4-core Intel® Xeon® CPU E5-2686 v4 @ 2.30GHz CPU. +We write “✗=k” when a bug is reported at depth k, and “✓<=k” when +no bug is reported up to depth k. + +![Experimental results](experiments.png) + +The experimental results for version 003 are to be added. + +## Attack Detection + +The [English specification](detection/detection_003_reviewed.md) +defines light client attacks (and how they differ from blockchain +forks), and describes the problem of a light client detecting +these attacks by communicating with a network of full nodes, +where at least one is correct. + +The specification also contains a detection protocol that checks +whether the header obtained from the primary via the verification +protocol matches corresponding headers provided by the secondaries. +If this is not the case, the protocol analyses the verification traces +of the involved full nodes +and generates +[evidence](detection/detection_003_reviewed.md#tmbc-lc-evidence-data1) +of misbehavior that can be submitted to a full node so that +the faulty validators can be punished. + +The [TLA+ specification](detection/LCDetector_003_draft.tla) +is a formal description of the +detection protocol for two peers, including the safety and +termination, which can be model checked with Apalache. + +The `LCD_MC*.tla` files contain concrete parameters for the +[TLA+ specification](detection/LCDetector_003_draft.tla), +in order to run the model checker. +For instance, [LCD_MC4_4_faulty.tla](detection/MC4_4_faulty.tla) +contains the following parameters +for the nodes, heights, the trusting period, the clock drifts, +correctness of the nodes, and the ratio of the faulty processes: + +```tla +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 3 +TRUSTING_PERIOD == 1400 \* the trusting period in some time units +CLOCK_DRIFT = 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT = 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +IS_SECONDARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators +``` + +To run a complete set of experiments, clone [apalache](https://github.com/informalsystems/apalache) and [apalache-tests](https://github.com/informalsystems/apalache-tests) into a directory `$DIR` and run the following commands: + +```sh +$DIR/apalache-tests/scripts/mk-run.py --memlimit 28 004bmc-apalache-ok.csv $DIR/apalache . out +./out/run-all.sh +``` + +After the experiments have finished, you can collect the logs by executing the following command: + +```sh +cd ./out +$DIR/apalache-tests/scripts/parse-logs.py --human . +``` + +All lines in `results.csv` should report `Deadlock`, which means that the algorithm +has terminated and no invariant violation was found. + +Similar to [004bmc-apalache-ok.csv](verification/004bmc-apalache-ok.csv), +file [005bmc-apalache-error.csv](verification/005bmc-apalache-error.csv) specifies +the set of experiments that should result in counterexamples: + +```sh +$DIR/apalache-tests/scripts/mk-run.py --memlimit 28 005bmc-apalache-error.csv $DIR/apalache . out +./out/run-all.sh +``` + +All lines in `results.csv` should report `Error`. + +The detailed experimental results are to be added soon. + +## Accountability + +The [English specification](attacks/isolate-attackers_002_reviewed.md) +defines the protocol that is executed on a full node upon receiving attack [evidence](detection/detection_003_reviewed.md#tmbc-lc-evidence-data1) from a lightclient. In particular, the protocol handles three types of attacks + +- lunatic +- equivocation +- amnesia + +We discussed in the [last part](attacks/isolate-attackers_002_reviewed.md#Part-III---Completeness) of the English specification +that the non-lunatic cases are defined by having the same validator set in the conflicting blocks. For these cases, +computer-aided analysis of [Tendermint Consensus in TLA+](./accountability/README.md) shows that equivocation and amnesia capture all non-lunatic attacks. + +The [TLA+ specification](attacks/Isolation_001_draft.tla) +is a formal description of the +protocol, including the safety property, which can be model checked with Apalache. + +Similar to the other specifications, [MC_5_3.tla](attacks/MC_5_3.tla) contains concrete parameters to run the model checker. The specification can be checked within seconds. + +[tendermint-accountability](./accountability/README.md) diff --git a/spec/light-client/accountability/001indinv-apalache.csv b/spec/light-client/accountability/001indinv-apalache.csv new file mode 100644 index 000000000..37c6aeda2 --- /dev/null +++ b/spec/light-client/accountability/001indinv-apalache.csv @@ -0,0 +1,13 @@ +no,filename,tool,timeout,init,inv,next,args +1,MC_n4_f1.tla,apalache,10h,TypedInv,TypedInv,,--length=1 --cinit=ConstInit +2,MC_n4_f2.tla,apalache,10h,TypedInv,TypedInv,,--length=1 --cinit=ConstInit +3,MC_n5_f1.tla,apalache,10h,TypedInv,TypedInv,,--length=1 --cinit=ConstInit +4,MC_n5_f2.tla,apalache,10h,TypedInv,TypedInv,,--length=1 --cinit=ConstInit +5,MC_n4_f1.tla,apalache,20h,Init,TypedInv,,--length=0 --cinit=ConstInit +6,MC_n4_f2.tla,apalache,20h,Init,TypedInv,,--length=0 --cinit=ConstInit +7,MC_n5_f1.tla,apalache,20h,Init,TypedInv,,--length=0 --cinit=ConstInit +8,MC_n5_f2.tla,apalache,20h,Init,TypedInv,,--length=0 --cinit=ConstInit +9,MC_n4_f1.tla,apalache,20h,TypedInv,Agreement,,--length=0 --cinit=ConstInit +10,MC_n4_f2.tla,apalache,20h,TypedInv,Accountability,,--length=0 --cinit=ConstInit +11,MC_n5_f1.tla,apalache,20h,TypedInv,Agreement,,--length=0 --cinit=ConstInit +12,MC_n5_f2.tla,apalache,20h,TypedInv,Accountability,,--length=0 --cinit=ConstInit diff --git a/spec/light-client/accountability/MC_n4_f1.tla b/spec/light-client/accountability/MC_n4_f1.tla new file mode 100644 index 000000000..7a828b498 --- /dev/null +++ b/spec/light-client/accountability/MC_n4_f1.tla @@ -0,0 +1,22 @@ +----------------------------- MODULE MC_n4_f1 ------------------------------- +CONSTANT Proposer \* the proposer function from 0..NRounds to 1..N + +\* the variables declared in TendermintAcc3 +VARIABLES + round, step, decision, lockedValue, lockedRound, validValue, validRound, + msgsPropose, msgsPrevote, msgsPrecommit, evidence, action + +INSTANCE TendermintAccDebug_004_draft WITH + Corr <- {"c1", "c2", "c3"}, + Faulty <- {"f1"}, + N <- 4, + T <- 1, + ValidValues <- { "v0", "v1" }, + InvalidValues <- {"v2"}, + MaxRound <- 2 + +\* run Apalache with --cinit=ConstInit +ConstInit == \* the proposer is arbitrary -- works for safety + Proposer \in [Rounds -> AllProcs] + +============================================================================= diff --git a/spec/light-client/accountability/MC_n4_f2.tla b/spec/light-client/accountability/MC_n4_f2.tla new file mode 100644 index 000000000..893f18db6 --- /dev/null +++ b/spec/light-client/accountability/MC_n4_f2.tla @@ -0,0 +1,22 @@ +----------------------------- MODULE MC_n4_f2 ------------------------------- +CONSTANT Proposer \* the proposer function from 0..NRounds to 1..N + +\* the variables declared in TendermintAcc3 +VARIABLES + round, step, decision, lockedValue, lockedRound, validValue, validRound, + msgsPropose, msgsPrevote, msgsPrecommit, evidence, action + +INSTANCE TendermintAccDebug_004_draft WITH + Corr <- {"c1", "c2"}, + Faulty <- {"f3", "f4"}, + N <- 4, + T <- 1, + ValidValues <- { "v0", "v1" }, + InvalidValues <- {"v2"}, + MaxRound <- 2 + +\* run Apalache with --cinit=ConstInit +ConstInit == \* the proposer is arbitrary -- works for safety + Proposer \in [Rounds -> AllProcs] + +============================================================================= diff --git a/spec/light-client/accountability/MC_n4_f2_amnesia.tla b/spec/light-client/accountability/MC_n4_f2_amnesia.tla new file mode 100644 index 000000000..434fffaeb --- /dev/null +++ b/spec/light-client/accountability/MC_n4_f2_amnesia.tla @@ -0,0 +1,40 @@ +---------------------- MODULE MC_n4_f2_amnesia ------------------------------- +EXTENDS Sequences + +CONSTANT Proposer \* the proposer function from 0..NRounds to 1..N + +\* the variables declared in TendermintAcc3 +VARIABLES + round, step, decision, lockedValue, lockedRound, validValue, validRound, + msgsPropose, msgsPrevote, msgsPrecommit, evidence, action + +\* the variable declared in TendermintAccTrace3 +VARIABLE + toReplay + +\* old apalache annotations, fix with the new release +a <: b == a + +INSTANCE TendermintAccTrace_004_draft WITH + Corr <- {"c1", "c2"}, + Faulty <- {"f3", "f4"}, + N <- 4, + T <- 1, + ValidValues <- { "v0", "v1" }, + InvalidValues <- {"v2"}, + MaxRound <- 2, + Trace <- << + "UponProposalInPropose", + "UponProposalInPrevoteOrCommitAndPrevote", + "UponProposalInPrecommitNoDecision", + "OnRoundCatchup", + "UponProposalInPropose", + "UponProposalInPrevoteOrCommitAndPrevote", + "UponProposalInPrecommitNoDecision" + >> <: Seq(STRING) + +\* run Apalache with --cinit=ConstInit +ConstInit == \* the proposer is arbitrary -- works for safety + Proposer \in [Rounds -> AllProcs] + +============================================================================= diff --git a/spec/light-client/accountability/MC_n4_f3.tla b/spec/light-client/accountability/MC_n4_f3.tla new file mode 100644 index 000000000..b794fff5e --- /dev/null +++ b/spec/light-client/accountability/MC_n4_f3.tla @@ -0,0 +1,22 @@ +----------------------------- MODULE MC_n4_f3 ------------------------------- +CONSTANT Proposer \* the proposer function from 0..NRounds to 1..N + +\* the variables declared in TendermintAcc3 +VARIABLES + round, step, decision, lockedValue, lockedRound, validValue, validRound, + msgsPropose, msgsPrevote, msgsPrecommit, evidence, action + +INSTANCE TendermintAccDebug_004_draft WITH + Corr <- {"c1"}, + Faulty <- {"f2", "f3", "f4"}, + N <- 4, + T <- 1, + ValidValues <- { "v0", "v1" }, + InvalidValues <- {"v2"}, + MaxRound <- 2 + +\* run Apalache with --cinit=ConstInit +ConstInit == \* the proposer is arbitrary -- works for safety + Proposer \in [Rounds -> AllProcs] + +============================================================================= diff --git a/spec/light-client/accountability/MC_n5_f1.tla b/spec/light-client/accountability/MC_n5_f1.tla new file mode 100644 index 000000000..d65673a58 --- /dev/null +++ b/spec/light-client/accountability/MC_n5_f1.tla @@ -0,0 +1,22 @@ +----------------------------- MODULE MC_n5_f1 ------------------------------- +CONSTANT Proposer \* the proposer function from 0..NRounds to 1..N + +\* the variables declared in TendermintAcc3 +VARIABLES + round, step, decision, lockedValue, lockedRound, validValue, validRound, + msgsPropose, msgsPrevote, msgsPrecommit, evidence, action + +INSTANCE TendermintAccDebug_004_draft WITH + Corr <- {"c1", "c2", "c3", "c4"}, + Faulty <- {"f5"}, + N <- 5, + T <- 1, + ValidValues <- { "v0", "v1" }, + InvalidValues <- {"v2"}, + MaxRound <- 2 + +\* run Apalache with --cinit=ConstInit +ConstInit == \* the proposer is arbitrary -- works for safety + Proposer \in [Rounds -> AllProcs] + +============================================================================= diff --git a/spec/light-client/accountability/MC_n5_f2.tla b/spec/light-client/accountability/MC_n5_f2.tla new file mode 100644 index 000000000..c19aa98cc --- /dev/null +++ b/spec/light-client/accountability/MC_n5_f2.tla @@ -0,0 +1,22 @@ +----------------------------- MODULE MC_n5_f2 ------------------------------- +CONSTANT Proposer \* the proposer function from 0..NRounds to 1..N + +\* the variables declared in TendermintAcc3 +VARIABLES + round, step, decision, lockedValue, lockedRound, validValue, validRound, + msgsPropose, msgsPrevote, msgsPrecommit, evidence, action + +INSTANCE TendermintAccDebug_004_draft WITH + Corr <- {"c1", "c2", "c3"}, + Faulty <- {"f4", "f5"}, + N <- 5, + T <- 1, + ValidValues <- { "v0", "v1" }, + InvalidValues <- {"v2"}, + MaxRound <- 2 + +\* run Apalache with --cinit=ConstInit +ConstInit == \* the proposer is arbitrary -- works for safety + Proposer \in [Rounds -> AllProcs] + +============================================================================= diff --git a/spec/light-client/accountability/MC_n6_f1.tla b/spec/light-client/accountability/MC_n6_f1.tla new file mode 100644 index 000000000..2e992974f --- /dev/null +++ b/spec/light-client/accountability/MC_n6_f1.tla @@ -0,0 +1,22 @@ +----------------------------- MODULE MC_n6_f1 ------------------------------- +CONSTANT Proposer \* the proposer function from 0..NRounds to 1..N + +\* the variables declared in TendermintAcc3 +VARIABLES + round, step, decision, lockedValue, lockedRound, validValue, validRound, + msgsPropose, msgsPrevote, msgsPrecommit, evidence, action + +INSTANCE TendermintAccDebug_004_draft WITH + Corr <- {"c1", "c2", "c3", "c4", "c5"}, + Faulty <- {"f6"}, + N <- 4, + T <- 1, + ValidValues <- { "v0", "v1" }, + InvalidValues <- {"v2"}, + MaxRound <- 2 + +\* run Apalache with --cinit=ConstInit +ConstInit == \* the proposer is arbitrary -- works for safety + Proposer \in [Rounds -> AllProcs] + +============================================================================= diff --git a/spec/light-client/accountability/README.md b/spec/light-client/accountability/README.md new file mode 100644 index 000000000..bb872649b --- /dev/null +++ b/spec/light-client/accountability/README.md @@ -0,0 +1,308 @@ +--- +order: 1 +parent: + title: Accountability + order: 4 +--- + +# Fork accountability + +## Problem Statement + +Tendermint consensus guarantees the following specifications for all heights: + +* agreement -- no two correct full nodes decide differently. +* validity -- the decided block satisfies the predefined predicate *valid()*. +* termination -- all correct full nodes eventually decide, + +If the faulty validators have less than 1/3 of voting power in the current validator set. In the case where this assumption +does not hold, each of the specification may be violated. + +The agreement property says that for a given height, any two correct validators that decide on a block for that height decide on the same block. That the block was indeed generated by the blockchain, can be verified starting from a trusted (genesis) block, and checking that all subsequent blocks are properly signed. + +However, faulty nodes may forge blocks and try to convince users (light clients) that the blocks had been correctly generated. In addition, Tendermint agreement might be violated in the case where 1/3 or more of the voting power belongs to faulty validators: Two correct validators decide on different blocks. The latter case motivates the term "fork": as Tendermint consensus also agrees on the next validator set, correct validators may have decided on disjoint next validator sets, and the chain branches into two or more partitions (possibly having faulty validators in common) and each branch continues to generate blocks independently of the other. + +We say that a fork is a case in which there are two commits for different blocks at the same height of the blockchain. The proplem is to ensure that in those cases we are able to detect faulty validators (and not mistakenly accuse correct validators), and incentivize therefore validators to behave according to the protocol specification. + +**Conceptual Limit.** In order to prove misbehavior of a node, we have to show that the behavior deviates from correct behavior with respect to a given algorithm. Thus, an algorithm that detects misbehavior of nodes executing some algorithm *A* must be defined with respect to algorithm *A*. In our case, *A* is Tendermint consensus (+ other protocols in the infrastructure; e.g.,full nodes and the Light Client). If the consensus algorithm is changed/updated/optimized in the future, we have to check whether changes to the accountability algorithm are also required. All the discussions in this document are thus inherently specific to Tendermint consensus and the Light Client specification. + +**Q:** Should we distinguish agreement for validators and full nodes for agreement? The case where all correct validators agree on a block, but a correct full node decides on a different block seems to be slightly less severe that the case where two correct validators decide on different blocks. Still, if a contaminated full node becomes validator that may be problematic later on. Also it is not clear how gossiping is impaired if a contaminated full node is on a different branch. + +*Remark.* In the case 1/3 or more of the voting power belongs to faulty validators, also validity and termination can be broken. Termination can be broken if faulty processes just do not send the messages that are needed to make progress. Due to asynchrony, this is not punishable, because faulty validators can always claim they never received the messages that would have forced them to send messages. + +## The Misbehavior of Faulty Validators + +Forks are the result of faulty validators deviating from the protocol. In principle several such deviations can be detected without a fork actually occurring: + +1. double proposal: A faulty proposer proposes two different values (blocks) for the same height and the same round in Tendermint consensus. + +2. double signing: Tendermint consensus forces correct validators to prevote and precommit for at most one value per round. In case a faulty validator sends multiple prevote and/or precommit messages for different values for the same height/round, this is a misbehavior. + +3. lunatic validator: Tendermint consensus forces correct validators to prevote and precommit only for values *v* that satisfy *valid(v)*. If faulty validators prevote and precommit for *v* although *valid(v)=false* this is misbehavior. + +*Remark.* In isolation, Point 3 is an attack on validity (rather than agreement). However, the prevotes and precommits can then also be used to forge blocks. + +1. amnesia: Tendermint consensus has a locking mechanism. If a validator has some value v locked, then it can only prevote/precommit for v or nil. Sending prevote/precomit message for a different value v' (that is not nil) while holding lock on value v is misbehavior. + +2. spurious messages: In Tendermint consensus most of the message send instructions are guarded by threshold guards, e.g., one needs to receive *2f + 1* prevote messages to send precommit. Faulty validators may send precommit without having received the prevote messages. + +Independently of a fork happening, punishing this behavior might be important to prevent forks altogether. This should keep attackers from misbehaving: if less than 1/3 of the voting power is faulty, this misbehavior is detectable but will not lead to a safety violation. Thus, unless they have 1/3 or more (or in some cases more than 2/3) of the voting power attackers have the incentive to not misbehave. If attackers control too much voting power, we have to deal with forks, as discussed in this document. + +## Two types of forks + +* Fork-Full. Two correct validators decide on different blocks for the same height. Since also the next validator sets are decided upon, the correct validators may be partitioned to participate in two distinct branches of the forked chain. + +As in this case we have two different blocks (both having the same right/no right to exist), a central system invariant (one block per height decided by correct validators) is violated. As full nodes are contaminated in this case, the contamination can spread also to light clients. However, even without breaking this system invariant, light clients can be subject to a fork: + +* Fork-Light. All correct validators decide on the same block for height *h*, but faulty processes (validators or not), forge a different block for that height, in order to fool users (who use the light client). + +# Attack scenarios + +## On-chain attacks + +### Equivocation (one round) + +There are several scenarios in which forks might happen. The first is double signing within a round. + +* F1. Equivocation: faulty validators sign multiple vote messages (prevote and/or precommit) for different values *during the same round r* at a given height h. + +### Flip-flopping + +Tendermint consensus implements a locking mechanism: If a correct validator *p* receives proposal for value v and *2f + 1* prevotes for a value *id(v)* in round *r*, it locks *v* and remembers *r*. In this case, *p* also sends a precommit message for *id(v)*, which later may serve as proof that *p* locked *v*. +In subsequent rounds, *p* only sends prevote messages for a value it had previously locked. However, it is possible to change the locked value if in a future round *r' > r*, if the process receives proposal and *2f + 1* prevotes for a different value *v'*. In this case, *p* could send a prevote/precommit for *id(v')*. This algorithmic feature can be exploited in two ways: + +* F2. Faulty Flip-flopping (Amnesia): faulty validators precommit some value *id(v)* in round *r* (value *v* is locked in round *r*) and then prevote for different value *id(v')* in higher round *r' > r* without previously correctly unlocking value *v*. In this case faulty processes "forget" that they have locked value *v* and prevote some other value in the following rounds. +Some correct validators might have decided on *v* in *r*, and other correct validators decide on *v'* in *r'*. Here we can have branching on the main chain (Fork-Full). + +* F3. Correct Flip-flopping (Back to the past): There are some precommit messages signed by (correct) validators for value *id(v)* in round *r*. Still, *v* is not decided upon, and all processes move on to the next round. Then correct validators (correctly) lock and decide a different value *v'* in some round *r' > r*. And the correct validators continue; there is no branching on the main chain. +However, faulty validators may use the correct precommit messages from round *r* together with a posteriori generated faulty precommit messages for round *r* to forge a block for a value that was not decided on the main chain (Fork-Light). + +## Off-chain attacks + +F1-F3 may contaminate the state of full nodes (and even validators). Contaminated (but otherwise correct) full nodes may thus communicate faulty blocks to light clients. +Similarly, without actually interfering with the main chain, we can have the following: + +* F4. Phantom validators: faulty validators vote (sign prevote and precommit messages) in heights in which they are not part of the validator sets (at the main chain). + +* F5. Lunatic validator: faulty validator that sign vote messages to support (arbitrary) application state that is different from the application state that resulted from valid state transitions. + +## Types of victims + +We consider three types of potential attack victims: + +* FN: full node +* LCS: light client with sequential header verification +* LCB: light client with bisection based header verification + +F1 and F2 can be used by faulty validators to actually create multiple branches on the blockchain. That means that correctly operating full nodes decide on different blocks for the same height. Until a fork is detected locally by a full node (by receiving evidence from others or by some other local check that fails), the full node can spread corrupted blocks to light clients. + +*Remark.* If full nodes take a branch different from the one taken by the validators, it may be that the liveness of the gossip protocol may be affected. We should eventually look at this more closely. However, as it does not influence safety it is not a primary concern. + +F3 is similar to F1, except that no two correct validators decide on different blocks. It may still be the case that full nodes become affected. + +In addition, without creating a fork on the main chain, light clients can be contaminated by more than a third of validators that are faulty and sign a forged header +F4 cannot fool correct full nodes as they know the current validator set. Similarly, LCS know who the validators are. Hence, F4 is an attack against LCB that do not necessarily know the complete prefix of headers (Fork-Light), as they trust a header that is signed by at least one correct validator (trusting period method). + +The following table gives an overview of how the different attacks may affect different nodes. F1-F3 are *on-chain* attacks so they can corrupt the state of full nodes. Then if a light client (LCS or LCB) contacts a full node to obtain headers (or blocks), the corrupted state may propagate to the light client. + +F4 and F5 are *off-chain*, that is, these attacks cannot be used to corrupt the state of full nodes (which have sufficient knowledge on the state of the chain to not be fooled). + +| Attack | FN | LCS | LCB | +|:------:|:------:|:------:|:------:| +| F1 | direct | FN | FN | +| F2 | direct | FN | FN | +| F3 | direct | FN | FN | +| F4 | | | direct | +| F5 | | | direct | + +**Q:** Light clients are more vulnerable than full nodes, because the former do only verify headers but do not execute transactions. What kind of certainty is gained by a full node that executes a transaction? + +As a full node verifies all transactions, it can only be +contaminated by an attack if the blockchain itself violates its invariant (one block per height), that is, in case of a fork that leads to branching. + +## Detailed Attack Scenarios + +### Equivocation based attacks + +In case of equivocation based attacks, faulty validators sign multiple votes (prevote and/or precommit) in the same +round of some height. This attack can be executed on both full nodes and light clients. It requires 1/3 or more of voting power to be executed. + +#### Scenario 1: Equivocation on the main chain + +Validators: + +* CA - a set of correct validators with less than 1/3 of the voting power +* CB - a set of correct validators with less than 1/3 of the voting power +* CA and CB are disjoint +* F - a set of faulty validators with 1/3 or more voting power + +Observe that this setting violates the Tendermint failure model. + +Execution: + +* A faulty proposer proposes block A to CA +* A faulty proposer proposes block B to CB +* Validators from the set CA and CB prevote for A and B, respectively. +* Faulty validators from the set F prevote both for A and B. +* The faulty prevote messages + * for A arrive at CA long before the B messages + * for B arrive at CB long before the A messages +* Therefore correct validators from set CA and CB will observe +more than 2/3 of prevotes for A and B and precommit for A and B, respectively. +* Faulty validators from the set F precommit both values A and B. +* Thus, we have more than 2/3 commits for both A and B. + +Consequences: + +* Creating evidence of misbehavior is simple in this case as we have multiple messages signed by the same faulty processes for different values in the same round. + +* We have to ensure that these different messages reach a correct process (full node, monitor?), which can submit evidence. + +* This is an attack on the full node level (Fork-Full). +* It extends also to the light clients, +* For both we need a detection and recovery mechanism. + +#### Scenario 2: Equivocation to a light client (LCS) + +Validators: + +* a set F of faulty validators with more than 2/3 of the voting power. + +Execution: + +* for the main chain F behaves nicely +* F coordinates to sign a block B that is different from the one on the main chain. +* the light clients obtains B and trusts at as it is signed by more than 2/3 of the voting power. + +Consequences: + +Once equivocation is used to attack light client it opens space +for different kind of attacks as application state can be diverged in any direction. For example, it can modify validator set such that it contains only validators that do not have any stake bonded. Note that after a light client is fooled by a fork, that means that an attacker can change application state and validator set arbitrarily. + +In order to detect such (equivocation-based attack), the light client would need to cross check its state with some correct validator (or to obtain a hash of the state from the main chain using out of band channels). + +*Remark.* The light client would be able to create evidence of misbehavior, but this would require to pull potentially a lot of data from correct full nodes. Maybe we need to figure out different architecture where a light client that is attacked will push all its data for the current unbonding period to a correct node that will inspect this data and submit corresponding evidence. There are also architectures that assumes a special role (sometimes called fisherman) whose goal is to collect as much as possible useful data from the network, to do analysis and create evidence transactions. That functionality is outside the scope of this document. + +*Remark.* The difference between LCS and LCB might only be in the amount of voting power needed to convince light client about arbitrary state. In case of LCB where security threshold is at minimum, an attacker can arbitrarily modify application state with 1/3 or more of voting power, while in case of LCS it requires more than 2/3 of the voting power. + +### Flip-flopping: Amnesia based attacks + +In case of amnesia, faulty validators lock some value *v* in some round *r*, and then vote for different value *v'* in higher rounds without correctly unlocking value *v*. This attack can be used both on full nodes and light clients. + +#### Scenario 3: At most 2/3 of faults + +Validators: + +* a set F of faulty validators with 1/3 or more but at most 2/3 of the voting power +* a set C of correct validators + +Execution: + +* Faulty validators commit (without exposing it on the main chain) a block A in round *r* by collecting more than 2/3 of the + voting power (containing correct and faulty validators). +* All validators (correct and faulty) reach a round *r' > r*. +* Some correct validators in C do not lock any value before round *r'*. +* The faulty validators in F deviate from Tendermint consensus by ignoring that they locked A in *r*, and propose a different block B in *r'*. +* As the validators in C that have not locked any value find B acceptable, they accept the proposal for B and commit a block B. + +*Remark.* In this case, the more than 1/3 of faulty validators do not need to commit an equivocation (F1) as they only vote once per round in the execution. + +Detecting faulty validators in the case of such an attack can be done by the fork accountability mechanism described in: . + +If a light client is attacked using this attack with 1/3 or more of voting power (and less than 2/3), the attacker cannot change the application state arbitrarily. Rather, the attacker is limited to a state a correct validator finds acceptable: In the execution above, correct validators still find the value acceptable, however, the block the light client trusts deviates from the one on the main chain. + +#### Scenario 4: More than 2/3 of faults + +In case there is an attack with more than 2/3 of the voting power, an attacker can arbitrarily change application state. + +Validators: + +* a set F1 of faulty validators with 1/3 or more of the voting power +* a set F2 of faulty validators with less than 1/3 of the voting power + +Execution + +* Similar to Scenario 3 (however, messages by correct validators are not needed) +* The faulty validators in F1 lock value A in round *r* +* They sign a different value in follow-up rounds +* F2 does not lock A in round *r* + +Consequences: + +* The validators in F1 will be detectable by the the fork accountability mechanisms. +* The validators in F2 cannot be detected using this mechanism. +Only in case they signed something which conflicts with the application this can be used against them. Otherwise they do not do anything incorrect. +* This case is not covered by the report as it only assumes at most 2/3 of faulty validators. + +**Q:** do we need to define a special kind of attack for the case where a validator sign arbitrarily state? It seems that detecting such attack requires a different mechanism that would require as an evidence a sequence of blocks that led to that state. This might be very tricky to implement. + +### Back to the past + +In this kind of attack, faulty validators take advantage of the fact that they did not sign messages in some of the past rounds. Due to the asynchronous network in which Tendermint operates, we cannot easily differentiate between such an attack and delayed message. This kind of attack can be used at both full nodes and light clients. + +#### Scenario 5 + +Validators: + +* C1 - a set of correct validators with over 1/3 of the voting power +* C2 - a set of correct validators with 1/3 of the voting power +* C1 and C2 are disjoint +* F - a set of faulty validators with less than 1/3 voting power +* one additional faulty process *q* +* F and *q* violate the Tendermint failure model. + +Execution: + +* in a round *r* of height *h* we have C1 precommitting a value A, +* C2 precommits nil, +* F does not send any message +* *q* precommits nil. +* In some round *r' > r*, F and *q* and C2 commit some other value B different from A. +* F and *fp* "go back to the past" and sign precommit message for value A in round *r*. +* Together with precomit messages of C1 this is sufficient for a commit for value A. + +Consequences: + +* Only a single faulty validator that previously precommited nil did equivocation, while the other 1/3 of faulty validators actually executed an attack that has exactly the same sequence of messages as part of amnesia attack. Detecting this kind of attack boil down to mechanisms for equivocation and amnesia. + +**Q:** should we keep this as a separate kind of attack? It seems that equivocation, amnesia and phantom validators are the only kind of attack we need to support and this gives us security also in other cases. This would not be surprising as equivocation and amnesia are attacks that followed from the protocol and phantom attack is not really an attack to Tendermint but more to the Proof of Stake module. + +### Phantom validators + +In case of phantom validators, processes that are not part of the current validator set but are still bonded (as attack happen during their unbonding period) can be part of the attack by signing vote messages. This attack can be executed against both full nodes and light clients. + +#### Scenario 6 + +Validators: + +* F -- a set of faulty validators that are not part of the validator set on the main chain at height *h + k* + +Execution: + +* There is a fork, and there exist two different headers for height *h + k*, with different validator sets: + * VS2 on the main chain + * forged header VS2', signed by F (and others) + +* a light client has a trust in a header for height *h* (and the corresponding validator set VS1). +* As part of bisection header verification, it verifies the header at height *h + k* with new validator set VS2'. + +Consequences: + +* To detect this, a node needs to see both, the forged header and the canonical header from the chain. +* If this is the case, detecting these kind of attacks is easy as it just requires verifying if processes are signing messages in heights in which they are not part of the validator set. + +**Remark.** We can have phantom-validator-based attacks as a follow up of equivocation or amnesia based attack where forked state contains validators that are not part of the validator set at the main chain. In this case, they keep signing messages contributed to a forked chain (the wrong branch) although they are not part of the validator set on the main chain. This attack can also be used to attack full node during a period of time it is eclipsed. + +**Remark.** Phantom validator evidence has been removed from implementation as it was deemed, although possibly a plausible form of evidence, not relevant. Any attack on +the light client involving a phantom validator will have needed to be initiated by 1/3+ lunatic +validators that can forge a new validator set that includes the phantom validator. Only in +that case will the light client accept the phantom validators vote. We need only worry about +punishing the 1/3+ lunatic cabal, that is the root cause of the attack. + +### Lunatic validator + +Lunatic validator agrees to sign commit messages for arbitrary application state. It is used to attack light clients. +Note that detecting this behavior require application knowledge. Detecting this behavior can probably be done by +referring to the block before the one in which height happen. + +**Q:** can we say that in this case a validator declines to check if a proposed value is valid before voting for it? diff --git a/spec/light-client/accountability/Synopsis.md b/spec/light-client/accountability/Synopsis.md new file mode 100644 index 000000000..76da3868c --- /dev/null +++ b/spec/light-client/accountability/Synopsis.md @@ -0,0 +1,105 @@ + +# Synopsis + + A TLA+ specification of a simplified Tendermint consensus, tuned for + fork accountability. The simplifications are as follows: + +- the procotol runs for one height, that is, one-shot consensus + +- this specification focuses on safety, so timeouts are modelled with + with non-determinism + +- the proposer function is non-determinstic, no fairness is assumed + +- the messages by the faulty processes are injected right in the initial states + +- every process has the voting power of 1 + +- hashes are modelled as identity + + Having the above assumptions in mind, the specification follows the pseudo-code + of the Tendermint paper: + + Byzantine processes can demonstrate arbitrary behavior, including + no communication. However, we have to show that under the collective evidence + collected by the correct processes, at least `f+1` Byzantine processes demonstrate + one of the following behaviors: + +- Equivocation: a Byzantine process sends two different values + in the same round. + +- Amnesia: a Byzantine process locks a value, although it has locked + another value in the past. + +# TLA+ modules + +- [TendermintAcc_004_draft](TendermintAcc_004_draft.tla) is the protocol + specification, + +- [TendermintAccInv_004_draft](TendermintAccInv_004_draft.tla) contains an + inductive invariant for establishing the protocol safety as well as the + forking cases, + +- `MC_n_f`, e.g., [MC_n4_f1](MC_n4_f1.tla), contains fixed constants for + model checking with the [Apalache model + checker](https://github.com/informalsystems/apalache), + +- [TendermintAccTrace_004_draft](TendermintAccTrace_004_draft.tla) shows how + to restrict the execution space to a fixed sequence of actions (e.g., to + instantiate a counterexample), + +- [TendermintAccDebug_004_draft](TendermintAccDebug_004_draft.tla) contains + the useful definitions for debugging the protocol specification with TLC and + Apalache. + +# Reasoning about fork scenarios + +The theorem statements can be found in +[TendermintAccInv_004_draft.tla](TendermintAccInv_004_draft.tla). + +First, we would like to show that `TypedInv` is an inductive invariant. +Formally, the statement looks as follows: + +```tla +THEOREM TypedInvIsInductive == + \/ FaultyQuorum + \//\ Init => TypedInv + /\ TypedInv /\ [Next]_vars => TypedInv' +``` + +When over two-thirds of processes are faulty, `TypedInv` is not inductive. +However, there is no hope to repair the protocol in this case. We run +[Apalache](https://github.com/informalsystems/apalache) to prove this theorem +only for fixed instances of 4 to 5 validators. Apalache does not parse theorem +statements at the moment, so we ran Apalache using a shell script. To find a +parameterized argument, one has to use a theorem prover, e.g., TLAPS. + +Second, we would like to show that the invariant implies `Agreement`, that is, +no fork, provided that less than one third of processes is faulty. By combining +this theorem with the previous theorem, we conclude that the protocol indeed +satisfies Agreement under the condition `LessThanThirdFaulty`. + +```tla +THEOREM AgreementWhenLessThanThirdFaulty == + LessThanThirdFaulty /\ TypedInv => Agreement +``` + +Third, in the general case, we either have no fork, or two fork scenarios: + +```tla +THEOREM AgreementOrFork == + ~FaultyQuorum /\ TypedInv => Accountability +``` + +# Model checking results + +Check the report on [model checking with Apalache](./results/001indinv-apalache-report.md). + +To run the model checking experiments, use the script: + +```console +./run.sh +``` + +This script assumes that the apalache build is available in +`~/devl/apalache-unstable`. diff --git a/spec/light-client/accountability/TendermintAccDebug_004_draft.tla b/spec/light-client/accountability/TendermintAccDebug_004_draft.tla new file mode 100644 index 000000000..deaa990ea --- /dev/null +++ b/spec/light-client/accountability/TendermintAccDebug_004_draft.tla @@ -0,0 +1,100 @@ +------------------ MODULE TendermintAccDebug_004_draft ------------------------- +(* + A few definitions that we use for debugging TendermintAcc3, which do not belong + to the specification itself. + + * Version 3. Modular and parameterized definitions. + + Igor Konnov, 2020. + *) + +EXTENDS TendermintAccInv_004_draft + +\* make them parameters? +NFaultyProposals == 0 \* the number of injected faulty PROPOSE messages +NFaultyPrevotes == 6 \* the number of injected faulty PREVOTE messages +NFaultyPrecommits == 6 \* the number of injected faulty PRECOMMIT messages + +\* Given a set of allowed messages Msgs, this operator produces a function from +\* rounds to sets of messages. +\* Importantly, there will be exactly k messages in the image of msgFun. +\* We use this action to produce k faults in an initial state. +ProduceFaults(msgFun, From, k) == + \E f \in [1..k -> From]: + msgFun = [r \in Rounds |-> {m \in {f[i]: i \in 1..k}: m.round = r}] + +\* As TLC explodes with faults, we may have initial states without faults +InitNoFaults == + /\ round = [p \in Corr |-> 0] + /\ step = [p \in Corr |-> "PROPOSE"] + /\ decision = [p \in Corr |-> NilValue] + /\ lockedValue = [p \in Corr |-> NilValue] + /\ lockedRound = [p \in Corr |-> NilRound] + /\ validValue = [p \in Corr |-> NilValue] + /\ validRound = [p \in Corr |-> NilRound] + /\ msgsPropose = [r \in Rounds |-> EmptyMsgSet] + /\ msgsPrevote = [r \in Rounds |-> EmptyMsgSet] + /\ msgsPrecommit = [r \in Rounds |-> EmptyMsgSet] + /\ evidence = EmptyMsgSet + +(* + A specialized version of Init that injects NFaultyProposals proposals, + NFaultyPrevotes prevotes, NFaultyPrecommits precommits by the faulty processes + *) +InitFewFaults == + /\ round = [p \in Corr |-> 0] + /\ step = [p \in Corr |-> "PROPOSE"] + /\ decision = [p \in Corr |-> NilValue] + /\ lockedValue = [p \in Corr |-> NilValue] + /\ lockedRound = [p \in Corr |-> NilRound] + /\ validValue = [p \in Corr |-> NilValue] + /\ validRound = [p \in Corr |-> NilRound] + /\ ProduceFaults(msgsPrevote', + SetOfMsgs([type: {"PREVOTE"}, src: Faulty, round: Rounds, id: Values]), + NFaultyPrevotes) + /\ ProduceFaults(msgsPrecommit', + SetOfMsgs([type: {"PRECOMMIT"}, src: Faulty, round: Rounds, id: Values]), + NFaultyPrecommits) + /\ ProduceFaults(msgsPropose', + SetOfMsgs([type: {"PROPOSAL"}, src: Faulty, round: Rounds, + proposal: Values, validRound: Rounds \cup {NilRound}]), + NFaultyProposals) + /\ evidence = EmptyMsgSet + +\* Add faults incrementally +NextWithFaults == + \* either the protocol makes a step + \/ Next + \* or a faulty process sends a message + \//\ UNCHANGED <> + /\ \E p \in Faulty: + \E r \in Rounds: + \//\ UNCHANGED <> + /\ \E proposal \in ValidValues \union {NilValue}: + \E vr \in RoundsOrNil: + BroadcastProposal(p, r, proposal, vr) + \//\ UNCHANGED <> + /\ \E id \in ValidValues \union {NilValue}: + BroadcastPrevote(p, r, id) + \//\ UNCHANGED <> + /\ \E id \in ValidValues \union {NilValue}: + BroadcastPrecommit(p, r, id) + +(******************************** PROPERTIES ***************************************) +\* simple reachability properties to see that the spec is progressing +NoPrevote == \A p \in Corr: step[p] /= "PREVOTE" + +NoPrecommit == \A p \in Corr: step[p] /= "PRECOMMIT" + +NoValidPrecommit == + \A r \in Rounds: + \A m \in msgsPrecommit[r]: + m.id = NilValue \/ m.src \in Faulty + +NoHigherRounds == \A p \in Corr: round[p] < 1 + +NoDecision == \A p \in Corr: decision[p] = NilValue + +============================================================================= + diff --git a/spec/light-client/accountability/TendermintAccInv_004_draft.tla b/spec/light-client/accountability/TendermintAccInv_004_draft.tla new file mode 100644 index 000000000..5dd15396d --- /dev/null +++ b/spec/light-client/accountability/TendermintAccInv_004_draft.tla @@ -0,0 +1,370 @@ +------------------- MODULE TendermintAccInv_004_draft -------------------------- +(* + An inductive invariant for TendermintAcc3, which capture the forked + and non-forked cases. + + * Version 3. Modular and parameterized definitions. + * Version 2. Bugfixes in the spec and an inductive invariant. + + Igor Konnov, 2020. + *) + +EXTENDS TendermintAcc_004_draft + +(************************** TYPE INVARIANT ***********************************) +(* first, we define the sets of all potential messages *) +AllProposals == + SetOfMsgs([type: {"PROPOSAL"}, + src: AllProcs, + round: Rounds, + proposal: ValuesOrNil, + validRound: RoundsOrNil]) + +AllPrevotes == + SetOfMsgs([type: {"PREVOTE"}, + src: AllProcs, + round: Rounds, + id: ValuesOrNil]) + +AllPrecommits == + SetOfMsgs([type: {"PRECOMMIT"}, + src: AllProcs, + round: Rounds, + id: ValuesOrNil]) + +(* the standard type invariant -- importantly, it is inductive *) +TypeOK == + /\ round \in [Corr -> Rounds] + /\ step \in [Corr -> { "PROPOSE", "PREVOTE", "PRECOMMIT", "DECIDED" }] + /\ decision \in [Corr -> ValidValues \union {NilValue}] + /\ lockedValue \in [Corr -> ValidValues \union {NilValue}] + /\ lockedRound \in [Corr -> RoundsOrNil] + /\ validValue \in [Corr -> ValidValues \union {NilValue}] + /\ validRound \in [Corr -> RoundsOrNil] + /\ msgsPropose \in [Rounds -> SUBSET AllProposals] + /\ BenignRoundsInMessages(msgsPropose) + /\ msgsPrevote \in [Rounds -> SUBSET AllPrevotes] + /\ BenignRoundsInMessages(msgsPrevote) + /\ msgsPrecommit \in [Rounds -> SUBSET AllPrecommits] + /\ BenignRoundsInMessages(msgsPrecommit) + /\ evidence \in SUBSET (AllProposals \union AllPrevotes \union AllPrecommits) + /\ action \in { + "Init", + "InsertProposal", + "UponProposalInPropose", + "UponProposalInProposeAndPrevote", + "UponQuorumOfPrevotesAny", + "UponProposalInPrevoteOrCommitAndPrevote", + "UponQuorumOfPrecommitsAny", + "UponProposalInPrecommitNoDecision", + "OnTimeoutPropose", + "OnQuorumOfNilPrevotes", + "OnRoundCatchup" + } + +(************************** INDUCTIVE INVARIANT *******************************) +EvidenceContainsMessages == + \* evidence contains only the messages from: + \* msgsPropose, msgsPrevote, and msgsPrecommit + \A m \in evidence: + LET r == m.round + t == m.type + IN + CASE t = "PROPOSAL" -> m \in msgsPropose[r] + [] t = "PREVOTE" -> m \in msgsPrevote[r] + [] OTHER -> m \in msgsPrecommit[r] + +NoFutureMessagesForLargerRounds(p) == + \* a correct process does not send messages for the future rounds + \A r \in { rr \in Rounds: rr > round[p] }: + /\ \A m \in msgsPropose[r]: m.src /= p + /\ \A m \in msgsPrevote[r]: m.src /= p + /\ \A m \in msgsPrecommit[r]: m.src /= p + +NoFutureMessagesForCurrentRound(p) == + \* a correct process does not send messages in the future + LET r == round[p] IN + /\ Proposer[r] = p \/ \A m \in msgsPropose[r]: m.src /= p + /\ \/ step[p] \in {"PREVOTE", "PRECOMMIT", "DECIDED"} + \/ \A m \in msgsPrevote[r]: m.src /= p + /\ \/ step[p] \in {"PRECOMMIT", "DECIDED"} + \/ \A m \in msgsPrecommit[r]: m.src /= p + +\* the correct processes never send future messages +AllNoFutureMessagesSent == + \A p \in Corr: + /\ NoFutureMessagesForCurrentRound(p) + /\ NoFutureMessagesForLargerRounds(p) + +\* a correct process in the PREVOTE state has sent a PREVOTE message +IfInPrevoteThenSentPrevote(p) == + step[p] = "PREVOTE" => + \E m \in msgsPrevote[round[p]]: + /\ m.id \in ValidValues \cup { NilValue } + /\ m.src = p + +AllIfInPrevoteThenSentPrevote == + \A p \in Corr: IfInPrevoteThenSentPrevote(p) + +\* a correct process in the PRECOMMIT state has sent a PRECOMMIT message +IfInPrecommitThenSentPrecommit(p) == + step[p] = "PRECOMMIT" => + \E m \in msgsPrecommit[round[p]]: + /\ m.id \in ValidValues \cup { NilValue } + /\ m.src = p + +AllIfInPrecommitThenSentPrecommit == + \A p \in Corr: IfInPrecommitThenSentPrecommit(p) + +\* a process in the PRECOMMIT state has sent a PRECOMMIT message +IfInDecidedThenValidDecision(p) == + step[p] = "DECIDED" <=> decision[p] \in ValidValues + +AllIfInDecidedThenValidDecision == + \A p \in Corr: IfInDecidedThenValidDecision(p) + +\* a decided process should have received a proposal on its decision +IfInDecidedThenReceivedProposal(p) == + step[p] = "DECIDED" => + \E r \in Rounds: \* r is not necessarily round[p] + /\ \E m \in msgsPropose[r] \intersect evidence: + /\ m.src = Proposer[r] + /\ m.proposal = decision[p] + \* not inductive: /\ m.src \in Corr => (m.validRound <= r) + +AllIfInDecidedThenReceivedProposal == + \A p \in Corr: + IfInDecidedThenReceivedProposal(p) + +\* a decided process has received two-thirds of precommit messages +IfInDecidedThenReceivedTwoThirds(p) == + step[p] = "DECIDED" => + \E r \in Rounds: + LET PV == + { m \in msgsPrecommit[r] \intersect evidence: m.id = decision[p] } + IN + Cardinality(PV) >= THRESHOLD2 + +AllIfInDecidedThenReceivedTwoThirds == + \A p \in Corr: + IfInDecidedThenReceivedTwoThirds(p) + +\* for a round r, there is proposal by the round proposer for a valid round vr +ProposalInRound(r, proposedVal, vr) == + \E m \in msgsPropose[r]: + /\ m.src = Proposer[r] + /\ m.proposal = proposedVal + /\ m.validRound = vr + +TwoThirdsPrevotes(vr, v) == + LET PV == { mm \in msgsPrevote[vr] \intersect evidence: mm.id = v } IN + Cardinality(PV) >= THRESHOLD2 + +\* if a process sends a PREVOTE, then there are three possibilities: +\* 1) the process is faulty, 2) the PREVOTE cotains Nil, +\* 3) there is a proposal in an earlier (valid) round and two thirds of PREVOTES +IfSentPrevoteThenReceivedProposalOrTwoThirds(r) == + \A mpv \in msgsPrevote[r]: + \/ mpv.src \in Faulty + \* lockedRound and lockedValue is beyond my comprehension + \/ mpv.id = NilValue + \//\ mpv.src \in Corr + /\ mpv.id /= NilValue + /\ \/ ProposalInRound(r, mpv.id, NilRound) + \/ \E vr \in { rr \in Rounds: rr < r }: + /\ ProposalInRound(r, mpv.id, vr) + /\ TwoThirdsPrevotes(vr, mpv.id) + +AllIfSentPrevoteThenReceivedProposalOrTwoThirds == + \A r \in Rounds: + IfSentPrevoteThenReceivedProposalOrTwoThirds(r) + +\* if a correct process has sent a PRECOMMIT, then there are two thirds, +\* either on a valid value, or a nil value +IfSentPrecommitThenReceivedTwoThirds == + \A r \in Rounds: + \A mpc \in msgsPrecommit[r]: + mpc.src \in Corr => + \/ /\ mpc.id \in ValidValues + /\ LET PV == + { m \in msgsPrevote[r] \intersect evidence: m.id = mpc.id } + IN + Cardinality(PV) >= THRESHOLD2 + \/ /\ mpc.id = NilValue + /\ Cardinality(msgsPrevote[r]) >= THRESHOLD2 + +\* if a correct process has sent a precommit message in a round, it should +\* have sent a prevote +IfSentPrecommitThenSentPrevote == + \A r \in Rounds: + \A mpc \in msgsPrecommit[r]: + mpc.src \in Corr => + \E m \in msgsPrevote[r]: + m.src = mpc.src + +\* there is a locked round if a only if there is a locked value +LockedRoundIffLockedValue(p) == + (lockedRound[p] = NilRound) <=> (lockedValue[p] = NilValue) + +AllLockedRoundIffLockedValue == + \A p \in Corr: + LockedRoundIffLockedValue(p) + +\* when a process locked a round, it must have sent a precommit on the locked value. +IfLockedRoundThenSentCommit(p) == + lockedRound[p] /= NilRound + => \E r \in { rr \in Rounds: rr <= round[p] }: + \E m \in msgsPrecommit[r]: + m.src = p /\ m.id = lockedValue[p] + +AllIfLockedRoundThenSentCommit == + \A p \in Corr: + IfLockedRoundThenSentCommit(p) + +\* a process always locks the latest round, for which it has sent a PRECOMMIT +LatestPrecommitHasLockedRound(p) == + LET pPrecommits == + {mm \in UNION { msgsPrecommit[r]: r \in Rounds }: mm.src = p /\ mm.id /= NilValue } + IN + pPrecommits /= {} <: {MT} + => LET latest == + CHOOSE m \in pPrecommits: + \A m2 \in pPrecommits: + m2.round <= m.round + IN + /\ lockedRound[p] = latest.round + /\ lockedValue[p] = latest.id + +AllLatestPrecommitHasLockedRound == + \A p \in Corr: + LatestPrecommitHasLockedRound(p) + +\* Every correct process sends only one value or NilValue. +\* This test has quantifier alternation -- a threat to all decision procedures. +\* Luckily, the sets Corr and ValidValues are small. +NoEquivocationByCorrect(r, msgs) == + \A p \in Corr: + \E v \in ValidValues \union {NilValue}: + \A m \in msgs[r]: + \/ m.src /= p + \/ m.id = v + +\* a proposer nevers sends two values +ProposalsByProposer(r, msgs) == + \* if the proposer is not faulty, it sends only one value + \E v \in ValidValues: + \A m \in msgs[r]: + \/ m.src \in Faulty + \/ m.src = Proposer[r] /\ m.proposal = v + +AllNoEquivocationByCorrect == + \A r \in Rounds: + /\ ProposalsByProposer(r, msgsPropose) + /\ NoEquivocationByCorrect(r, msgsPrevote) + /\ NoEquivocationByCorrect(r, msgsPrecommit) + +\* construct the set of the message senders +Senders(M) == { m.src: m \in M } + +\* The final piece by Josef Widder: +\* if T + 1 processes precommit on the same value in a round, +\* then in the future rounds there are less than 2T + 1 prevotes for another value +PrecommitsLockValue == + \A r \in Rounds: + \A v \in ValidValues \union {NilValue}: + \/ LET Precommits == {m \in msgsPrecommit[r]: m.id = v} + IN + Cardinality(Senders(Precommits)) < THRESHOLD1 + \/ \A fr \in { rr \in Rounds: rr > r }: \* future rounds + \A w \in (ValuesOrNil) \ {v}: + LET Prevotes == {m \in msgsPrevote[fr]: m.id = w} + IN + Cardinality(Senders(Prevotes)) < THRESHOLD2 + +\* a combination of all lemmas +Inv == + /\ EvidenceContainsMessages + /\ AllNoFutureMessagesSent + /\ AllIfInPrevoteThenSentPrevote + /\ AllIfInPrecommitThenSentPrecommit + /\ AllIfInDecidedThenReceivedProposal + /\ AllIfInDecidedThenReceivedTwoThirds + /\ AllIfInDecidedThenValidDecision + /\ AllLockedRoundIffLockedValue + /\ AllIfLockedRoundThenSentCommit + /\ AllLatestPrecommitHasLockedRound + /\ AllIfSentPrevoteThenReceivedProposalOrTwoThirds + /\ IfSentPrecommitThenSentPrevote + /\ IfSentPrecommitThenReceivedTwoThirds + /\ AllNoEquivocationByCorrect + /\ PrecommitsLockValue + +\* this is the inductive invariant we like to check +TypedInv == TypeOK /\ Inv + +\* UNUSED FOR SAFETY +ValidRoundNotSmallerThanLockedRound(p) == + validRound[p] >= lockedRound[p] + +\* UNUSED FOR SAFETY +ValidRoundIffValidValue(p) == + (validRound[p] = NilRound) <=> (validValue[p] = NilValue) + +\* UNUSED FOR SAFETY +AllValidRoundIffValidValue == + \A p \in Corr: ValidRoundIffValidValue(p) + +\* if validRound is defined, then there are two-thirds of PREVOTEs +IfValidRoundThenTwoThirds(p) == + \/ validRound[p] = NilRound + \/ LET PV == { m \in msgsPrevote[validRound[p]]: m.id = validValue[p] } IN + Cardinality(PV) >= THRESHOLD2 + +\* UNUSED FOR SAFETY +AllIfValidRoundThenTwoThirds == + \A p \in Corr: IfValidRoundThenTwoThirds(p) + +\* a valid round can be only set to a valid value that was proposed earlier +IfValidRoundThenProposal(p) == + \/ validRound[p] = NilRound + \/ \E m \in msgsPropose[validRound[p]]: + m.proposal = validValue[p] + +\* UNUSED FOR SAFETY +AllIfValidRoundThenProposal == + \A p \in Corr: IfValidRoundThenProposal(p) + +(******************************** THEOREMS ***************************************) +(* Under this condition, the faulty processes can decide alone *) +FaultyQuorum == Cardinality(Faulty) >= THRESHOLD2 + +(* The standard condition of the Tendermint security model *) +LessThanThirdFaulty == N > 3 * T /\ Cardinality(Faulty) <= T + +(* + TypedInv is an inductive invariant, provided that there is no faulty quorum. + We run Apalache to prove this theorem only for fixed instances of 4 to 10 processes. + (We run Apalache manually, as it does not parse theorem statements at the moment.) + To get a parameterized argument, one has to use a theorem prover, e.g., TLAPS. + *) +THEOREM TypedInvIsInductive == + \/ FaultyQuorum \* if there are 2 * T + 1 faulty processes, we give up + \//\ Init => TypedInv + /\ TypedInv /\ [Next]_vars => TypedInv' + +(* + There should be no fork, when there are less than 1/3 faulty processes. + *) +THEOREM AgreementWhenLessThanThirdFaulty == + LessThanThirdFaulty /\ TypedInv => Agreement + +(* + In a more general case, when there are less than 2/3 faulty processes, + there is either Agreement (no fork), or two scenarios exist: + equivocation by Faulty, or amnesia by Faulty. + *) +THEOREM AgreementOrFork == + ~FaultyQuorum /\ TypedInv => Accountability + +============================================================================= + diff --git a/spec/light-client/accountability/TendermintAccTrace_004_draft.tla b/spec/light-client/accountability/TendermintAccTrace_004_draft.tla new file mode 100644 index 000000000..436c2275a --- /dev/null +++ b/spec/light-client/accountability/TendermintAccTrace_004_draft.tla @@ -0,0 +1,33 @@ +------------------ MODULE TendermintAccTrace_004_draft ------------------------- +(* + When Apalache is running too slow and we have an idea of a counterexample, + we use this module to restrict the behaviors only to certain actions. + Once the whole trace is replayed, the system deadlocks. + + Version 1. + + Igor Konnov, 2020. + *) + +EXTENDS Sequences, Apalache, TendermintAcc_004_draft + +\* a sequence of action names that should appear in the given order, +\* excluding "Init" +CONSTANT Trace + +VARIABLE toReplay + +TraceInit == + /\ toReplay = Trace + /\ action' := "Init" + /\ Init + +TraceNext == + /\ Len(toReplay) > 0 + /\ toReplay' = Tail(toReplay) + \* Here is the trick. We restrict the action to the expected one, + \* so the other actions will be pruned + /\ action' := Head(toReplay) + /\ Next + +================================================================================ diff --git a/spec/light-client/accountability/TendermintAcc_004_draft.tla b/spec/light-client/accountability/TendermintAcc_004_draft.tla new file mode 100644 index 000000000..9d3a543d4 --- /dev/null +++ b/spec/light-client/accountability/TendermintAcc_004_draft.tla @@ -0,0 +1,474 @@ +-------------------- MODULE TendermintAcc_004_draft --------------------------- +(* + A TLA+ specification of a simplified Tendermint consensus, tuned for + fork accountability. The simplifications are as follows: + + - the protocol runs for one height, that is, it is one-shot consensus + + - this specification focuses on safety, so timeouts are modelled + with non-determinism + + - the proposer function is non-determinstic, no fairness is assumed + + - the messages by the faulty processes are injected right in the initial states + + - every process has the voting power of 1 + + - hashes are modelled as identity + + Having the above assumptions in mind, the specification follows the pseudo-code + of the Tendermint paper: https://arxiv.org/abs/1807.04938 + + Byzantine processes can demonstrate arbitrary behavior, including + no communication. We show that if agreement is violated, then the Byzantine + processes demonstrate one of the two behaviours: + + - Equivocation: a Byzantine process may send two different values + in the same round. + + - Amnesia: a Byzantine process may lock a value without unlocking + the previous value that it has locked in the past. + + * Version 4. Remove defective processes, fix bugs, collect global evidence. + * Version 3. Modular and parameterized definitions. + * Version 2. Bugfixes in the spec and an inductive invariant. + * Version 1. A preliminary specification. + + Zarko Milosevic, Igor Konnov, Informal Systems, 2019-2020. + *) + +EXTENDS Integers, FiniteSets + +(********************* PROTOCOL PARAMETERS **********************************) +CONSTANTS + Corr, \* the set of correct processes + Faulty, \* the set of Byzantine processes, may be empty + N, \* the total number of processes: correct, defective, and Byzantine + T, \* an upper bound on the number of Byzantine processes + ValidValues, \* the set of valid values, proposed both by correct and faulty + InvalidValues, \* the set of invalid values, never proposed by the correct ones + MaxRound, \* the maximal round number + Proposer \* the proposer function from 0..NRounds to 1..N + +ASSUME(N = Cardinality(Corr \union Faulty)) + +(*************************** DEFINITIONS ************************************) +AllProcs == Corr \union Faulty \* the set of all processes +Rounds == 0..MaxRound \* the set of potential rounds +NilRound == -1 \* a special value to denote a nil round, outside of Rounds +RoundsOrNil == Rounds \union {NilRound} +Values == ValidValues \union InvalidValues \* the set of all values +NilValue == "None" \* a special value for a nil round, outside of Values +ValuesOrNil == Values \union {NilValue} + +\* a value hash is modeled as identity +Id(v) == v + +\* The validity predicate +IsValid(v) == v \in ValidValues + +\* the two thresholds that are used in the algorithm +THRESHOLD1 == T + 1 \* at least one process is not faulty +THRESHOLD2 == 2 * T + 1 \* a quorum when having N > 3 * T + +(********************* TYPE ANNOTATIONS FOR APALACHE **************************) +\* the operator for type annotations +a <: b == a + +\* the type of message records +MT == [type |-> STRING, src |-> STRING, round |-> Int, + proposal |-> STRING, validRound |-> Int, id |-> STRING] + +\* a type annotation for a message +AsMsg(m) == m <: MT +\* a type annotation for a set of messages +SetOfMsgs(S) == S <: {MT} +\* a type annotation for an empty set of messages +EmptyMsgSet == SetOfMsgs({}) + +(********************* PROTOCOL STATE VARIABLES ******************************) +VARIABLES + round, \* a process round number: Corr -> Rounds + step, \* a process step: Corr -> { "PROPOSE", "PREVOTE", "PRECOMMIT", "DECIDED" } + decision, \* process decision: Corr -> ValuesOrNil + lockedValue, \* a locked value: Corr -> ValuesOrNil + lockedRound, \* a locked round: Corr -> RoundsOrNil + validValue, \* a valid value: Corr -> ValuesOrNil + validRound \* a valid round: Corr -> RoundsOrNil + +\* book-keeping variables +VARIABLES + msgsPropose, \* PROPOSE messages broadcast in the system, Rounds -> Messages + msgsPrevote, \* PREVOTE messages broadcast in the system, Rounds -> Messages + msgsPrecommit, \* PRECOMMIT messages broadcast in the system, Rounds -> Messages + evidence, \* the messages that were used by the correct processes to make transitions + action \* we use this variable to see which action was taken + +(* to see a type invariant, check TendermintAccInv3 *) + +\* a handy definition used in UNCHANGED +vars == <> + +(********************* PROTOCOL INITIALIZATION ******************************) +FaultyProposals(r) == + SetOfMsgs([type: {"PROPOSAL"}, src: Faulty, + round: {r}, proposal: Values, validRound: RoundsOrNil]) + +AllFaultyProposals == + SetOfMsgs([type: {"PROPOSAL"}, src: Faulty, + round: Rounds, proposal: Values, validRound: RoundsOrNil]) + +FaultyPrevotes(r) == + SetOfMsgs([type: {"PREVOTE"}, src: Faulty, round: {r}, id: Values]) + +AllFaultyPrevotes == + SetOfMsgs([type: {"PREVOTE"}, src: Faulty, round: Rounds, id: Values]) + +FaultyPrecommits(r) == + SetOfMsgs([type: {"PRECOMMIT"}, src: Faulty, round: {r}, id: Values]) + +AllFaultyPrecommits == + SetOfMsgs([type: {"PRECOMMIT"}, src: Faulty, round: Rounds, id: Values]) + +BenignRoundsInMessages(msgfun) == + \* the message function never contains a message for a wrong round + \A r \in Rounds: + \A m \in msgfun[r]: + r = m.round + +\* The initial states of the protocol. Some faults can be in the system already. +Init == + /\ round = [p \in Corr |-> 0] + /\ step = [p \in Corr |-> "PROPOSE"] + /\ decision = [p \in Corr |-> NilValue] + /\ lockedValue = [p \in Corr |-> NilValue] + /\ lockedRound = [p \in Corr |-> NilRound] + /\ validValue = [p \in Corr |-> NilValue] + /\ validRound = [p \in Corr |-> NilRound] + /\ msgsPropose \in [Rounds -> SUBSET AllFaultyProposals] + /\ msgsPrevote \in [Rounds -> SUBSET AllFaultyPrevotes] + /\ msgsPrecommit \in [Rounds -> SUBSET AllFaultyPrecommits] + /\ BenignRoundsInMessages(msgsPropose) + /\ BenignRoundsInMessages(msgsPrevote) + /\ BenignRoundsInMessages(msgsPrecommit) + /\ evidence = EmptyMsgSet + /\ action' = "Init" + +(************************ MESSAGE PASSING ********************************) +BroadcastProposal(pSrc, pRound, pProposal, pValidRound) == + LET newMsg == + AsMsg([type |-> "PROPOSAL", src |-> pSrc, round |-> pRound, + proposal |-> pProposal, validRound |-> pValidRound]) + IN + msgsPropose' = [msgsPropose EXCEPT ![pRound] = msgsPropose[pRound] \union {newMsg}] + +BroadcastPrevote(pSrc, pRound, pId) == + LET newMsg == AsMsg([type |-> "PREVOTE", + src |-> pSrc, round |-> pRound, id |-> pId]) + IN + msgsPrevote' = [msgsPrevote EXCEPT ![pRound] = msgsPrevote[pRound] \union {newMsg}] + +BroadcastPrecommit(pSrc, pRound, pId) == + LET newMsg == AsMsg([type |-> "PRECOMMIT", + src |-> pSrc, round |-> pRound, id |-> pId]) + IN + msgsPrecommit' = [msgsPrecommit EXCEPT ![pRound] = msgsPrecommit[pRound] \union {newMsg}] + + +(********************* PROTOCOL TRANSITIONS ******************************) +\* lines 12-13 +StartRound(p, r) == + /\ step[p] /= "DECIDED" \* a decided process does not participate in consensus + /\ round' = [round EXCEPT ![p] = r] + /\ step' = [step EXCEPT ![p] = "PROPOSE"] + +\* lines 14-19, a proposal may be sent later +InsertProposal(p) == + LET r == round[p] IN + /\ p = Proposer[r] + /\ step[p] = "PROPOSE" + \* if the proposer is sending a proposal, then there are no other proposals + \* by the correct processes for the same round + /\ \A m \in msgsPropose[r]: m.src /= p + /\ \E v \in ValidValues: + LET proposal == IF validValue[p] /= NilValue THEN validValue[p] ELSE v IN + BroadcastProposal(p, round[p], proposal, validRound[p]) + /\ UNCHANGED <> + /\ action' = "InsertProposal" + +\* lines 22-27 +UponProposalInPropose(p) == + \E v \in Values: + /\ step[p] = "PROPOSE" (* line 22 *) + /\ LET msg == + AsMsg([type |-> "PROPOSAL", src |-> Proposer[round[p]], + round |-> round[p], proposal |-> v, validRound |-> NilRound]) IN + /\ msg \in msgsPropose[round[p]] \* line 22 + /\ evidence' = {msg} \union evidence + /\ LET mid == (* line 23 *) + IF IsValid(v) /\ (lockedRound[p] = NilRound \/ lockedValue[p] = v) + THEN Id(v) + ELSE NilValue + IN + BroadcastPrevote(p, round[p], mid) \* lines 24-26 + /\ step' = [step EXCEPT ![p] = "PREVOTE"] + /\ UNCHANGED <> + /\ action' = "UponProposalInPropose" + +\* lines 28-33 +UponProposalInProposeAndPrevote(p) == + \E v \in Values, vr \in Rounds: + /\ step[p] = "PROPOSE" /\ 0 <= vr /\ vr < round[p] \* line 28, the while part + /\ LET msg == + AsMsg([type |-> "PROPOSAL", src |-> Proposer[round[p]], + round |-> round[p], proposal |-> v, validRound |-> vr]) + IN + /\ msg \in msgsPropose[round[p]] \* line 28 + /\ LET PV == { m \in msgsPrevote[vr]: m.id = Id(v) } IN + /\ Cardinality(PV) >= THRESHOLD2 \* line 28 + /\ evidence' = PV \union {msg} \union evidence + /\ LET mid == (* line 29 *) + IF IsValid(v) /\ (lockedRound[p] <= vr \/ lockedValue[p] = v) + THEN Id(v) + ELSE NilValue + IN + BroadcastPrevote(p, round[p], mid) \* lines 24-26 + /\ step' = [step EXCEPT ![p] = "PREVOTE"] + /\ UNCHANGED <> + /\ action' = "UponProposalInProposeAndPrevote" + + \* lines 34-35 + lines 61-64 (onTimeoutPrevote) +UponQuorumOfPrevotesAny(p) == + /\ step[p] = "PREVOTE" \* line 34 and 61 + /\ \E MyEvidence \in SUBSET msgsPrevote[round[p]]: + \* find the unique voters in the evidence + LET Voters == { m.src: m \in MyEvidence } IN + \* compare the number of the unique voters against the threshold + /\ Cardinality(Voters) >= THRESHOLD2 \* line 34 + /\ evidence' = MyEvidence \union evidence + /\ BroadcastPrecommit(p, round[p], NilValue) + /\ step' = [step EXCEPT ![p] = "PRECOMMIT"] + /\ UNCHANGED <> + /\ action' = "UponQuorumOfPrevotesAny" + +\* lines 36-46 +UponProposalInPrevoteOrCommitAndPrevote(p) == + \E v \in ValidValues, vr \in RoundsOrNil: + /\ step[p] \in {"PREVOTE", "PRECOMMIT"} \* line 36 + /\ LET msg == + AsMsg([type |-> "PROPOSAL", src |-> Proposer[round[p]], + round |-> round[p], proposal |-> v, validRound |-> vr]) IN + /\ msg \in msgsPropose[round[p]] \* line 36 + /\ LET PV == { m \in msgsPrevote[round[p]]: m.id = Id(v) } IN + /\ Cardinality(PV) >= THRESHOLD2 \* line 36 + /\ evidence' = PV \union {msg} \union evidence + /\ IF step[p] = "PREVOTE" + THEN \* lines 38-41: + /\ lockedValue' = [lockedValue EXCEPT ![p] = v] + /\ lockedRound' = [lockedRound EXCEPT ![p] = round[p]] + /\ BroadcastPrecommit(p, round[p], Id(v)) + /\ step' = [step EXCEPT ![p] = "PRECOMMIT"] + ELSE + UNCHANGED <> + \* lines 42-43 + /\ validValue' = [validValue EXCEPT ![p] = v] + /\ validRound' = [validRound EXCEPT ![p] = round[p]] + /\ UNCHANGED <> + /\ action' = "UponProposalInPrevoteOrCommitAndPrevote" + +\* lines 47-48 + 65-67 (onTimeoutPrecommit) +UponQuorumOfPrecommitsAny(p) == + /\ \E MyEvidence \in SUBSET msgsPrecommit[round[p]]: + \* find the unique committers in the evidence + LET Committers == { m.src: m \in MyEvidence } IN + \* compare the number of the unique committers against the threshold + /\ Cardinality(Committers) >= THRESHOLD2 \* line 47 + /\ evidence' = MyEvidence \union evidence + /\ round[p] + 1 \in Rounds + /\ StartRound(p, round[p] + 1) + /\ UNCHANGED <> + /\ action' = "UponQuorumOfPrecommitsAny" + +\* lines 49-54 +UponProposalInPrecommitNoDecision(p) == + /\ decision[p] = NilValue \* line 49 + /\ \E v \in ValidValues (* line 50*) , r \in Rounds, vr \in RoundsOrNil: + /\ LET msg == AsMsg([type |-> "PROPOSAL", src |-> Proposer[r], + round |-> r, proposal |-> v, validRound |-> vr]) IN + /\ msg \in msgsPropose[r] \* line 49 + /\ LET PV == { m \in msgsPrecommit[r]: m.id = Id(v) } IN + /\ Cardinality(PV) >= THRESHOLD2 \* line 49 + /\ evidence' = PV \union {msg} \union evidence + /\ decision' = [decision EXCEPT ![p] = v] \* update the decision, line 51 + \* The original algorithm does not have 'DECIDED', but it increments the height. + \* We introduced 'DECIDED' here to prevent the process from changing its decision. + /\ step' = [step EXCEPT ![p] = "DECIDED"] + /\ UNCHANGED <> + /\ action' = "UponProposalInPrecommitNoDecision" + +\* the actions below are not essential for safety, but added for completeness + +\* lines 20-21 + 57-60 +OnTimeoutPropose(p) == + /\ step[p] = "PROPOSE" + /\ p /= Proposer[round[p]] + /\ BroadcastPrevote(p, round[p], NilValue) + /\ step' = [step EXCEPT ![p] = "PREVOTE"] + /\ UNCHANGED <> + /\ action' = "OnTimeoutPropose" + +\* lines 44-46 +OnQuorumOfNilPrevotes(p) == + /\ step[p] = "PREVOTE" + /\ LET PV == { m \in msgsPrevote[round[p]]: m.id = Id(NilValue) } IN + /\ Cardinality(PV) >= THRESHOLD2 \* line 36 + /\ evidence' = PV \union evidence + /\ BroadcastPrecommit(p, round[p], Id(NilValue)) + /\ step' = [step EXCEPT ![p] = "PRECOMMIT"] + /\ UNCHANGED <> + /\ action' = "OnQuorumOfNilPrevotes" + +\* lines 55-56 +OnRoundCatchup(p) == + \E r \in {rr \in Rounds: rr > round[p]}: + LET RoundMsgs == msgsPropose[r] \union msgsPrevote[r] \union msgsPrecommit[r] IN + \E MyEvidence \in SUBSET RoundMsgs: + LET Faster == { m.src: m \in MyEvidence } IN + /\ Cardinality(Faster) >= THRESHOLD1 + /\ evidence' = MyEvidence \union evidence + /\ StartRound(p, r) + /\ UNCHANGED <> + /\ action' = "OnRoundCatchup" + +(* + * A system transition. In this specificatiom, the system may eventually deadlock, + * e.g., when all processes decide. This is expected behavior, as we focus on safety. + *) +Next == + \E p \in Corr: + \/ InsertProposal(p) + \/ UponProposalInPropose(p) + \/ UponProposalInProposeAndPrevote(p) + \/ UponQuorumOfPrevotesAny(p) + \/ UponProposalInPrevoteOrCommitAndPrevote(p) + \/ UponQuorumOfPrecommitsAny(p) + \/ UponProposalInPrecommitNoDecision(p) + \* the actions below are not essential for safety, but added for completeness + \/ OnTimeoutPropose(p) + \/ OnQuorumOfNilPrevotes(p) + \/ OnRoundCatchup(p) + + +(**************************** FORK SCENARIOS ***************************) + +\* equivocation by a process p +EquivocationBy(p) == + \E m1, m2 \in evidence: + /\ m1 /= m2 + /\ m1.src = p + /\ m2.src = p + /\ m1.round = m2.round + /\ m1.type = m2.type + +\* amnesic behavior by a process p +AmnesiaBy(p) == + \E r1, r2 \in Rounds: + /\ r1 < r2 + /\ \E v1, v2 \in ValidValues: + /\ v1 /= v2 + /\ AsMsg([type |-> "PRECOMMIT", src |-> p, + round |-> r1, id |-> Id(v1)]) \in evidence + /\ AsMsg([type |-> "PREVOTE", src |-> p, + round |-> r2, id |-> Id(v2)]) \in evidence + /\ \A r \in { rnd \in Rounds: r1 <= rnd /\ rnd < r2 }: + LET prevotes == + { m \in evidence: + m.type = "PREVOTE" /\ m.round = r /\ m.id = Id(v2) } + IN + Cardinality(prevotes) < THRESHOLD2 + +(******************************** PROPERTIES ***************************************) + +\* the safety property -- agreement +Agreement == + \A p, q \in Corr: + \/ decision[p] = NilValue + \/ decision[q] = NilValue + \/ decision[p] = decision[q] + +\* the protocol validity +Validity == + \A p \in Corr: decision[p] \in ValidValues \union {NilValue} + +(* + The protocol safety. Two cases are possible: + 1. There is no fork, that is, Agreement holds true. + 2. A subset of faulty processes demonstrates equivocation or amnesia. + *) +Accountability == + \/ Agreement + \/ \E Detectable \in SUBSET Faulty: + /\ Cardinality(Detectable) >= THRESHOLD1 + /\ \A p \in Detectable: + EquivocationBy(p) \/ AmnesiaBy(p) + +(****************** FALSE INVARIANTS TO PRODUCE EXAMPLES ***********************) + +\* This property is violated. You can check it to see how amnesic behavior +\* appears in the evidence variable. +NoAmnesia == + \A p \in Faulty: ~AmnesiaBy(p) + +\* This property is violated. You can check it to see an example of equivocation. +NoEquivocation == + \A p \in Faulty: ~EquivocationBy(p) + +\* This property is violated. You can check it to see an example of agreement. +\* It is not exactly ~Agreement, as we do not want to see the states where +\* decision[p] = NilValue +NoAgreement == + \A p, q \in Corr: + (p /= q /\ decision[p] /= NilValue /\ decision[q] /= NilValue) + => decision[p] /= decision[q] + +\* Either agreement holds, or the faulty processes indeed demonstrate amnesia. +\* This property is violated. A counterexample should demonstrate equivocation. +AgreementOrAmnesia == + Agreement \/ (\A p \in Faulty: AmnesiaBy(p)) + +\* We expect this property to be violated. It shows us a protocol run, +\* where one faulty process demonstrates amnesia without equivocation. +\* However, the absence of amnesia +\* is a tough constraint for Apalache. It has not reported a counterexample +\* for n=4,f=2, length <= 5. +ShowMeAmnesiaWithoutEquivocation == + (~Agreement /\ \E p \in Faulty: ~EquivocationBy(p)) + => \A p \in Faulty: ~AmnesiaBy(p) + +\* This property is violated on n=4,f=2, length=4 in less than 10 min. +\* Two faulty processes may demonstrate amnesia without equivocation. +AmnesiaImpliesEquivocation == + (\E p \in Faulty: AmnesiaBy(p)) => (\E q \in Faulty: EquivocationBy(q)) + +(* + This property is violated. You can check it to see that all correct processes + may reach MaxRound without making a decision. + *) +NeverUndecidedInMaxRound == + LET AllInMax == \A p \in Corr: round[p] = MaxRound + AllDecided == \A p \in Corr: decision[p] /= NilValue + IN + AllInMax => AllDecided + +============================================================================= + diff --git a/spec/light-client/accountability/results/001indinv-apalache-mem-log.svg b/spec/light-client/accountability/results/001indinv-apalache-mem-log.svg new file mode 100644 index 000000000..5821418da --- /dev/null +++ b/spec/light-client/accountability/results/001indinv-apalache-mem-log.svg @@ -0,0 +1,1063 @@ + + + + + + + + + 2020-12-11T20:07:39.617177 + image/svg+xml + + + Matplotlib v3.3.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/light-client/accountability/results/001indinv-apalache-mem.svg b/spec/light-client/accountability/results/001indinv-apalache-mem.svg new file mode 100644 index 000000000..dc7213eae --- /dev/null +++ b/spec/light-client/accountability/results/001indinv-apalache-mem.svg @@ -0,0 +1,1141 @@ + + + + + + + + + 2020-12-11T20:07:40.321995 + image/svg+xml + + + Matplotlib v3.3.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/light-client/accountability/results/001indinv-apalache-ncells.svg b/spec/light-client/accountability/results/001indinv-apalache-ncells.svg new file mode 100644 index 000000000..20c49f4f1 --- /dev/null +++ b/spec/light-client/accountability/results/001indinv-apalache-ncells.svg @@ -0,0 +1,1015 @@ + + + + + + + + + 2020-12-11T20:07:40.804886 + image/svg+xml + + + Matplotlib v3.3.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/light-client/accountability/results/001indinv-apalache-nclauses.svg b/spec/light-client/accountability/results/001indinv-apalache-nclauses.svg new file mode 100644 index 000000000..86d19143b --- /dev/null +++ b/spec/light-client/accountability/results/001indinv-apalache-nclauses.svg @@ -0,0 +1,1133 @@ + + + + + + + + + 2020-12-11T20:07:41.276750 + image/svg+xml + + + Matplotlib v3.3.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/light-client/accountability/results/001indinv-apalache-report.md b/spec/light-client/accountability/results/001indinv-apalache-report.md new file mode 100644 index 000000000..0c14742c5 --- /dev/null +++ b/spec/light-client/accountability/results/001indinv-apalache-report.md @@ -0,0 +1,61 @@ +# Results of 001indinv-apalache + +## 1. Awesome plots + +### 1.1. Time (logarithmic scale) + +![time-log](001indinv-apalache-time-log.svg "Time Log") + +### 1.2. Time (linear) + +![time-log](001indinv-apalache-time.svg "Time Log") + +### 1.3. Memory (logarithmic scale) + +![mem-log](001indinv-apalache-mem-log.svg "Memory Log") + +### 1.4. Memory (linear) + +![mem](001indinv-apalache-mem.svg "Memory Log") + +### 1.5. Number of arena cells (linear) + +![ncells](001indinv-apalache-ncells.svg "Number of arena cells") + +### 1.6. Number of SMT clauses (linear) + +![nclauses](001indinv-apalache-nclauses.svg "Number of SMT clauses") + +## 2. Input parameters + +no | filename | tool | timeout | init | inv | next | args +----|----------------|------------|-----------|------------|------------------|--------|------------------------------ +1 | MC_n4_f1.tla | apalache | 10h | TypedInv | TypedInv | | --length=1 --cinit=ConstInit +2 | MC_n4_f2.tla | apalache | 10h | TypedInv | TypedInv | | --length=1 --cinit=ConstInit +3 | MC_n5_f1.tla | apalache | 10h | TypedInv | TypedInv | | --length=1 --cinit=ConstInit +4 | MC_n5_f2.tla | apalache | 10h | TypedInv | TypedInv | | --length=1 --cinit=ConstInit +5 | MC_n4_f1.tla | apalache | 20h | Init | TypedInv | | --length=0 --cinit=ConstInit +6 | MC_n4_f2.tla | apalache | 20h | Init | TypedInv | | --length=0 --cinit=ConstInit +7 | MC_n5_f1.tla | apalache | 20h | Init | TypedInv | | --length=0 --cinit=ConstInit +8 | MC_n5_f2.tla | apalache | 20h | Init | TypedInv | | --length=0 --cinit=ConstInit +9 | MC_n4_f1.tla | apalache | 20h | TypedInv | Agreement | | --length=0 --cinit=ConstInit +10 | MC_n4_f2.tla | apalache | 20h | TypedInv | Accountability | | --length=0 --cinit=ConstInit +11 | MC_n5_f1.tla | apalache | 20h | TypedInv | Agreement | | --length=0 --cinit=ConstInit +12 | MC_n5_f2.tla | apalache | 20h | TypedInv | Accountability | | --length=0 --cinit=ConstInit + +## 3. Detailed results: 001indinv-apalache-unstable.csv + +01:no | 02:tool | 03:status | 04:time_sec | 05:depth | 05:mem_kb | 10:ninit_trans | 11:ninit_trans | 12:ncells | 13:nclauses | 14:navg_clause_len +-------|------------|-------------|---------------|------------|-------------|------------------|------------------|-------------|---------------|-------------------- +1 | apalache | NoError | 11m | 1 | 3.0GB | 0 | 0 | 217K | 1.0M | 89 +2 | apalache | NoError | 11m | 1 | 3.0GB | 0 | 0 | 207K | 1.0M | 88 +3 | apalache | NoError | 16m | 1 | 4.0GB | 0 | 0 | 311K | 2.0M | 101 +4 | apalache | NoError | 14m | 1 | 3.0GB | 0 | 0 | 290K | 1.0M | 103 +5 | apalache | NoError | 9s | 0 | 563MB | 0 | 0 | 2.0K | 14K | 42 +6 | apalache | NoError | 10s | 0 | 657MB | 0 | 0 | 2.0K | 28K | 43 +7 | apalache | NoError | 8s | 0 | 635MB | 0 | 0 | 2.0K | 17K | 44 +8 | apalache | NoError | 10s | 0 | 667MB | 0 | 0 | 3.0K | 32K | 45 +9 | apalache | NoError | 5m05s | 0 | 2.0GB | 0 | 0 | 196K | 889K | 108 +10 | apalache | NoError | 8m08s | 0 | 6.0GB | 0 | 0 | 2.0M | 3.0M | 34 +11 | apalache | NoError | 9m09s | 0 | 3.0GB | 0 | 0 | 284K | 1.0M | 128 +12 | apalache | NoError | 14m | 0 | 7.0GB | 0 | 0 | 4.0M | 5.0M | 38 diff --git a/spec/light-client/accountability/results/001indinv-apalache-time-log.svg b/spec/light-client/accountability/results/001indinv-apalache-time-log.svg new file mode 100644 index 000000000..458d67c6c --- /dev/null +++ b/spec/light-client/accountability/results/001indinv-apalache-time-log.svg @@ -0,0 +1,1134 @@ + + + + + + + + + 2020-12-11T20:07:38.347583 + image/svg+xml + + + Matplotlib v3.3.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/light-client/accountability/results/001indinv-apalache-time.svg b/spec/light-client/accountability/results/001indinv-apalache-time.svg new file mode 100644 index 000000000..a5db5a8b5 --- /dev/null +++ b/spec/light-client/accountability/results/001indinv-apalache-time.svg @@ -0,0 +1,957 @@ + + + + + + + + + 2020-12-11T20:07:39.136767 + image/svg+xml + + + Matplotlib v3.3.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/light-client/accountability/results/001indinv-apalache-unstable.csv b/spec/light-client/accountability/results/001indinv-apalache-unstable.csv new file mode 100644 index 000000000..db1a06093 --- /dev/null +++ b/spec/light-client/accountability/results/001indinv-apalache-unstable.csv @@ -0,0 +1,13 @@ +01:no,02:tool,03:status,04:time_sec,05:depth,05:mem_kb,10:ninit_trans,11:ninit_trans,12:ncells,13:nclauses,14:navg_clause_len +1,apalache,NoError,704,1,3215424,0,0,217385,1305718,89 +2,apalache,NoError,699,1,3195020,0,0,207969,1341979,88 +3,apalache,NoError,1018,1,4277060,0,0,311798,2028544,101 +4,apalache,NoError,889,1,4080012,0,0,290989,1951616,103 +5,apalache,NoError,9,0,577100,0,0,2045,14655,42 +6,apalache,NoError,10,0,673772,0,0,2913,28213,43 +7,apalache,NoError,8,0,651008,0,0,2214,17077,44 +8,apalache,NoError,10,0,683188,0,0,3082,32651,45 +9,apalache,NoError,340,0,3053848,0,0,196943,889859,108 +10,apalache,NoError,517,0,6424536,0,0,2856378,3802779,34 +11,apalache,NoError,587,0,4028516,0,0,284369,1343296,128 +12,apalache,NoError,880,0,7881148,0,0,4382556,5778072,38 diff --git a/spec/light-client/accountability/run.sh b/spec/light-client/accountability/run.sh new file mode 100755 index 000000000..75e57a5f8 --- /dev/null +++ b/spec/light-client/accountability/run.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# +# The script to run all experiments at once + +export SCRIPTS_DIR=~/devl/apalache-tests/scripts +export BUILDS="unstable" +export BENCHMARK=001indinv-apalache +export RUN_SCRIPT=./run-all.sh # alternatively, use ./run-parallel.sh +make -e -f ~/devl/apalache-tests/Makefile.common diff --git a/spec/light-client/assets/light-node-image.png b/spec/light-client/assets/light-node-image.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b93c6e415ebc39bc2313c885faacb35aa7af9a GIT binary patch literal 122270 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ɸa2^db4^;{u=6(Fa8oE-0a}M)8Mf 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 NF6my(J#Y2ke*p{XTJ-<` literal 0 HcmV?d00001 diff --git a/spec/light-client/attacks/Blockchain_003_draft.tla b/spec/light-client/attacks/Blockchain_003_draft.tla new file mode 100644 index 000000000..fb6e6e8e8 --- /dev/null +++ b/spec/light-client/attacks/Blockchain_003_draft.tla @@ -0,0 +1,166 @@ +------------------------ MODULE Blockchain_003_draft ----------------------------- +(* + This is a high-level specification of Tendermint blockchain + that is designed specifically for the light client. + Validators have the voting power of one. If you like to model various + voting powers, introduce multiple copies of the same validator + (do not forget to give them unique names though). + *) +EXTENDS Integers, FiniteSets, Apalache + +Min(a, b) == IF a < b THEN a ELSE b + +CONSTANT + AllNodes, + (* a set of all nodes that can act as validators (correct and faulty) *) + ULTIMATE_HEIGHT, + (* a maximal height that can be ever reached (modelling artifact) *) + TRUSTING_PERIOD + (* the period within which the validators are trusted *) + +Heights == 1..ULTIMATE_HEIGHT (* possible heights *) + +(* A commit is just a set of nodes who have committed the block *) +Commits == SUBSET AllNodes + +(* The set of all block headers that can be on the blockchain. + This is a simplified version of the Block data structure in the actual implementation. *) +BlockHeaders == [ + height: Heights, + \* the block height + time: Int, + \* the block timestamp in some integer units + lastCommit: Commits, + \* the nodes who have voted on the previous block, the set itself instead of a hash + (* in the implementation, only the hashes of V and NextV are stored in a block, + as V and NextV are stored in the application state *) + VS: SUBSET AllNodes, + \* the validators of this bloc. We store the validators instead of the hash. + NextVS: SUBSET AllNodes + \* the validators of the next block. We store the next validators instead of the hash. +] + +(* A signed header is just a header together with a set of commits *) +LightBlocks == [header: BlockHeaders, Commits: Commits] + +VARIABLES + refClock, + (* the current global time in integer units as perceived by the reference chain *) + blockchain, + (* A sequence of BlockHeaders, which gives us a bird view of the blockchain. *) + Faulty + (* A set of faulty nodes, which can act as validators. We assume that the set + of faulty processes is non-decreasing. If a process has recovered, it should + connect using a different id. *) + +(* all variables, to be used with UNCHANGED *) +vars == <> + +(* The set of all correct nodes in a state *) +Corr == AllNodes \ Faulty + +(* APALACHE annotations *) +a <: b == a \* type annotation + +NT == STRING +NodeSet(S) == S <: {NT} +EmptyNodeSet == NodeSet({}) + +BT == [height |-> Int, time |-> Int, lastCommit |-> {NT}, VS |-> {NT}, NextVS |-> {NT}] + +LBT == [header |-> BT, Commits |-> {NT}] +(* end of APALACHE annotations *) + +(****************************** BLOCKCHAIN ************************************) + +(* the header is still within the trusting period *) +InTrustingPeriod(header) == + refClock < header.time + TRUSTING_PERIOD + +(* + Given a function pVotingPower \in D -> Powers for some D \subseteq AllNodes + and pNodes \subseteq D, test whether the set pNodes \subseteq AllNodes has + more than 2/3 of voting power among the nodes in D. + *) +TwoThirds(pVS, pNodes) == + LET TP == Cardinality(pVS) + SP == Cardinality(pVS \intersect pNodes) + IN + 3 * SP > 2 * TP \* when thinking in real numbers, not integers: SP > 2.0 / 3.0 * TP + +(* + Given a set of FaultyNodes, test whether the voting power of the correct nodes in D + is more than 2/3 of the voting power of the faulty nodes in D. + + Parameters: + - pFaultyNodes is a set of nodes that are considered faulty + - pVS is a set of all validators, maybe including Faulty, intersecting with it, etc. + - pMaxFaultRatio is a pair <> that limits the ratio a / b of the faulty + validators from above (exclusive) + *) +FaultyValidatorsFewerThan(pFaultyNodes, pVS, maxRatio) == + LET FN == pFaultyNodes \intersect pVS \* faulty nodes in pNodes + CN == pVS \ pFaultyNodes \* correct nodes in pNodes + CP == Cardinality(CN) \* power of the correct nodes + FP == Cardinality(FN) \* power of the faulty nodes + IN + \* CP + FP = TP is the total voting power + LET TP == CP + FP IN + FP * maxRatio[2] < TP * maxRatio[1] + +(* Can a block be produced by a correct peer, or an authenticated Byzantine peer *) +IsLightBlockAllowedByDigitalSignatures(ht, block) == + \/ block.header = blockchain[ht] \* signed by correct and faulty (maybe) + \/ /\ block.Commits \subseteq Faulty + /\ block.header.height = ht + /\ block.header.time >= 0 \* signed only by faulty + +(* + Initialize the blockchain to the ultimate height right in the initial states. + We pick the faulty validators statically, but that should not affect the light client. + + Parameters: + - pMaxFaultyRatioExclusive is a pair <> that bound the number of + faulty validators in each block by the ratio a / b (exclusive) + *) +InitToHeight(pMaxFaultyRatioExclusive) == + /\ \E Nodes \in SUBSET AllNodes: + Faulty := Nodes \* pick a subset of nodes to be faulty + \* pick the validator sets and last commits + /\ \E vs, lastCommit \in [Heights -> SUBSET AllNodes]: + \E timestamp \in [Heights -> Int]: + \* refClock is at least as early as the timestamp in the last block + /\ \E tm \in Int: + refClock := tm /\ tm >= timestamp[ULTIMATE_HEIGHT] + \* the genesis starts on day 1 + /\ timestamp[1] = 1 + /\ vs[1] = AllNodes + /\ lastCommit[1] = EmptyNodeSet + /\ \A h \in Heights \ {1}: + /\ lastCommit[h] \subseteq vs[h - 1] \* the non-validators cannot commit + /\ TwoThirds(vs[h - 1], lastCommit[h]) \* the commit has >2/3 of validator votes + \* the faulty validators have the power below the threshold + /\ FaultyValidatorsFewerThan(Faulty, vs[h], pMaxFaultyRatioExclusive) + /\ timestamp[h] > timestamp[h - 1] \* the time grows monotonically + /\ timestamp[h] < timestamp[h - 1] + TRUSTING_PERIOD \* but not too fast + \* form the block chain out of validator sets and commits (this makes apalache faster) + /\ blockchain := [h \in Heights |-> + [height |-> h, + time |-> timestamp[h], + VS |-> vs[h], + NextVS |-> IF h < ULTIMATE_HEIGHT THEN vs[h + 1] ELSE AllNodes, + lastCommit |-> lastCommit[h]] + ] \****** + +(********************* BLOCKCHAIN ACTIONS ********************************) +(* + Advance the clock by zero or more time units. + *) +AdvanceTime == + /\ \E tm \in Int: tm >= refClock /\ refClock' = tm + /\ UNCHANGED <> + +============================================================================= +\* Modification History +\* Last modified Wed Jun 10 14:10:54 CEST 2020 by igor +\* Created Fri Oct 11 15:45:11 CEST 2019 by igor diff --git a/spec/light-client/attacks/Isolation_001_draft.tla b/spec/light-client/attacks/Isolation_001_draft.tla new file mode 100644 index 000000000..2fc889cd7 --- /dev/null +++ b/spec/light-client/attacks/Isolation_001_draft.tla @@ -0,0 +1,159 @@ +----------------------- MODULE Isolation_001_draft ---------------------------- +(** + * The specification of the attackers isolation at full node, + * when it has received an evidence from the light client. + * We check that the isolation spec produces a set of validators + * that have more than 1/3 of the voting power. + * + * It follows the English specification: + * + * https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/attacks/isolate-attackers_001_draft.md + * + * The assumptions made in this specification: + * + * - the voting power of every validator is 1 + * (add more validators, if you need more validators) + * + * - Tendermint security model is violated + * (there are Byzantine validators who signed a conflicting block) + * + * Igor Konnov, Zarko Milosevic, Josef Widder, Informal Systems, 2020 + *) + + +EXTENDS Integers, FiniteSets, Apalache + +\* algorithm parameters +CONSTANTS + AllNodes, + (* a set of all nodes that can act as validators (correct and faulty) *) + COMMON_HEIGHT, + (* an index of the block header that two peers agree upon *) + CONFLICT_HEIGHT, + (* an index of the block header that two peers disagree upon *) + TRUSTING_PERIOD, + (* the period within which the validators are trusted *) + FAULTY_RATIO + (* a pair <> that limits that ratio of faulty validator in the blockchain + from above (exclusive). Tendermint security model prescribes 1 / 3. *) + +VARIABLES + blockchain, (* the chain at the full node *) + refClock, (* the reference clock at the full node *) + Faulty, (* the set of faulty validators *) + conflictingBlock, (* an evidence that two peers reported conflicting blocks *) + state, (* the state of the attack isolation machine at the full node *) + attackers (* the set of the identified attackers *) + +vars == <> + +\* instantiate the chain at the full node +ULTIMATE_HEIGHT == CONFLICT_HEIGHT + 1 +BC == INSTANCE Blockchain_003_draft + +\* use the light client API +TRUSTING_HEIGHT == COMMON_HEIGHT +TARGET_HEIGHT == CONFLICT_HEIGHT + +LC == INSTANCE LCVerificationApi_003_draft + WITH localClock <- refClock, REAL_CLOCK_DRIFT <- 0, CLOCK_DRIFT <- 0 + +\* old-style type annotations in apalache +a <: b == a + +\* [LCAI-NONVALID-OUTPUT.1::TLA.1] +ViolatesValidity(header1, header2) == + \/ header1.VS /= header2.VS + \/ header1.NextVS /= header2.NextVS + \/ header1.height /= header2.height + \/ header1.time /= header2.time + (* The English specification also checks the fields that we do not have + at this level of abstraction: + - header1.ConsensusHash != header2.ConsensusHash or + - header1.AppHash != header2.AppHash or + - header1.LastResultsHash header2 != ev.LastResultsHash + *) + +Init == + /\ state := "init" + \* Pick an arbitrary blockchain from 1 to COMMON_HEIGHT + 1. + /\ BC!InitToHeight(FAULTY_RATIO) \* initializes blockchain, Faulty, and refClock + /\ attackers := {} <: {STRING} \* attackers are unknown + \* Receive an arbitrary evidence. + \* Instantiate the light block fields one by one, + \* to avoid combinatorial explosion of records. + /\ \E time \in Int: + \E VS, NextVS, lastCommit, Commits \in SUBSET AllNodes: + LET conflicting == + [ Commits |-> Commits, + header |-> + [height |-> CONFLICT_HEIGHT, + time |-> time, + VS |-> VS, + NextVS |-> NextVS, + lastCommit |-> lastCommit] ] + IN + LET refBlock == [ header |-> blockchain[COMMON_HEIGHT], + Commits |-> blockchain[COMMON_HEIGHT + 1].lastCommit ] + IN + /\ "SUCCESS" = LC!ValidAndVerifiedUntimed(refBlock, conflicting) + \* More than third of next validators in the common reference block + \* is faulty. That is a precondition for a fork. + /\ 3 * Cardinality(Faulty \intersect refBlock.header.NextVS) + > Cardinality(refBlock.header.NextVS) + \* correct validators cannot sign an invalid block + /\ ViolatesValidity(conflicting.header, refBlock.header) + => conflicting.Commits \subseteq Faulty + /\ conflictingBlock := conflicting + + +\* This is a specification of isolateMisbehavingProcesses. +\* +\* [LCAI-FUNC-MAIN.1::TLA.1] +Next == + /\ state = "init" + \* Extract the rounds from the reference block and the conflicting block. + \* In this specification, we just pick rounds non-deterministically. + \* The English specification calls RoundOf on the blocks. + /\ \E referenceRound, evidenceRound \in Int: + /\ referenceRound >= 0 /\ evidenceRound >= 0 + /\ LET reference == blockchain[CONFLICT_HEIGHT] + referenceCommit == blockchain[CONFLICT_HEIGHT + 1].lastCommit + evidenceHeader == conflictingBlock.header + evidenceCommit == conflictingBlock.Commits + IN + IF ViolatesValidity(reference, evidenceHeader) + THEN /\ attackers' := blockchain[COMMON_HEIGHT].NextVS \intersect evidenceCommit + /\ state' := "Lunatic" + ELSE IF referenceRound = evidenceRound + THEN /\ attackers' := referenceCommit \intersect evidenceCommit + /\ state' := "Equivocation" + ELSE + \* This property is shown in property + \* Accountability of TendermintAcc3.tla + /\ state' := "Amnesia" + /\ \E Attackers \in SUBSET (Faulty \intersect reference.VS): + /\ 3 * Cardinality(Attackers) > Cardinality(reference.VS) + /\ attackers' := Attackers + /\ blockchain' := blockchain + /\ refClock' := refClock + /\ Faulty' := Faulty + /\ conflictingBlock' := conflictingBlock + +(********************************** INVARIANTS *******************************) + +\* This invariant ensure that the attackers have +\* more than 1/3 of the voting power +\* +\* [LCAI-INV-Output.1::TLA-DETECTION-COMPLETENESS.1] +DetectionCompleteness == + state /= "init" => + 3 * Cardinality(attackers) > Cardinality(blockchain[CONFLICT_HEIGHT].VS) + +\* This invariant ensures that only the faulty validators are detected +\* +\* [LCAI-INV-Output.1::TLA-DETECTION-ACCURACY.1] +DetectionAccuracy == + attackers \subseteq Faulty + +============================================================================== diff --git a/spec/light-client/attacks/LCVerificationApi_003_draft.tla b/spec/light-client/attacks/LCVerificationApi_003_draft.tla new file mode 100644 index 000000000..909eab92b --- /dev/null +++ b/spec/light-client/attacks/LCVerificationApi_003_draft.tla @@ -0,0 +1,192 @@ +-------------------- MODULE LCVerificationApi_003_draft -------------------------- +(** + * The common interface of the light client verification and detection. + *) +EXTENDS Integers, FiniteSets + +\* the parameters of Light Client +CONSTANTS + TRUSTING_PERIOD, + (* the period within which the validators are trusted *) + CLOCK_DRIFT, + (* the assumed precision of the clock *) + REAL_CLOCK_DRIFT, + (* the actual clock drift, which under normal circumstances should not + be larger than CLOCK_DRIFT (otherwise, there will be a bug) *) + FAULTY_RATIO + (* a pair <> that limits that ratio of faulty validator in the blockchain + from above (exclusive). Tendermint security model prescribes 1 / 3. *) + +VARIABLES + localClock (* current time as measured by the light client *) + +(* the header is still within the trusting period *) +InTrustingPeriodLocal(header) == + \* note that the assumption about the drift reduces the period of trust + localClock < header.time + TRUSTING_PERIOD - CLOCK_DRIFT + +(* the header is still within the trusting period, even if the clock can go backwards *) +InTrustingPeriodLocalSurely(header) == + \* note that the assumption about the drift reduces the period of trust + localClock < header.time + TRUSTING_PERIOD - 2 * CLOCK_DRIFT + +(* ensure that the local clock does not drift far away from the global clock *) +IsLocalClockWithinDrift(local, global) == + /\ global - REAL_CLOCK_DRIFT <= local + /\ local <= global + REAL_CLOCK_DRIFT + +(** + * Check that the commits in an untrusted block form 1/3 of the next validators + * in a trusted header. + *) +SignedByOneThirdOfTrusted(trusted, untrusted) == + LET TP == Cardinality(trusted.header.NextVS) + SP == Cardinality(untrusted.Commits \intersect trusted.header.NextVS) + IN + 3 * SP > TP + +(** + The first part of the precondition of ValidAndVerified, which does not take + the current time into account. + + [LCV-FUNC-VALID.1::TLA-PRE-UNTIMED.1] + *) +ValidAndVerifiedPreUntimed(trusted, untrusted) == + LET thdr == trusted.header + uhdr == untrusted.header + IN + /\ thdr.height < uhdr.height + \* the trusted block has been created earlier + /\ thdr.time < uhdr.time + /\ untrusted.Commits \subseteq uhdr.VS + /\ LET TP == Cardinality(uhdr.VS) + SP == Cardinality(untrusted.Commits) + IN + 3 * SP > 2 * TP + /\ thdr.height + 1 = uhdr.height => thdr.NextVS = uhdr.VS + (* As we do not have explicit hashes we ignore these three checks of the English spec: + + 1. "trusted.Commit is a commit is for the header trusted.Header, + i.e. it contains the correct hash of the header". + 2. untrusted.Validators = hash(untrusted.Header.Validators) + 3. untrusted.NextValidators = hash(untrusted.Header.NextValidators) + *) + +(** + Check the precondition of ValidAndVerified, including the time checks. + + [LCV-FUNC-VALID.1::TLA-PRE.1] + *) +ValidAndVerifiedPre(trusted, untrusted, checkFuture) == + LET thdr == trusted.header + uhdr == untrusted.header + IN + /\ InTrustingPeriodLocal(thdr) + \* The untrusted block is not from the future (modulo clock drift). + \* Do the check, if it is required. + /\ checkFuture => uhdr.time < localClock + CLOCK_DRIFT + /\ ValidAndVerifiedPreUntimed(trusted, untrusted) + + +(** + Check, whether an untrusted block is valid and verifiable w.r.t. a trusted header. + This test does take current time into account, but only looks at the block structure. + + [LCV-FUNC-VALID.1::TLA-UNTIMED.1] + *) +ValidAndVerifiedUntimed(trusted, untrusted) == + IF ~ValidAndVerifiedPreUntimed(trusted, untrusted) + THEN "INVALID" + ELSE IF untrusted.header.height = trusted.header.height + 1 + \/ SignedByOneThirdOfTrusted(trusted, untrusted) + THEN "SUCCESS" + ELSE "NOT_ENOUGH_TRUST" + +(** + Check, whether an untrusted block is valid and verifiable w.r.t. a trusted header. + + [LCV-FUNC-VALID.1::TLA.1] + *) +ValidAndVerified(trusted, untrusted, checkFuture) == + IF ~ValidAndVerifiedPre(trusted, untrusted, checkFuture) + THEN "INVALID" + ELSE IF ~InTrustingPeriodLocal(untrusted.header) + (* We leave the following test for the documentation purposes. + The implementation should do this test, as signature verification may be slow. + In the TLA+ specification, ValidAndVerified happens in no time. + *) + THEN "FAILED_TRUSTING_PERIOD" + ELSE IF untrusted.header.height = trusted.header.height + 1 + \/ SignedByOneThirdOfTrusted(trusted, untrusted) + THEN "SUCCESS" + ELSE "NOT_ENOUGH_TRUST" + + +(** + The invariant of the light store that is not related to the blockchain + *) +LightStoreInv(fetchedLightBlocks, lightBlockStatus) == + \A lh, rh \in DOMAIN fetchedLightBlocks: + \* for every pair of stored headers that have been verified + \/ lh >= rh + \/ lightBlockStatus[lh] /= "StateVerified" + \/ lightBlockStatus[rh] /= "StateVerified" + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh /\ lightBlockStatus[mh] = "StateVerified" + \* or the left header is outside the trusting period, so no guarantees + \/ LET lhdr == fetchedLightBlocks[lh] + rhdr == fetchedLightBlocks[rh] + IN + \* we can verify the right one using the left one + "SUCCESS" = ValidAndVerifiedUntimed(lhdr, rhdr) + +(** + Correctness states that all the obtained headers are exactly like in the blockchain. + + It is always the case that every verified header in LightStore was generated by + an instance of Tendermint consensus. + + [LCV-DIST-SAFE.1::CORRECTNESS-INV.1] + *) +CorrectnessInv(blockchain, fetchedLightBlocks, lightBlockStatus) == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] = "StateVerified" => + fetchedLightBlocks[h].header = blockchain[h] + +(** + * When the light client terminates, there are no failed blocks. + * (Otherwise, someone lied to us.) + *) +NoFailedBlocksOnSuccessInv(fetchedLightBlocks, lightBlockStatus) == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] /= "StateFailed" + +(** + The expected post-condition of VerifyToTarget. + *) +VerifyToTargetPost(blockchain, isPeerCorrect, + fetchedLightBlocks, lightBlockStatus, + trustedHeight, targetHeight, finalState) == + LET trustedHeader == fetchedLightBlocks[trustedHeight].header IN + \* The light client is not lying us on the trusted block. + \* It is straightforward to detect. + /\ lightBlockStatus[trustedHeight] = "StateVerified" + /\ trustedHeight \in DOMAIN fetchedLightBlocks + /\ trustedHeader = blockchain[trustedHeight] + \* the invariants we have found in the light client verification + \* there is a problem with trusting period + /\ isPeerCorrect + => CorrectnessInv(blockchain, fetchedLightBlocks, lightBlockStatus) + \* a correct peer should fail the light client, + \* if the trusted block is in the trusting period + /\ isPeerCorrect /\ InTrustingPeriodLocalSurely(trustedHeader) + => finalState = "finishedSuccess" + /\ finalState = "finishedSuccess" => + /\ lightBlockStatus[targetHeight] = "StateVerified" + /\ targetHeight \in DOMAIN fetchedLightBlocks + /\ NoFailedBlocksOnSuccessInv(fetchedLightBlocks, lightBlockStatus) + /\ LightStoreInv(fetchedLightBlocks, lightBlockStatus) + + +================================================================================== diff --git a/spec/light-client/attacks/MC_5_3.tla b/spec/light-client/attacks/MC_5_3.tla new file mode 100644 index 000000000..552de49ae --- /dev/null +++ b/spec/light-client/attacks/MC_5_3.tla @@ -0,0 +1,18 @@ +------------------------- MODULE MC_5_3 ------------------------------------- + +AllNodes == {"n1", "n2", "n3", "n4", "n5"} +COMMON_HEIGHT == 1 +CONFLICT_HEIGHT == 3 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +FAULTY_RATIO == <<1, 2>> \* < 1 / 2 faulty validators + +VARIABLES + blockchain, \* the reference blockchain + refClock, \* current time in the reference blockchain + Faulty, \* the set of faulty validators + state, \* the state of the light client detector + conflictingBlock, \* an evidence that two peers reported conflicting blocks + attackers + +INSTANCE Isolation_001_draft +============================================================================ diff --git a/spec/light-client/attacks/isolate-attackers_001_draft.md b/spec/light-client/attacks/isolate-attackers_001_draft.md new file mode 100644 index 000000000..93cb43410 --- /dev/null +++ b/spec/light-client/attacks/isolate-attackers_001_draft.md @@ -0,0 +1,221 @@ + +# Lightclient Attackers Isolation + +> Warning: This is the beginning of an unfinished draft. Don't continue reading! + +Adversarial nodes may have the incentive to lie to a lightclient about the state of a Tendermint blockchain. An attempt to do so is called attack. Light client [verification][verification] checks incoming data by checking a so-called "commit", which is a forwarded set of signed messages that is (supposedly) produced during executing Tendermint consensus. Thus, an attack boils down to creating and signing Tendermint consensus messages in deviation from the Tendermint consensus algorithm rules. + +As Tendermint consensus and light client verification is safe under the assumption of more than 2/3 of correct voting power per block [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link], this implies that if there was an attack then [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link] was violated, that is, there is a block such that + +- validators deviated from the protocol, and +- these validators represent more than 1/3 of the voting power in that block. + +In the case of an [attack][node-based-attack-characterization], the lightclient [attack detection mechanism][detection] computes data, so called evidence [[LC-DATA-EVIDENCE.1]][LC-DATA-EVIDENCE-link], that can be used + +- to proof that there has been attack [[TMBC-LC-EVIDENCE-DATA.1]][TMBC-LC-EVIDENCE-DATA-link] and +- as basis to find the actual nodes that deviated from the Tendermint protocol. + +This specification considers how a full node in a Tendermint blockchain can isolate a set of attackers that launched the attack. The set should satisfy + +- the set does not contain a correct validator +- the set contains validators that represent more than 1/3 of the voting power of a block that is still within the unbonding period + +# Outline + +**TODO** when preparing a version for broader review. + +# Part I - Basics + +For definitions of data structures used here, in particular LightBlocks [[LCV-DATA-LIGHTBLOCK.1]](https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#lcv-data-lightblock1), cf. [Light Client Verification][verification]. + +# Part II - Definition of the Problem + +The specification of the [detection mechanism][detection] describes + +- what is a light client attack, +- conditions under which the detector will detect a light client attack, +- and the format of the output data, called evidence, in the case an attack is detected. The format is defined in +[[LC-DATA-EVIDENCE.1]][LC-DATA-EVIDENCE-link] and looks as follows + +```go +type LightClientAttackEvidence struct { + ConflictingBlock LightBlock + CommonHeight int64 +} +``` + +The isolator is a function that gets as input evidence `ev` +and a prefix of the blockchain `bc` at least up to height `ev.ConflictingBlock.Header.Height + 1`. The output is a set of *peerIDs* of validators. + +We assume that the full node is synchronized with the blockchain and has reached the height `ev.ConflictingBlock.Header.Height + 1`. + +#### **[FN-INV-Output.1]** + +When an output is generated it satisfies the following properties: + +- If + - `bc[CommonHeight].bfttime` is within the unbonding period w.r.t. the time at the full node, + - `ev.ConflictingBlock.Header != bc[ev.ConflictingBlock.Header.Height]` + - Validators in `ev.ConflictingBlock.Commit` represent more than 1/3 of the voting power in `bc[ev.CommonHeight].NextValidators` +- Then: A set of validators in `bc[CommonHeight].NextValidators` that + - represent more than 1/3 of the voting power in `bc[ev.commonHeight].NextValidators` + - signed Tendermint consensus messages for height `ev.ConflictingBlock.Header.Height` by violating the Tendermint consensus protocol. +- Else: the empty set. + +# Part IV - Protocol + +Here we discuss how to solve the problem of isolating misbehaving processes. We describe the function `isolateMisbehavingProcesses` as well as all the helping functions below. In [Part V](#part-v---Completeness), we discuss why the solution is complete based on result from analysis with automated tools. + +## Isolation + +### Outline + +> Describe solution (in English), decomposition into functions, where communication to other components happens. + +#### **[LCAI-FUNC-MAIN.1]** + +```go +func isolateMisbehavingProcesses(ev LightClientAttackEvidence, bc Blockchain) []ValidatorAddress { + + reference := bc[ev.conflictingBlock.Header.Height].Header + ev_header := ev.conflictingBlock.Header + + ref_commit := bc[ev.conflictingBlock.Header.Height + 1].Header.LastCommit // + 1 !! + ev_commit := ev.conflictingBlock.Commit + + if violatesTMValidity(reference, ev_header) { + // lunatic light client attack + signatories := Signers(ev.ConflictingBlock.Commit) + bonded_vals := Addresses(bc[ev.CommonHeight].NextValidators) + return intersection(signatories,bonded_vals) + + } + // If this point is reached the validator sets in reference and ev_header are identical + else if RoundOf(ref_commit) == RoundOf(ev_commit) { + // equivocation light client attack + return intersection(Signers(ref_commit), Signers(ev_commit)) + } + else { + // amnesia light client attack + return IsolateAmnesiaAttacker(ev, bc) + } +} +``` + +- Implementation comment + - If the full node has only reached height `ev.conflictingBlock.Header.Height` then `bc[ev.conflictingBlock.Header.Height + 1].Header.LastCommit` refers to the locally stored commit for this height. (This commit must be present by the precondition on `length(bc)`.) + - We check in the precondition that the unbonding period is not expired. However, since time moves on, before handing the validators over Cosmos SDK, the time needs to be checked again to satisfy the contract which requires that only bonded validators are reported. This passing of validators to the SDK is out of scope of this specification. +- Expected precondition + - `length(bc) >= ev.conflictingBlock.Header.Height` + - `ValidAndVerifiedUnbonding(bc[ev.CommonHeight], ev.ConflictingBlock) == SUCCESS` + - `ev.ConflictingBlock.Header != bc[ev.ConflictingBlock.Header.Height]` + - TODO: input light blocks pass basic validation +- Expected postcondition + - [[FN-INV-Output.1]](#FN-INV-Output1) holds +- Error condition + - returns an error if precondition is violated. + +### Details of the Functions + +#### **[LCAI-FUNC-VVU.1]** + +```go +func ValidAndVerifiedUnbonding(trusted LightBlock, untrusted LightBlock) Result +``` + +- Conditions are identical to [[LCV-FUNC-VALID.2]][LCV-FUNC-VALID.link] except the precondition "*trusted.Header.Time > now - trustingPeriod*" is substituted with + - `trusted.Header.Time > now - UnbondingPeriod` + +#### **[LCAI-FUNC-NONVALID.1]** + +```go +func violatesTMValidity(ref Header, ev Header) boolean +``` + +- Implementation remarks + - checks whether the evidence header `ev` violates the validity property of Tendermint Consensus, by checking agains a reference header +- Expected precondition + - `ref.Height == ev.Height` +- Expected postcondition + - returns evaluation of the following disjunction + **[[LCAI-NONVALID-OUTPUT.1]]** == + `ref.ValidatorsHash != ev.ValidatorsHash` or + `ref.NextValidatorsHash != ev.NextValidatorsHash` or + `ref.ConsensusHash != ev.ConsensusHash` or + `ref.AppHash != ev.AppHash` or + `ref.LastResultsHash != ev.LastResultsHash` + +```go +func IsolateAmnesiaAttacker(ev LightClientAttackEvidence, bc Blockchain) []ValidatorAddress +``` + +- Implementation remarks + **TODO:** What should we do here? Refer to the accountability doc? +- Expected postcondition + **TODO:** What should we do here? Refer to the accountability doc? + +```go +func RoundOf(commit Commit) []ValidatorAddress +``` + +- Expected precondition + - `commit` is well-formed. In particular all votes are from the same round `r`. +- Expected postcondition + - returns round `r` that is encoded in all the votes of the commit + +```go +func Signers(commit Commit) []ValidatorAddress +``` + +- Expected postcondition + - returns all validator addresses in `commit` + +```go +func Addresses(vals Validator[]) ValidatorAddress[] +``` + +- Expected postcondition + - returns all validator addresses in `vals` + +# Part V - Completeness + +As discussed in the beginning of this document, an attack boils down to creating and signing Tendermint consensus messages in deviation from the Tendermint consensus algorithm rules. +The main function `isolateMisbehavingProcesses` distinguishes three kinds of wrongly signing messages, namely, + +- lunatic: signing invalid blocks +- equivocation: double-signing valid blocks in the same consensus round +- amnesia: signing conflicting blocks in different consensus rounds, without having seen a quorum of messages that would have allowed to do so. + +The question is whether this captures all attacks. +First observe that the first checking in `isolateMisbehavingProcesses` is `violatesTMValidity`. It takes care of lunatic attacks. If this check passes, that is, if `violatesTMValidity` returns `FALSE` this means that [FN-NONVALID-OUTPUT] evaluates to false, which implies that `ref.ValidatorsHash = ev.ValidatorsHash`. Hence after `violatesTMValidity`, all the involved validators are the ones from the blockchain. It is thus sufficient to analyze one instance of Tendermint consensus with a fixed group membership (set of validators). Also it is sufficient to consider two different valid consensus values, that is, binary consensus. + +**TODO** we have analyzed Tendermint consensus with TLA+ and have accompanied Galois in an independent study of the protocol based on [Ivy proofs](https://github.com/tendermint/spec/tree/master/ivy-proofs). + +# References + +[[supervisor]] The specification of the light client supervisor. + +[[verification]] The specification of the light client verification protocol + +[[detection]] The specification of the light client attack detection mechanism. + +[supervisor]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/supervisor/supervisor_001_draft.md + +[verification]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md + +[detection]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/detection/detection_003_reviewed.md + +[LC-DATA-EVIDENCE-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/detection/detection_003_reviewed.md#lc-data-evidence1 + +[TMBC-LC-EVIDENCE-DATA-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/detection/detection_003_reviewed.md#tmbc-lc-evidence-data1 + +[node-based-attack-characterization]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/detection/detection_003_reviewed.md#node-based-characterization-of-attacks + +[TMBC-FM-2THIRDS-link]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-fm-2thirds1 + +[LCV-FUNC-VALID.link]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#lcv-func-valid2 diff --git a/spec/light-client/attacks/isolate-attackers_002_reviewed.md b/spec/light-client/attacks/isolate-attackers_002_reviewed.md new file mode 100644 index 000000000..24bb5aba8 --- /dev/null +++ b/spec/light-client/attacks/isolate-attackers_002_reviewed.md @@ -0,0 +1,223 @@ +# Lightclient Attackers Isolation + +Adversarial nodes may have the incentive to lie to a lightclient about the state of a Tendermint blockchain. An attempt to do so is called attack. Light client [verification][verification] checks incoming data by checking a so-called "commit", which is a forwarded set of signed messages that is (supposedly) produced during executing Tendermint consensus. Thus, an attack boils down to creating and signing Tendermint consensus messages in deviation from the Tendermint consensus algorithm rules. + +As Tendermint consensus and light client verification is safe under the assumption of more than 2/3 of correct voting power per block [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link], this implies that if there was an attack then [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link] was violated, that is, there is a block such that + +- validators deviated from the protocol, and +- these validators represent more than 1/3 of the voting power in that block. + +In the case of an [attack][node-based-attack-characterization], the lightclient [attack detection mechanism][detection] computes data, so called evidence [[LC-DATA-EVIDENCE.1]][LC-DATA-EVIDENCE-link], that can be used + +- to proof that there has been attack [[TMBC-LC-EVIDENCE-DATA.1]][TMBC-LC-EVIDENCE-DATA-link] and +- as basis to find the actual nodes that deviated from the Tendermint protocol. + +This specification considers how a full node in a Tendermint blockchain can isolate a set of attackers that launched the attack. The set should satisfy + +- the set does not contain a correct validator +- the set contains validators that represent more than 1/3 of the voting power of a block that is still within the unbonding period + +# Outline + +After providing the [problem statement](#Part-I---Basics-and-Definition-of-the-Problem), we specify the [isolator function](#Part-II---Protocol) and close with the discussion about its [correctness](#Part-III---Completeness) which is based on computer-aided analysis of Tendermint Consensus. + +# Part I - Basics and Definition of the Problem + +For definitions of data structures used here, in particular LightBlocks [[LCV-DATA-LIGHTBLOCK.1]](https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#lcv-data-lightblock1), we refer to the specification of [Light Client Verification][verification]. + +The specification of the [detection mechanism][detection] describes + +- what is a light client attack, +- conditions under which the detector will detect a light client attack, +- and the format of the output data, called evidence, in the case an attack is detected. The format is defined in +[[LC-DATA-EVIDENCE.1]][LC-DATA-EVIDENCE-link] and looks as follows + +```go +type LightClientAttackEvidence struct { + ConflictingBlock LightBlock + CommonHeight int64 +} +``` + +The isolator is a function that gets as input evidence `ev` +and a prefix of the blockchain `bc` at least up to height `ev.ConflictingBlock.Header.Height + 1`. The output is a set of *peerIDs* of validators. + +We assume that the full node is synchronized with the blockchain and has reached the height `ev.ConflictingBlock.Header.Height + 1`. + +#### **[LCAI-INV-Output.1]** + +When an output is generated it satisfies the following properties: + +- If + - `bc[CommonHeight].bfttime` is within the unbonding period w.r.t. the time at the full node, + - `ev.ConflictingBlock.Header != bc[ev.ConflictingBlock.Header.Height]` + - Validators in `ev.ConflictingBlock.Commit` represent more than 1/3 of the voting power in `bc[ev.CommonHeight].NextValidators` +- Then: The output is a set of validators in `bc[CommonHeight].NextValidators` that + - represent more than 1/3 of the voting power in `bc[ev.commonHeight].NextValidators` + - signed Tendermint consensus messages for height `ev.ConflictingBlock.Header.Height` by violating the Tendermint consensus protocol. +- Else: the empty set. + +# Part II - Protocol + +Here we discuss how to solve the problem of isolating misbehaving processes. We describe the function `isolateMisbehavingProcesses` as well as all the helping functions below. In [Part III](#part-III---Completeness), we discuss why the solution is complete based on result from analysis with automated tools. + +## Isolation + +### Outline + +We first check whether the conflicting block can indeed be verified from the common height. We then first check whether it was a lunatic attack (violating validity). If this is not the case, we check for equivocation. If this also is not the case, we start the on-chain [accountability protocol](https://docs.google.com/document/d/11ZhMsCj3y7zIZz4udO9l25xqb0kl7gmWqNpGVRzOeyY/edit). + +#### **[LCAI-FUNC-MAIN.1]** + +```go +func isolateMisbehavingProcesses(ev LightClientAttackEvidence, bc Blockchain) []ValidatorAddress { + + reference := bc[ev.conflictingBlock.Header.Height].Header + ev_header := ev.conflictingBlock.Header + + ref_commit := bc[ev.conflictingBlock.Header.Height + 1].Header.LastCommit // + 1 !! + ev_commit := ev.conflictingBlock.Commit + + if violatesTMValidity(reference, ev_header) { + // lunatic light client attack + signatories := Signers(ev.ConflictingBlock.Commit) + bonded_vals := Addresses(bc[ev.CommonHeight].NextValidators) + return intersection(signatories,bonded_vals) + + } + // If this point is reached the validator sets in reference and ev_header are identical + else if RoundOf(ref_commit) == RoundOf(ev_commit) { + // equivocation light client attack + return intersection(Signers(ref_commit), Signers(ev_commit)) + } + else { + // amnesia light client attack + return IsolateAmnesiaAttacker(ev, bc) + } +} +``` + +- Implementation comment + - If the full node has only reached height `ev.conflictingBlock.Header.Height` then `bc[ev.conflictingBlock.Header.Height + 1].Header.LastCommit` refers to the locally stored commit for this height. (This commit must be present by the precondition on `length(bc)`.) + - We check in the precondition that the unbonding period is not expired. However, since time moves on, before handing the validators over Cosmos SDK, the time needs to be checked again to satisfy the contract which requires that only bonded validators are reported. This passing of validators to the SDK is out of scope of this specification. +- Expected precondition + - `length(bc) >= ev.conflictingBlock.Header.Height` + - `ValidAndVerifiedUnbonding(bc[ev.CommonHeight], ev.ConflictingBlock) == SUCCESS` + - `ev.ConflictingBlock.Header != bc[ev.ConflictingBlock.Header.Height]` + - `ev.conflictingBlock` satisfies basic validation (in particular all signed messages in the Commit are from the same round) +- Expected postcondition + - [[FN-INV-Output.1]](#FN-INV-Output1) holds +- Error condition + - returns an error if precondition is violated. + +### Details of the Functions + +#### **[LCAI-FUNC-VVU.1]** + +```go +func ValidAndVerifiedUnbonding(trusted LightBlock, untrusted LightBlock) Result +``` + +- Conditions are identical to [[LCV-FUNC-VALID.2]][LCV-FUNC-VALID.link] except the precondition "*trusted.Header.Time > now - trustingPeriod*" is substituted with + - `trusted.Header.Time > now - UnbondingPeriod` + +#### **[LCAI-FUNC-NONVALID.1]** + +```go +func violatesTMValidity(ref Header, ev Header) boolean +``` + +- Implementation remarks + - checks whether the evidence header `ev` violates the validity property of Tendermint Consensus, by checking against a reference header +- Expected precondition + - `ref.Height == ev.Height` +- Expected postcondition + - returns evaluation of the following disjunction + **[LCAI-NONVALID-OUTPUT.1]** == + `ref.ValidatorsHash != ev.ValidatorsHash` or + `ref.NextValidatorsHash != ev.NextValidatorsHash` or + `ref.ConsensusHash != ev.ConsensusHash` or + `ref.AppHash != ev.AppHash` or + `ref.LastResultsHash != ev.LastResultsHash` + +```go +func IsolateAmnesiaAttacker(ev LightClientAttackEvidence, bc Blockchain) []ValidatorAddress +``` + +- Implementation remarks + - This triggers the [query/response protocol](https://docs.google.com/document/d/11ZhMsCj3y7zIZz4udO9l25xqb0kl7gmWqNpGVRzOeyY/edit). +- Expected postcondition + - returns attackers according to [LCAI-INV-Output.1]. + +```go +func RoundOf(commit Commit) []ValidatorAddress +``` + +- Expected precondition + - `commit` is well-formed. In particular all votes are from the same round `r`. +- Expected postcondition + - returns round `r` that is encoded in all the votes of the commit +- Error condition + - reports error if precondition is violated + +```go +func Signers(commit Commit) []ValidatorAddress +``` + +- Expected postcondition + - returns all validator addresses in `commit` + +```go +func Addresses(vals Validator[]) ValidatorAddress[] +``` + +- Expected postcondition + - returns all validator addresses in `vals` + +# Part III - Completeness + +As discussed in the beginning of this document, an attack boils down to creating and signing Tendermint consensus messages in deviation from the Tendermint consensus algorithm rules. +The main function `isolateMisbehavingProcesses` distinguishes three kinds of wrongly signed messages, namely, + +- lunatic: signing invalid blocks +- equivocation: double-signing valid blocks in the same consensus round +- amnesia: signing conflicting blocks in different consensus rounds, without having seen a quorum of messages that would have allowed to do so. + +The question is whether this captures all attacks. +First observe that the first check in `isolateMisbehavingProcesses` is `violatesTMValidity`. It takes care of lunatic attacks. If this check passes, that is, if `violatesTMValidity` returns `FALSE` this means that [[LCAI-NONVALID-OUTPUT.1]](#LCAI-FUNC-NONVALID1]) evaluates to false, which implies that `ref.ValidatorsHash = ev.ValidatorsHash`. Hence, after `violatesTMValidity`, all the involved validators are the ones from the blockchain. It is thus sufficient to analyze one instance of Tendermint consensus with a fixed group membership (set of validators). Also, as we have two different blocks for the same height, it is sufficient to consider two different valid consensus values, that is, binary consensus. + +For this fixed group membership, we have analyzed the attacks using the TLA+ specification of [Tendermint Consensus in TLA+][tendermint-accountability]. We checked that indeed the only possible scenarios that can lead to violation of agreement are **equivocation** and **amnesia**. An independent study by Galois of the protocol based on [Ivy proofs](https://github.com/tendermint/spec/tree/master/ivy-proofs) led to the same conclusion. + +# References + +[[supervisor]] The specification of the light client supervisor. + +[[verification]] The specification of the light client verification protocol. + +[[detection]] The specification of the light client attack detection mechanism. + +[[tendermint-accountability]]: TLA+ specification to check the types of attacks + +[tendermint-accountability]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/tendermint-accountability/README.md + +[supervisor]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/supervisor/supervisor_001_draft.md + +[verification]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md + +[detection]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/detection/detection_003_reviewed.md + +[LC-DATA-EVIDENCE-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/detection/detection_003_reviewed.md#lc-data-evidence1 + +[TMBC-LC-EVIDENCE-DATA-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/detection/detection_003_reviewed.md#tmbc-lc-evidence-data1 + +[node-based-attack-characterization]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/detection/detection_003_reviewed.md#node-based-characterization-of-attacks + +[TMBC-FM-2THIRDS-link]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-fm-2thirds1 + +[LCV-FUNC-VALID.link]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#lcv-func-valid2 diff --git a/spec/light-client/attacks/notes-on-evidence-handling.md b/spec/light-client/attacks/notes-on-evidence-handling.md new file mode 100644 index 000000000..4b7d81919 --- /dev/null +++ b/spec/light-client/attacks/notes-on-evidence-handling.md @@ -0,0 +1,219 @@ + +# Light client attacks + +We define a light client attack as detection of conflicting headers for a given height that can be verified +starting from the trusted light block. A light client attack is defined in the context of interactions of +light client with two peers. One of the peers (called primary) defines a trace of verified light blocks +(primary trace) that are being checked against trace of the other peer (called witness) that we call +witness trace. + +A light client attack is defined by the primary and witness traces +that have a common root (the same trusted light block for a common height) but forms +conflicting branches (end of traces is for the same height but with different headers). +Note that conflicting branches could be arbitrarily big as branches continue to diverge after +a bifurcation point. We propose an approach that allows us to define a valid light client attack +only with a common light block and a single conflicting light block. We rely on the fact that +we assume that the primary is under suspicion (therefore not trusted) and that the witness plays +support role to detect and process an attack (therefore trusted). Therefore, once a light client +detects an attack, it needs to send to a witness only missing data (common height +and conflicting light block) as it has its trace. Keeping light client attack data of constant size +saves bandwidth and reduces an attack surface. As we will explain below, although in the context of +light client core +[verification](https://github.com/informalsystems/tendermint-rs/tree/master/docs/spec/lightclient/verification) +the roles of primary and witness are clearly defined, +in case of the attack, we run the same attack detection procedure twice where the roles are swapped. +The rationale is that the light client does not know what peer is correct (on a right main branch) +so it tries to create and submit an attack evidence to both peers. + +Light client attack evidence consists of a conflicting light block and a common height. + +```go +type LightClientAttackEvidence struct { + ConflictingBlock LightBlock + CommonHeight int64 +} +``` + +Full node can validate a light client attack evidence by executing the following procedure: + +```go +func IsValid(lcaEvidence LightClientAttackEvidence, bc Blockchain) boolean { + commonBlock = GetLightBlock(bc, lcaEvidence.CommonHeight) + if commonBlock == nil return false + + // Note that trustingPeriod in ValidAndVerified is set to UNBONDING_PERIOD + verdict = ValidAndVerified(commonBlock, lcaEvidence.ConflictingBlock) + conflictingHeight = lcaEvidence.ConflictingBlock.Header.Height + + return verdict == OK and bc[conflictingHeight].Header != lcaEvidence.ConflictingBlock.Header +} +``` + +## Light client attack creation + +Given a trusted light block `trusted`, a light node executes the bisection algorithm to verify header +`untrusted` at some height `h`. If the bisection algorithm succeeds, then the header `untrusted` is verified. +Headers that are downloaded as part of the bisection algorithm are stored in a store and they are also in +the verified state. Therefore, after the bisection algorithm successfully terminates we have a trace of +the light blocks ([] LightBlock) we obtained from the primary that we call primary trace. + +### Primary trace + +The following invariant holds for the primary trace: + +- Given a `trusted` light block, target height `h`, and `primary_trace` ([] LightBlock): + *primary_trace[0] == trusted* and *primary_trace[len(primary_trace)-1].Height == h* and + successive light blocks are passing light client verification logic. + +### Witness with a conflicting header + +The verified header at height `h` is cross-checked with every witness as part of +[detection](https://github.com/informalsystems/tendermint-rs/tree/master/docs/spec/lightclient/detection). +If a witness returns the conflicting header at the height `h` the following procedure is executed to verify +if the conflicting header comes from the valid trace and if that's the case to create an attack evidence: + +#### Helper functions + +We assume the following helper functions: + +```go +// Returns trace of verified light blocks starting from rootHeight and ending with targetHeight. +Trace(lightStore LightStore, rootHeight int64, targetHeight int64) LightBlock[] + +// Returns validator set for the given height +GetValidators(bc Blockchain, height int64) Validator[] + +// Returns validator set for the given height +GetValidators(bc Blockchain, height int64) Validator[] + +// Return validator addresses for the given validators +GetAddresses(vals Validator[]) ValidatorAddress[] +``` + +```go +func DetectLightClientAttacks(primary PeerID, + primary_trace []LightBlock, + witness PeerID) (LightClientAttackEvidence, LightClientAttackEvidence) { + primary_lca_evidence, witness_trace = DetectLightClientAttack(primary_trace, witness) + + witness_lca_evidence = nil + if witness_trace != nil { + witness_lca_evidence, _ = DetectLightClientAttack(witness_trace, primary) + } + return primary_lca_evidence, witness_lca_evidence +} + +func DetectLightClientAttack(trace []LightBlock, peer PeerID) (LightClientAttackEvidence, []LightBlock) { + + lightStore = new LightStore().Update(trace[0], StateTrusted) + + for i in 1..len(trace)-1 { + lightStore, result = VerifyToTarget(peer, lightStore, trace[i].Header.Height) + + if result == ResultFailure then return (nil, nil) + + current = lightStore.Get(trace[i].Header.Height) + + // if obtained header is the same as in the trace we continue with a next height + if current.Header == trace[i].Header continue + + // we have identified a conflicting header + commonBlock = trace[i-1] + conflictingBlock = trace[i] + + return (LightClientAttackEvidence { conflictingBlock, commonBlock.Header.Height }, + Trace(lightStore, trace[i-1].Header.Height, trace[i].Header.Height)) + } + return (nil, nil) +} +``` + +## Evidence handling + +As part of on chain evidence handling, full nodes identifies misbehaving processes and informs +the application, so they can be slashed. Note that only bonded validators should +be reported to the application. There are three types of attacks that can be executed against +Tendermint light client: + +- lunatic attack +- equivocation attack and +- amnesia attack. + +We now specify the evidence handling logic. + +```go +func detectMisbehavingProcesses(lcAttackEvidence LightClientAttackEvidence, bc Blockchain) []ValidatorAddress { + assume IsValid(lcaEvidence, bc) + + // lunatic light client attack + if !isValidBlock(current.Header, conflictingBlock.Header) { + conflictingCommit = lcAttackEvidence.ConflictingBlock.Commit + bondedValidators = GetNextValidators(bc, lcAttackEvidence.CommonHeight) + + return getSigners(conflictingCommit) intersection GetAddresses(bondedValidators) + + // equivocation light client attack + } else if current.Header.Round == conflictingBlock.Header.Round { + conflictingCommit = lcAttackEvidence.ConflictingBlock.Commit + trustedCommit = bc[conflictingBlock.Header.Height+1].LastCommit + + return getSigners(trustedCommit) intersection getSigners(conflictingCommit) + + // amnesia light client attack + } else { + HandleAmnesiaAttackEvidence(lcAttackEvidence, bc) + } +} + +// Block validity in this context is defined by the trusted header. +func isValidBlock(trusted Header, conflicting Header) boolean { + return trusted.ValidatorsHash == conflicting.ValidatorsHash and + trusted.NextValidatorsHash == conflicting.NextValidatorsHash and + trusted.ConsensusHash == conflicting.ConsensusHash and + trusted.AppHash == conflicting.AppHash and + trusted.LastResultsHash == conflicting.LastResultsHash +} + +func getSigners(commit Commit) []ValidatorAddress { + signers = []ValidatorAddress + for (i, commitSig) in commit.Signatures { + if commitSig.BlockIDFlag == BlockIDFlagCommit { + signers.append(commitSig.ValidatorAddress) + } + } + return signers +} +``` + +Note that amnesia attack evidence handling involves more complex processing, i.e., cannot be +defined simply on amnesia attack evidence. We explain in the following section a protocol +for handling amnesia attack evidence. + +### Amnesia attack evidence handling + +Detecting faulty processes in case of the amnesia attack is more complex and cannot be inferred +purely based on attack evidence data. In this case, in order to detect misbehaving processes we need +access to votes processes sent/received during the conflicting height. Therefore, amnesia handling assumes that +validators persist all votes received and sent during multi-round heights (as amnesia attack +is only possible in heights that executes over multiple rounds, i.e., commit round > 0). + +To simplify description of the algorithm we assume existence of the trusted oracle called monitor that will +drive the algorithm and output faulty processes at the end. Monitor can be implemented in a +distributed setting as on-chain module. The algorithm works as follows: + 1) Monitor sends votesets request to validators of the conflicting height. Validators + are expected to send their votesets within predefined timeout. + 2) Upon receiving votesets request, validators send their votesets to a monitor. + 2) Validators which have not sent its votesets within timeout are considered faulty. + 3) The preprocessing of the votesets is done. That means that the received votesets are analyzed + and each vote (valid) sent by process p is added to the voteset of the sender p. This phase ensures that + votes sent by faulty processes observed by at least one correct validator cannot be excluded from the analysis. + 4) Votesets of every validator are analyzed independently to decide whether the validator is correct or faulty. + A faulty validators is the one where at least one of those invalid transitions is found: + - More than one PREVOTE message is sent in a round + - More than one PRECOMMIT message is sent in a round + - PRECOMMIT message is sent without receiving +2/3 of voting-power equivalent + appropriate PREVOTE messages + - PREVOTE message is sent for the value V’ in round r’ and the PRECOMMIT message had + been sent for the value V in round r by the same process (r’ > r) and there are no + +2/3 of voting-power equivalent PREVOTE(vr, V’) messages (vr ≥ 0 and vr > r and vr < r’) + as the justification for sending PREVOTE(r’, V’) diff --git a/spec/light-client/detection/004bmc-apalache-ok.csv b/spec/light-client/detection/004bmc-apalache-ok.csv new file mode 100644 index 000000000..bf4f53ea2 --- /dev/null +++ b/spec/light-client/detection/004bmc-apalache-ok.csv @@ -0,0 +1,10 @@ +no;filename;tool;timeout;init;inv;next;args +1;LCD_MC3_3_faulty.tla;apalache;1h;;CommonHeightOnEvidenceInv;;--length=10 +2;LCD_MC3_3_faulty.tla;apalache;1h;;AccuracyInv;;--length=10 +3;LCD_MC3_3_faulty.tla;apalache;1h;;PrecisionInvLocal;;--length=10 +4;LCD_MC3_4_faulty.tla;apalache;1h;;CommonHeightOnEvidenceInv;;--length=10 +5;LCD_MC3_4_faulty.tla;apalache;1h;;AccuracyInv;;--length=10 +6;LCD_MC3_4_faulty.tla;apalache;1h;;PrecisionInvLocal;;--length=10 +7;LCD_MC4_4_faulty.tla;apalache;1h;;CommonHeightOnEvidenceInv;;--length=10 +8;LCD_MC4_4_faulty.tla;apalache;1h;;AccuracyInv;;--length=10 +9;LCD_MC4_4_faulty.tla;apalache;1h;;PrecisionInvLocal;;--length=10 diff --git a/spec/light-client/detection/005bmc-apalache-error.csv b/spec/light-client/detection/005bmc-apalache-error.csv new file mode 100644 index 000000000..1b9dd05ca --- /dev/null +++ b/spec/light-client/detection/005bmc-apalache-error.csv @@ -0,0 +1,4 @@ +no;filename;tool;timeout;init;inv;next;args +1;LCD_MC3_3_faulty.tla;apalache;1h;;PrecisionInvGrayZone;;--length=10 +2;LCD_MC3_4_faulty.tla;apalache;1h;;PrecisionInvGrayZone;;--length=10 +3;LCD_MC4_4_faulty.tla;apalache;1h;;PrecisionInvGrayZone;;--length=10 diff --git a/spec/light-client/detection/Blockchain_003_draft.tla b/spec/light-client/detection/Blockchain_003_draft.tla new file mode 100644 index 000000000..2b37c1b18 --- /dev/null +++ b/spec/light-client/detection/Blockchain_003_draft.tla @@ -0,0 +1,164 @@ +------------------------ MODULE Blockchain_003_draft ----------------------------- +(* + This is a high-level specification of Tendermint blockchain + that is designed specifically for the light client. + Validators have the voting power of one. If you like to model various + voting powers, introduce multiple copies of the same validator + (do not forget to give them unique names though). + *) +EXTENDS Integers, FiniteSets + +Min(a, b) == IF a < b THEN a ELSE b + +CONSTANT + AllNodes, + (* a set of all nodes that can act as validators (correct and faulty) *) + ULTIMATE_HEIGHT, + (* a maximal height that can be ever reached (modelling artifact) *) + TRUSTING_PERIOD + (* the period within which the validators are trusted *) + +Heights == 1..ULTIMATE_HEIGHT (* possible heights *) + +(* A commit is just a set of nodes who have committed the block *) +Commits == SUBSET AllNodes + +(* The set of all block headers that can be on the blockchain. + This is a simplified version of the Block data structure in the actual implementation. *) +BlockHeaders == [ + height: Heights, + \* the block height + time: Int, + \* the block timestamp in some integer units + lastCommit: Commits, + \* the nodes who have voted on the previous block, the set itself instead of a hash + (* in the implementation, only the hashes of V and NextV are stored in a block, + as V and NextV are stored in the application state *) + VS: SUBSET AllNodes, + \* the validators of this bloc. We store the validators instead of the hash. + NextVS: SUBSET AllNodes + \* the validators of the next block. We store the next validators instead of the hash. +] + +(* A signed header is just a header together with a set of commits *) +LightBlocks == [header: BlockHeaders, Commits: Commits] + +VARIABLES + refClock, + (* the current global time in integer units as perceived by the reference chain *) + blockchain, + (* A sequence of BlockHeaders, which gives us a bird view of the blockchain. *) + Faulty + (* A set of faulty nodes, which can act as validators. We assume that the set + of faulty processes is non-decreasing. If a process has recovered, it should + connect using a different id. *) + +(* all variables, to be used with UNCHANGED *) +vars == <> + +(* The set of all correct nodes in a state *) +Corr == AllNodes \ Faulty + +(* APALACHE annotations *) +a <: b == a \* type annotation + +NT == STRING +NodeSet(S) == S <: {NT} +EmptyNodeSet == NodeSet({}) + +BT == [height |-> Int, time |-> Int, lastCommit |-> {NT}, VS |-> {NT}, NextVS |-> {NT}] + +LBT == [header |-> BT, Commits |-> {NT}] +(* end of APALACHE annotations *) + +(****************************** BLOCKCHAIN ************************************) + +(* the header is still within the trusting period *) +InTrustingPeriod(header) == + refClock < header.time + TRUSTING_PERIOD + +(* + Given a function pVotingPower \in D -> Powers for some D \subseteq AllNodes + and pNodes \subseteq D, test whether the set pNodes \subseteq AllNodes has + more than 2/3 of voting power among the nodes in D. + *) +TwoThirds(pVS, pNodes) == + LET TP == Cardinality(pVS) + SP == Cardinality(pVS \intersect pNodes) + IN + 3 * SP > 2 * TP \* when thinking in real numbers, not integers: SP > 2.0 / 3.0 * TP + +(* + Given a set of FaultyNodes, test whether the voting power of the correct nodes in D + is more than 2/3 of the voting power of the faulty nodes in D. + + Parameters: + - pFaultyNodes is a set of nodes that are considered faulty + - pVS is a set of all validators, maybe including Faulty, intersecting with it, etc. + - pMaxFaultRatio is a pair <> that limits the ratio a / b of the faulty + validators from above (exclusive) + *) +FaultyValidatorsFewerThan(pFaultyNodes, pVS, maxRatio) == + LET FN == pFaultyNodes \intersect pVS \* faulty nodes in pNodes + CN == pVS \ pFaultyNodes \* correct nodes in pNodes + CP == Cardinality(CN) \* power of the correct nodes + FP == Cardinality(FN) \* power of the faulty nodes + IN + \* CP + FP = TP is the total voting power + LET TP == CP + FP IN + FP * maxRatio[2] < TP * maxRatio[1] + +(* Can a block be produced by a correct peer, or an authenticated Byzantine peer *) +IsLightBlockAllowedByDigitalSignatures(ht, block) == + \/ block.header = blockchain[ht] \* signed by correct and faulty (maybe) + \/ /\ block.Commits \subseteq Faulty + /\ block.header.height = ht + /\ block.header.time >= 0 \* signed only by faulty + +(* + Initialize the blockchain to the ultimate height right in the initial states. + We pick the faulty validators statically, but that should not affect the light client. + + Parameters: + - pMaxFaultyRatioExclusive is a pair <> that bound the number of + faulty validators in each block by the ratio a / b (exclusive) + *) +InitToHeight(pMaxFaultyRatioExclusive) == + /\ Faulty \in SUBSET AllNodes \* some nodes may fail + \* pick the validator sets and last commits + /\ \E vs, lastCommit \in [Heights -> SUBSET AllNodes]: + \E timestamp \in [Heights -> Int]: + \* refClock is at least as early as the timestamp in the last block + /\ \E tm \in Int: refClock = tm /\ tm >= timestamp[ULTIMATE_HEIGHT] + \* the genesis starts on day 1 + /\ timestamp[1] = 1 + /\ vs[1] = AllNodes + /\ lastCommit[1] = EmptyNodeSet + /\ \A h \in Heights \ {1}: + /\ lastCommit[h] \subseteq vs[h - 1] \* the non-validators cannot commit + /\ TwoThirds(vs[h - 1], lastCommit[h]) \* the commit has >2/3 of validator votes + \* the faulty validators have the power below the threshold + /\ FaultyValidatorsFewerThan(Faulty, vs[h], pMaxFaultyRatioExclusive) + /\ timestamp[h] > timestamp[h - 1] \* the time grows monotonically + /\ timestamp[h] < timestamp[h - 1] + TRUSTING_PERIOD \* but not too fast + \* form the block chain out of validator sets and commits (this makes apalache faster) + /\ blockchain = [h \in Heights |-> + [height |-> h, + time |-> timestamp[h], + VS |-> vs[h], + NextVS |-> IF h < ULTIMATE_HEIGHT THEN vs[h + 1] ELSE AllNodes, + lastCommit |-> lastCommit[h]] + ] \****** + +(********************* BLOCKCHAIN ACTIONS ********************************) +(* + Advance the clock by zero or more time units. + *) +AdvanceTime == + /\ \E tm \in Int: tm >= refClock /\ refClock' = tm + /\ UNCHANGED <> + +============================================================================= +\* Modification History +\* Last modified Wed Jun 10 14:10:54 CEST 2020 by igor +\* Created Fri Oct 11 15:45:11 CEST 2019 by igor diff --git a/spec/light-client/detection/LCD_MC3_3_faulty.tla b/spec/light-client/detection/LCD_MC3_3_faulty.tla new file mode 100644 index 000000000..cef1df4d3 --- /dev/null +++ b/spec/light-client/detection/LCD_MC3_3_faulty.tla @@ -0,0 +1,27 @@ +------------------------- MODULE LCD_MC3_3_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 3 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +IS_SECONDARY_CORRECT == TRUE +FAULTY_RATIO == <<2, 3>> \* < 1 / 3 faulty validators + +VARIABLES + blockchain, (* the reference blockchain *) + localClock, (* current time in the light client *) + refClock, (* current time in the reference blockchain *) + Faulty, (* the set of faulty validators *) + state, (* the state of the light client detector *) + fetchedLightBlocks1, (* a function from heights to LightBlocks *) + fetchedLightBlocks2, (* a function from heights to LightBlocks *) + fetchedLightBlocks1b, (* a function from heights to LightBlocks *) + commonHeight, (* the height that is trusted in CreateEvidenceForPeer *) + nextHeightToTry, (* the index in CreateEvidenceForPeer *) + evidences + +INSTANCE LCDetector_003_draft +============================================================================ diff --git a/spec/light-client/detection/LCD_MC3_4_faulty.tla b/spec/light-client/detection/LCD_MC3_4_faulty.tla new file mode 100644 index 000000000..06bcdee13 --- /dev/null +++ b/spec/light-client/detection/LCD_MC3_4_faulty.tla @@ -0,0 +1,27 @@ +------------------------- MODULE LCD_MC3_4_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 4 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +IS_SECONDARY_CORRECT == TRUE +FAULTY_RATIO == <<2, 3>> \* < 1 / 3 faulty validators + +VARIABLES + blockchain, (* the reference blockchain *) + localClock, (* current time in the light client *) + refClock, (* current time in the reference blockchain *) + Faulty, (* the set of faulty validators *) + state, (* the state of the light client detector *) + fetchedLightBlocks1, (* a function from heights to LightBlocks *) + fetchedLightBlocks2, (* a function from heights to LightBlocks *) + fetchedLightBlocks1b, (* a function from heights to LightBlocks *) + commonHeight, (* the height that is trusted in CreateEvidenceForPeer *) + nextHeightToTry, (* the index in CreateEvidenceForPeer *) + evidences + +INSTANCE LCDetector_003_draft +============================================================================ diff --git a/spec/light-client/detection/LCD_MC4_4_faulty.tla b/spec/light-client/detection/LCD_MC4_4_faulty.tla new file mode 100644 index 000000000..fdb97d961 --- /dev/null +++ b/spec/light-client/detection/LCD_MC4_4_faulty.tla @@ -0,0 +1,27 @@ +------------------------- MODULE LCD_MC4_4_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 4 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +IS_SECONDARY_CORRECT == TRUE +FAULTY_RATIO == <<2, 3>> \* < 2 / 3 faulty validators + +VARIABLES + blockchain, (* the reference blockchain *) + localClock, (* current time in the light client *) + refClock, (* current time in the reference blockchain *) + Faulty, (* the set of faulty validators *) + state, (* the state of the light client detector *) + fetchedLightBlocks1, (* a function from heights to LightBlocks *) + fetchedLightBlocks2, (* a function from heights to LightBlocks *) + fetchedLightBlocks1b, (* a function from heights to LightBlocks *) + commonHeight, (* the height that is trusted in CreateEvidenceForPeer *) + nextHeightToTry, (* the index in CreateEvidenceForPeer *) + evidences + +INSTANCE LCDetector_003_draft +============================================================================ diff --git a/spec/light-client/detection/LCD_MC5_5_faulty.tla b/spec/light-client/detection/LCD_MC5_5_faulty.tla new file mode 100644 index 000000000..fdbd87b8b --- /dev/null +++ b/spec/light-client/detection/LCD_MC5_5_faulty.tla @@ -0,0 +1,27 @@ +------------------------- MODULE LCD_MC5_5_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4", "n5"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 5 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +IS_SECONDARY_CORRECT == TRUE +FAULTY_RATIO == <<2, 3>> \* < 1 / 3 faulty validators + +VARIABLES + blockchain, (* the reference blockchain *) + localClock, (* current time in the light client *) + refClock, (* current time in the reference blockchain *) + Faulty, (* the set of faulty validators *) + state, (* the state of the light client detector *) + fetchedLightBlocks1, (* a function from heights to LightBlocks *) + fetchedLightBlocks2, (* a function from heights to LightBlocks *) + fetchedLightBlocks1b, (* a function from heights to LightBlocks *) + commonHeight, (* the height that is trusted in CreateEvidenceForPeer *) + nextHeightToTry, (* the index in CreateEvidenceForPeer *) + evidences + +INSTANCE LCDetector_003_draft +============================================================================ diff --git a/spec/light-client/detection/LCDetector_003_draft.tla b/spec/light-client/detection/LCDetector_003_draft.tla new file mode 100644 index 000000000..351d77275 --- /dev/null +++ b/spec/light-client/detection/LCDetector_003_draft.tla @@ -0,0 +1,373 @@ +-------------------------- MODULE LCDetector_003_draft ----------------------------- +(** + * This is a specification of the light client detector module. + * It follows the English specification: + * + * https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/detection/detection_003_reviewed.md + * + * The assumptions made in this specification: + * + * - light client connects to one primary and one secondary peer + * + * - the light client has its own local clock that can drift from the reference clock + * within the envelope [refClock - CLOCK_DRIFT, refClock + CLOCK_DRIFT]. + * The local clock may increase as well as decrease in the the envelope + * (similar to clock synchronization). + * + * - the ratio of the faulty validators is set as the parameter. + * + * Igor Konnov, Josef Widder, 2020 + *) + +EXTENDS Integers + +\* the parameters of Light Client +CONSTANTS + AllNodes, + (* a set of all nodes that can act as validators (correct and faulty) *) + TRUSTED_HEIGHT, + (* an index of the block header that the light client trusts by social consensus *) + TARGET_HEIGHT, + (* an index of the block header that the light client tries to verify *) + TRUSTING_PERIOD, + (* the period within which the validators are trusted *) + CLOCK_DRIFT, + (* the assumed precision of the clock *) + REAL_CLOCK_DRIFT, + (* the actual clock drift, which under normal circumstances should not + be larger than CLOCK_DRIFT (otherwise, there will be a bug) *) + FAULTY_RATIO, + (* a pair <> that limits that ratio of faulty validator in the blockchain + from above (exclusive). Tendermint security model prescribes 1 / 3. *) + IS_PRIMARY_CORRECT, + IS_SECONDARY_CORRECT + +VARIABLES + blockchain, (* the reference blockchain *) + localClock, (* the local clock of the light client *) + refClock, (* the reference clock in the reference blockchain *) + Faulty, (* the set of faulty validators *) + state, (* the state of the light client detector *) + fetchedLightBlocks1, (* a function from heights to LightBlocks *) + fetchedLightBlocks2, (* a function from heights to LightBlocks *) + fetchedLightBlocks1b, (* a function from heights to LightBlocks *) + commonHeight, (* the height that is trusted in CreateEvidenceForPeer *) + nextHeightToTry, (* the index in CreateEvidenceForPeer *) + evidences (* a set of evidences *) + +vars == <> + +\* (old) type annotations in Apalache +a <: b == a + + +\* instantiate a reference chain +ULTIMATE_HEIGHT == TARGET_HEIGHT + 1 +BC == INSTANCE Blockchain_003_draft + WITH ULTIMATE_HEIGHT <- (TARGET_HEIGHT + 1) + +\* use the light client API +LC == INSTANCE LCVerificationApi_003_draft + +\* evidence type +ET == [peer |-> STRING, conflictingBlock |-> BC!LBT, commonHeight |-> Int] + +\* is the algorithm in the terminating state +IsTerminated == + state \in { <<"NoEvidence", "PRIMARY">>, + <<"NoEvidence", "SECONDARY">>, + <<"FaultyPeer", "PRIMARY">>, + <<"FaultyPeer", "SECONDARY">>, + <<"FoundEvidence", "PRIMARY">> } + + +(********************************* Initialization ******************************) + +\* initialization for the light blocks data structure +InitLightBlocks(lb, Heights) == + \* BC!LightBlocks is an infinite set, as time is not restricted. + \* Hence, we initialize the light blocks by picking the sets inside. + \E vs, nextVS, lastCommit, commit \in [Heights -> SUBSET AllNodes]: + \* although [Heights -> Int] is an infinite set, + \* Apalache needs just one instance of this set, so it does not complain. + \E timestamp \in [Heights -> Int]: + LET hdr(h) == + [height |-> h, + time |-> timestamp[h], + VS |-> vs[h], + NextVS |-> nextVS[h], + lastCommit |-> lastCommit[h]] + IN + LET lightHdr(h) == + [header |-> hdr(h), Commits |-> commit[h]] + IN + lb = [ h \in Heights |-> lightHdr(h) ] + +\* initialize the detector algorithm +Init == + \* initialize the blockchain to TARGET_HEIGHT + 1 + /\ BC!InitToHeight(FAULTY_RATIO) + /\ \E tm \in Int: + tm >= 0 /\ LC!IsLocalClockWithinDrift(tm, refClock) /\ localClock = tm + \* start with the secondary looking for evidence + /\ state = <<"Init", "SECONDARY">> /\ commonHeight = 0 /\ nextHeightToTry = 0 + /\ evidences = {} <: {ET} + \* Precompute a possible result of light client verification for the primary. + \* It is the input to the detection algorithm. + /\ \E Heights1 \in SUBSET(TRUSTED_HEIGHT..TARGET_HEIGHT): + /\ TRUSTED_HEIGHT \in Heights1 + /\ TARGET_HEIGHT \in Heights1 + /\ InitLightBlocks(fetchedLightBlocks1, Heights1) + \* As we have a non-deterministic scheduler, for every trace that has + \* an unverified block, there is a filtered trace that only has verified + \* blocks. This is a deep observation. + /\ LET status == [h \in Heights1 |-> "StateVerified"] IN + LC!VerifyToTargetPost(blockchain, IS_PRIMARY_CORRECT, + fetchedLightBlocks1, status, + TRUSTED_HEIGHT, TARGET_HEIGHT, "finishedSuccess") + \* initialize the other data structures to the default values + /\ LET trustedBlock == blockchain[TRUSTED_HEIGHT] + trustedLightBlock == [header |-> trustedBlock, Commits |-> AllNodes] + IN + /\ fetchedLightBlocks2 = [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock] + /\ fetchedLightBlocks1b = [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock] + + +(********************************* Transitions ******************************) + +\* a block should contain a copy of the block from the reference chain, +\* with a matching commit +CopyLightBlockFromChain(block, height) == + LET ref == blockchain[height] + lastCommit == + IF height < ULTIMATE_HEIGHT + THEN blockchain[height + 1].lastCommit + \* for the ultimate block, which we never use, + \* as ULTIMATE_HEIGHT = TARGET_HEIGHT + 1 + ELSE blockchain[height].VS + IN + block = [header |-> ref, Commits |-> lastCommit] + +\* Either the primary is correct and the block comes from the reference chain, +\* or the block is produced by a faulty primary. +\* +\* [LCV-FUNC-FETCH.1::TLA.1] +FetchLightBlockInto(isPeerCorrect, block, height) == + IF isPeerCorrect + THEN CopyLightBlockFromChain(block, height) + ELSE BC!IsLightBlockAllowedByDigitalSignatures(height, block) + + +(** + * Pick the next height, for which there is a block. + *) +PickNextHeight(fetchedBlocks, height) == + LET largerHeights == { h \in DOMAIN fetchedBlocks: h > height } IN + IF largerHeights = ({} <: {Int}) + THEN -1 + ELSE CHOOSE h \in largerHeights: + \A h2 \in largerHeights: h <= h2 + + +(** + * Check, whether the target header matches at the secondary and primary. + *) +CompareLast == + /\ state = <<"Init", "SECONDARY">> + \* fetch a block from the secondary: + \* non-deterministically pick a block that matches the constraints + /\ \E latest \in BC!LightBlocks: + \* for the moment, we ignore the possibility of a timeout when fetching a block + /\ FetchLightBlockInto(IS_SECONDARY_CORRECT, latest, TARGET_HEIGHT) + /\ IF latest.header = fetchedLightBlocks1[TARGET_HEIGHT].header + THEN \* if the headers match, CreateEvidence is not called + /\ state' = <<"NoEvidence", "SECONDARY">> + \* save the retrieved block for further analysis + /\ fetchedLightBlocks2' = + [h \in (DOMAIN fetchedLightBlocks2) \union {TARGET_HEIGHT} |-> + IF h = TARGET_HEIGHT THEN latest ELSE fetchedLightBlocks2[h]] + /\ UNCHANGED <> + ELSE \* prepare the parameters for CreateEvidence + /\ commonHeight' = TRUSTED_HEIGHT + /\ nextHeightToTry' = PickNextHeight(fetchedLightBlocks1, TRUSTED_HEIGHT) + /\ state' = IF nextHeightToTry' >= 0 + THEN <<"CreateEvidence", "SECONDARY">> + ELSE <<"FaultyPeer", "SECONDARY">> + /\ UNCHANGED fetchedLightBlocks2 + + /\ UNCHANGED <> + + +\* the actual loop in CreateEvidence +CreateEvidence(peer, isPeerCorrect, refBlocks, targetBlocks) == + /\ state = <<"CreateEvidence", peer>> + \* precompute a possible result of light client verification for the secondary + \* we have to introduce HeightRange, because Apalache can only handle a..b + \* for constant a and b + /\ LET HeightRange == { h \in TRUSTED_HEIGHT..TARGET_HEIGHT: + commonHeight <= h /\ h <= nextHeightToTry } IN + \E HeightsRange \in SUBSET(HeightRange): + /\ commonHeight \in HeightsRange /\ nextHeightToTry \in HeightsRange + /\ InitLightBlocks(targetBlocks, HeightsRange) + \* As we have a non-deterministic scheduler, for every trace that has + \* an unverified block, there is a filtered trace that only has verified + \* blocks. This is a deep observation. + /\ \E result \in {"finishedSuccess", "finishedFailure"}: + LET targetStatus == [h \in HeightsRange |-> "StateVerified"] IN + \* call VerifyToTarget for (commonHeight, nextHeightToTry). + /\ LC!VerifyToTargetPost(blockchain, isPeerCorrect, + targetBlocks, targetStatus, + commonHeight, nextHeightToTry, result) + \* case 1: the peer has failed (or the trusting period has expired) + /\ \/ /\ result /= "finishedSuccess" + /\ state' = <<"FaultyPeer", peer>> + /\ UNCHANGED <> + \* case 2: success + \/ /\ result = "finishedSuccess" + /\ LET block1 == refBlocks[nextHeightToTry] IN + LET block2 == targetBlocks[nextHeightToTry] IN + IF block1.header /= block2.header + THEN \* the target blocks do not match + /\ state' = <<"FoundEvidence", peer>> + /\ evidences' = evidences \union + {[peer |-> peer, + conflictingBlock |-> block1, + commonHeight |-> commonHeight]} + /\ UNCHANGED <> + ELSE \* the target blocks match + /\ nextHeightToTry' = PickNextHeight(refBlocks, nextHeightToTry) + /\ commonHeight' = nextHeightToTry + /\ state' = IF nextHeightToTry' >= 0 + THEN state + ELSE <<"NoEvidence", peer>> + /\ UNCHANGED evidences + +SwitchToPrimary == + /\ state = <<"FoundEvidence", "SECONDARY">> + /\ nextHeightToTry' = PickNextHeight(fetchedLightBlocks2, commonHeight) + /\ state' = <<"CreateEvidence", "PRIMARY">> + /\ UNCHANGED <> + + +CreateEvidenceForSecondary == + /\ CreateEvidence("SECONDARY", IS_SECONDARY_CORRECT, + fetchedLightBlocks1, fetchedLightBlocks2') + /\ UNCHANGED <> + +CreateEvidenceForPrimary == + /\ CreateEvidence("PRIMARY", IS_PRIMARY_CORRECT, + fetchedLightBlocks2, + fetchedLightBlocks1b') + /\ UNCHANGED <> + +(* + The local and global clocks can be updated. They can also drift from each other. + Note that the local clock can actually go backwards in time. + However, it still stays in the drift envelope + of [refClock - REAL_CLOCK_DRIFT, refClock + REAL_CLOCK_DRIFT]. + *) +AdvanceClocks == + /\ \E tm \in Int: + tm >= refClock /\ refClock' = tm + /\ \E tm \in Int: + /\ tm >= localClock + /\ LC!IsLocalClockWithinDrift(tm, refClock') + /\ localClock' = tm + +(** + Execute AttackDetector for one secondary. + + [LCD-FUNC-DETECTOR.2::LOOP.1] + *) +Next == + /\ AdvanceClocks + /\ \/ CompareLast + \/ CreateEvidenceForSecondary + \/ SwitchToPrimary + \/ CreateEvidenceForPrimary + + +\* simple invariants to see the progress of the detector +NeverNoEvidence == state[1] /= "NoEvidence" +NeverFoundEvidence == state[1] /= "FoundEvidence" +NeverFaultyPeer == state[1] /= "FaultyPeer" +NeverCreateEvidence == state[1] /= "CreateEvidence" + +NeverFoundEvidencePrimary == state /= <<"FoundEvidence", "PRIMARY">> + +NeverReachTargetHeight == nextHeightToTry < TARGET_HEIGHT + +EvidenceWhenFaultyInv == + (state[1] = "FoundEvidence") => (~IS_PRIMARY_CORRECT \/ ~IS_SECONDARY_CORRECT) + +NoEvidenceForCorrectInv == + IS_PRIMARY_CORRECT /\ IS_SECONDARY_CORRECT => evidences = {} <: {ET} + +(** + * If we find an evidence by peer A, peer B has ineded given us a corrupted + * header following the common height. Also, we have a verification trace by peer A. + *) +CommonHeightOnEvidenceInv == + \A e \in evidences: + LET conflicting == e.conflictingBlock IN + LET conflictingHeader == conflicting.header IN + \* the evidence by suspectingPeer can be verified by suspectingPeer in one step + LET SoundEvidence(suspectingPeer, peerBlocks) == + \/ e.peer /= suspectingPeer + \* the conflicting block from another peer verifies against the common height + \/ /\ "SUCCESS" = + LC!ValidAndVerifiedUntimed(peerBlocks[e.commonHeight], conflicting) + \* and the headers of the same height by the two peers do not match + /\ peerBlocks[conflictingHeader.height].header /= conflictingHeader + IN + /\ SoundEvidence("PRIMARY", fetchedLightBlocks1b) + /\ SoundEvidence("SECONDARY", fetchedLightBlocks2) + +(** + * If the light client does not find an evidence, + * then there is no attack on the light client. + *) +AccuracyInv == + (LC!InTrustingPeriodLocal(fetchedLightBlocks1[TARGET_HEIGHT].header) + /\ state = <<"NoEvidence", "SECONDARY">>) + => + (fetchedLightBlocks1[TARGET_HEIGHT].header = blockchain[TARGET_HEIGHT] + /\ fetchedLightBlocks2[TARGET_HEIGHT].header = blockchain[TARGET_HEIGHT]) + +(** + * The primary reports a corrupted block at the target height. If the secondary is + * correct and the algorithm has terminated, we should get the evidence. + * This property is violated due to clock drift. VerifyToTarget may fail with + * the correct secondary within the trusting period (due to clock drift, locally + * we think that we are outside of the trusting period). + *) +PrecisionInvGrayZone == + (/\ fetchedLightBlocks1[TARGET_HEIGHT].header /= blockchain[TARGET_HEIGHT] + /\ BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) + /\ IS_SECONDARY_CORRECT + /\ IsTerminated) + => + evidences /= {} <: {ET} + +(** + * The primary reports a corrupted block at the target height. If the secondary is + * correct and the algorithm has terminated, we should get the evidence. + * This invariant does not fail, as we are using the local clock to check the trusting + * period. + *) +PrecisionInvLocal == + (/\ fetchedLightBlocks1[TARGET_HEIGHT].header /= blockchain[TARGET_HEIGHT] + /\ LC!InTrustingPeriodLocalSurely(blockchain[TRUSTED_HEIGHT]) + /\ IS_SECONDARY_CORRECT + /\ IsTerminated) + => + evidences /= {} <: {ET} + +==================================================================================== diff --git a/spec/light-client/detection/LCVerificationApi_003_draft.tla b/spec/light-client/detection/LCVerificationApi_003_draft.tla new file mode 100644 index 000000000..909eab92b --- /dev/null +++ b/spec/light-client/detection/LCVerificationApi_003_draft.tla @@ -0,0 +1,192 @@ +-------------------- MODULE LCVerificationApi_003_draft -------------------------- +(** + * The common interface of the light client verification and detection. + *) +EXTENDS Integers, FiniteSets + +\* the parameters of Light Client +CONSTANTS + TRUSTING_PERIOD, + (* the period within which the validators are trusted *) + CLOCK_DRIFT, + (* the assumed precision of the clock *) + REAL_CLOCK_DRIFT, + (* the actual clock drift, which under normal circumstances should not + be larger than CLOCK_DRIFT (otherwise, there will be a bug) *) + FAULTY_RATIO + (* a pair <> that limits that ratio of faulty validator in the blockchain + from above (exclusive). Tendermint security model prescribes 1 / 3. *) + +VARIABLES + localClock (* current time as measured by the light client *) + +(* the header is still within the trusting period *) +InTrustingPeriodLocal(header) == + \* note that the assumption about the drift reduces the period of trust + localClock < header.time + TRUSTING_PERIOD - CLOCK_DRIFT + +(* the header is still within the trusting period, even if the clock can go backwards *) +InTrustingPeriodLocalSurely(header) == + \* note that the assumption about the drift reduces the period of trust + localClock < header.time + TRUSTING_PERIOD - 2 * CLOCK_DRIFT + +(* ensure that the local clock does not drift far away from the global clock *) +IsLocalClockWithinDrift(local, global) == + /\ global - REAL_CLOCK_DRIFT <= local + /\ local <= global + REAL_CLOCK_DRIFT + +(** + * Check that the commits in an untrusted block form 1/3 of the next validators + * in a trusted header. + *) +SignedByOneThirdOfTrusted(trusted, untrusted) == + LET TP == Cardinality(trusted.header.NextVS) + SP == Cardinality(untrusted.Commits \intersect trusted.header.NextVS) + IN + 3 * SP > TP + +(** + The first part of the precondition of ValidAndVerified, which does not take + the current time into account. + + [LCV-FUNC-VALID.1::TLA-PRE-UNTIMED.1] + *) +ValidAndVerifiedPreUntimed(trusted, untrusted) == + LET thdr == trusted.header + uhdr == untrusted.header + IN + /\ thdr.height < uhdr.height + \* the trusted block has been created earlier + /\ thdr.time < uhdr.time + /\ untrusted.Commits \subseteq uhdr.VS + /\ LET TP == Cardinality(uhdr.VS) + SP == Cardinality(untrusted.Commits) + IN + 3 * SP > 2 * TP + /\ thdr.height + 1 = uhdr.height => thdr.NextVS = uhdr.VS + (* As we do not have explicit hashes we ignore these three checks of the English spec: + + 1. "trusted.Commit is a commit is for the header trusted.Header, + i.e. it contains the correct hash of the header". + 2. untrusted.Validators = hash(untrusted.Header.Validators) + 3. untrusted.NextValidators = hash(untrusted.Header.NextValidators) + *) + +(** + Check the precondition of ValidAndVerified, including the time checks. + + [LCV-FUNC-VALID.1::TLA-PRE.1] + *) +ValidAndVerifiedPre(trusted, untrusted, checkFuture) == + LET thdr == trusted.header + uhdr == untrusted.header + IN + /\ InTrustingPeriodLocal(thdr) + \* The untrusted block is not from the future (modulo clock drift). + \* Do the check, if it is required. + /\ checkFuture => uhdr.time < localClock + CLOCK_DRIFT + /\ ValidAndVerifiedPreUntimed(trusted, untrusted) + + +(** + Check, whether an untrusted block is valid and verifiable w.r.t. a trusted header. + This test does take current time into account, but only looks at the block structure. + + [LCV-FUNC-VALID.1::TLA-UNTIMED.1] + *) +ValidAndVerifiedUntimed(trusted, untrusted) == + IF ~ValidAndVerifiedPreUntimed(trusted, untrusted) + THEN "INVALID" + ELSE IF untrusted.header.height = trusted.header.height + 1 + \/ SignedByOneThirdOfTrusted(trusted, untrusted) + THEN "SUCCESS" + ELSE "NOT_ENOUGH_TRUST" + +(** + Check, whether an untrusted block is valid and verifiable w.r.t. a trusted header. + + [LCV-FUNC-VALID.1::TLA.1] + *) +ValidAndVerified(trusted, untrusted, checkFuture) == + IF ~ValidAndVerifiedPre(trusted, untrusted, checkFuture) + THEN "INVALID" + ELSE IF ~InTrustingPeriodLocal(untrusted.header) + (* We leave the following test for the documentation purposes. + The implementation should do this test, as signature verification may be slow. + In the TLA+ specification, ValidAndVerified happens in no time. + *) + THEN "FAILED_TRUSTING_PERIOD" + ELSE IF untrusted.header.height = trusted.header.height + 1 + \/ SignedByOneThirdOfTrusted(trusted, untrusted) + THEN "SUCCESS" + ELSE "NOT_ENOUGH_TRUST" + + +(** + The invariant of the light store that is not related to the blockchain + *) +LightStoreInv(fetchedLightBlocks, lightBlockStatus) == + \A lh, rh \in DOMAIN fetchedLightBlocks: + \* for every pair of stored headers that have been verified + \/ lh >= rh + \/ lightBlockStatus[lh] /= "StateVerified" + \/ lightBlockStatus[rh] /= "StateVerified" + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh /\ lightBlockStatus[mh] = "StateVerified" + \* or the left header is outside the trusting period, so no guarantees + \/ LET lhdr == fetchedLightBlocks[lh] + rhdr == fetchedLightBlocks[rh] + IN + \* we can verify the right one using the left one + "SUCCESS" = ValidAndVerifiedUntimed(lhdr, rhdr) + +(** + Correctness states that all the obtained headers are exactly like in the blockchain. + + It is always the case that every verified header in LightStore was generated by + an instance of Tendermint consensus. + + [LCV-DIST-SAFE.1::CORRECTNESS-INV.1] + *) +CorrectnessInv(blockchain, fetchedLightBlocks, lightBlockStatus) == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] = "StateVerified" => + fetchedLightBlocks[h].header = blockchain[h] + +(** + * When the light client terminates, there are no failed blocks. + * (Otherwise, someone lied to us.) + *) +NoFailedBlocksOnSuccessInv(fetchedLightBlocks, lightBlockStatus) == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] /= "StateFailed" + +(** + The expected post-condition of VerifyToTarget. + *) +VerifyToTargetPost(blockchain, isPeerCorrect, + fetchedLightBlocks, lightBlockStatus, + trustedHeight, targetHeight, finalState) == + LET trustedHeader == fetchedLightBlocks[trustedHeight].header IN + \* The light client is not lying us on the trusted block. + \* It is straightforward to detect. + /\ lightBlockStatus[trustedHeight] = "StateVerified" + /\ trustedHeight \in DOMAIN fetchedLightBlocks + /\ trustedHeader = blockchain[trustedHeight] + \* the invariants we have found in the light client verification + \* there is a problem with trusting period + /\ isPeerCorrect + => CorrectnessInv(blockchain, fetchedLightBlocks, lightBlockStatus) + \* a correct peer should fail the light client, + \* if the trusted block is in the trusting period + /\ isPeerCorrect /\ InTrustingPeriodLocalSurely(trustedHeader) + => finalState = "finishedSuccess" + /\ finalState = "finishedSuccess" => + /\ lightBlockStatus[targetHeight] = "StateVerified" + /\ targetHeight \in DOMAIN fetchedLightBlocks + /\ NoFailedBlocksOnSuccessInv(fetchedLightBlocks, lightBlockStatus) + /\ LightStoreInv(fetchedLightBlocks, lightBlockStatus) + + +================================================================================== diff --git a/spec/light-client/detection/README.md b/spec/light-client/detection/README.md new file mode 100644 index 000000000..50d8ab6fe --- /dev/null +++ b/spec/light-client/detection/README.md @@ -0,0 +1,75 @@ +--- +order: 1 +parent: + title: Fork Detection + order: 2 +--- + +# Tendermint fork detection and IBC fork detection + +## Status + +This is a work in progress. +This directory captures the ongoing work and discussion on fork +detection both in the context of a Tendermint light node and in the +context of IBC. It contains the following files + +### [detection.md](./detection.md) + +a draft of the light node fork detection including "proof of fork" + definition, that is, the data structure to submit evidence to full + nodes. + +### [discussions.md](./discussions.md) + +A collection of ideas and intuitions from recent discussions + +- the outcome of recent discussion +- a sketch of the light client supervisor to provide the context in + which fork detection happens +- a discussion about lightstore semantics + +### [req-ibc-detection.md](./req-ibc-detection.md) + +- a collection of requirements for fork detection in the IBC + context. In particular it contains a section "Required Changes in + ICS 007" with necessary updates to ICS 007 to support Tendermint + fork detection + +### [draft-functions.md](./draft-functions.md) + +In order to address the collected requirements, we started to sketch +some functions that we will need in the future when we specify in more +detail the + +- fork detections +- proof of fork generation +- proof of fork verification + +on the following components. + +- IBC on-chain components +- Relayer + +### TODOs + +We decided to merge the files while there are still open points to +address to record the current state an move forward. In particular, +the following points need to be addressed: + +- + +- + +- + +- + +Most likely we will write a specification on the light client +supervisor along the outcomes of + +- + +that also addresses initialization + +- diff --git a/spec/light-client/detection/detection_001_reviewed.md b/spec/light-client/detection/detection_001_reviewed.md new file mode 100644 index 000000000..52a1e41d4 --- /dev/null +++ b/spec/light-client/detection/detection_001_reviewed.md @@ -0,0 +1,788 @@ +# ***This an unfinished draft. Comments are welcome!*** + +**TODO:** We will need to do small adaptations to the verification +spec to reflect the semantics in the LightStore (verified, trusted, +untrusted, etc. not needed anymore). In more detail: + +- The state of the Lightstore needs to go. Functions like `LatestVerified` can +keep the name but will ignore state as it will not exist anymore. + +- verification spec should be adapted to the second parameter of +`VerifyToTarget` +being a lightblock; new version number of function tag; + +- We should clarify what is the expectation of VerifyToTarget +so if it returns TimeoutError it can be assumed faulty. I guess that +VerifyToTarget with correct full node should never terminate with +TimeoutError. + +- We need to introduce a new version number for the new +specification. So we should decide how + to handle that. + +# Light Client Attack Detector + +In this specification, we strengthen the light client to be resistant +against so-called light client attacks. In a light client attack, all +the correct Tendermint full nodes agree on the sequence of generated +blocks (no fork), but a set of faulty full nodes attack a light client +by generating (signing) a block that deviates from the block of the +same height on the blockchain. In order to do so, some of these faulty +full nodes must have been validators before and violate +[[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link), as otherwise, if +[[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link) would hold, +[verification](verification) would satisfy +[[LCV-SEQ-SAFE.1]](LCV-SEQ-SAFE-link). + +An attack detector (or detector for short) is a mechanism that is used +by the light client [supervisor](supervisor) after +[verification](verification) of a new light block +with the primary, to cross-check the newly learned light block with +other peers (secondaries). It expects as input a light block with some +height *root* (that serves as a root of trust), and a verification +trace (a sequence of lightblocks) that the primary provided. + +In case the detector observes a light client attack, it computes +evidence data that can be used by Tendermint full nodes to isolate a +set of faulty full nodes that are still within the unbonding period +(more than 1/3 of the voting power of the validator set at some block of the chain), +and report them via ABCI to the application of a Tendermint blockchain +in order to punish faulty nodes. + +## Context of this document + +The light client [verification](verification) specification is +designed for the Tendermint failure model (1/3 assumption) +[[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link). It is safe under this +assumption, and live if it can reliably (that is, no message loss, no +duplication, and eventually delivered) and timely communicate with a +correct full node. If [[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link) assumption is violated, the light client +can be fooled to trust a light block that was not generated by +Tendermint consensus. + +This specification, the attack detector, is a "second line of +defense", in case the 1/3 assumption is violated. Its goal is to +detect a light client attack (conflicting light blocks) and collect +evidence. However, it is impractical to probe all full nodes. At this +time we consider a simple scheme of maintaining an address book of +known full nodes from which a small subset (e.g., 4) are chosen +initially to communicate with. More involved book keeping with +probabilistic guarantees can be considered at later stages of the +project. + +The light client maintains a simple address book containing addresses +of full nodes that it can pick as primary and secondaries. To obtain +a new light block, the light client first does +[verification](verification) with the primary, and then cross-checks +the light block (and the trace of light blocks that led to it) with +the secondaries using this specification. + +## Tendermint Consensus and Light Client Attacks + +In this section we will give some mathematical definitions of what we +mean by light client attacks (that are considered in this +specification) and how they differ from main-chain forks. To this end +we start by defining some properties of the sequence of blocks that is +decided upon by Tendermint consensus in normal operation (if the +Tendermint failure model holds +[[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link)), +and then define different +deviations that correspond to attack scenarios. + +#### **[TMBC-GENESIS.1]** + +Let *Genesis* be the agreed-upon initial block (file). + +#### **[TMBC-FUNC-SIGN.1]** + +Let *b* and *c* be two light blocks with *b.Header.Height + 1 = +c.Header.Height*. We define the predicate **signs(b,c)** to hold +iff *c.Header.LastCommit* is in *PossibleCommit(b)*. +[[TMBC-SOUND-DISTR-POSS-COMMIT.1]](TMBC-SOUND-DISTR-POSS-COMMIT-link). + +> The above encodes sequential verification, that is, intuitively, +> b.Header.NextValidators = c.Header.Validators and 2/3 of +> these Validators signed c? + +#### **[TMBC-FUNC-SUPPORT.1]** + +Let *b* and *c* be two light blocks. We define the predicate +**supports(b,c,t)** to hold iff + +- *t - trustingPeriod < b.Header.Time < t* +- the voting power in *b.NextValidators* of nodes in *c.Commit* + is more than 1/3 of *TotalVotingPower(b.Header.NextValidators)* + +> That is, if the [Tendermint failure model](TMBC-FM-2THIRDS-link) +> holds, then *c* has been signed by at least one correct full node, cf. +> [[TMBC-VAL-CONTAINS-CORR.1]](TMBC-VAL-CONTAINS-CORR-link). +> The following formalizes that *b* was properly generated by +> Tendermint; *b* can be traced back to genesis + +#### **[TMBC-SEQ-ROOTED.1]** + +Let *b* be a light block. +We define *sequ-rooted(b)* iff for all *i*, *1 <= i < h = b.Header.Height*, +there exist light blocks *a(i)* s.t. + +- *a(1) = Genesis* and +- *a(h) = b* and +- *signs( a(i) , a(i+1) )*. + +> The following formalizes that *c* is trusted based on *b* in +> skipping verification. Observe that we do not require here (yet) +> that *b* was properly generated. + +#### **[TMBC-SKIP-TRACE.1]** + +Let *b* and *c* be light blocks. We define *skip-trace(b,c,t)* if at +time t there exists an *h* and a sequence *a(1)*, ... *a(h)* s.t. + +- *a(1) = b* and +- *a(h) = c* and +- *supports( a(i), a(i+1), t)*, for all i, *1 <= i < h*. + +We call such a sequence *a(1)*, ... *a(h)* a **verification trace**. + +> The following formalizes that two light blocks of the same height +> should agree on the content of the header. Observe that *b* and *c* +> may disagree on the Commit. This is a special case if the canonical +> commit has not been decided on, that is, if b.Header.Height is the +> maximum height of all blocks decided upon by Tendermint at this +> moment. + +#### **[TMBC-SIGN-SKIP-MATCH.1]** + +Let *a*, *b*, *c*, be light blocks and *t* a time, we define +*sign-skip-match(a,b,c,t) = true* iff the following implication +evaluates to true: + +- *sequ-rooted(a)* and +- *b.Header.Height = c.Header.Height* and +- *skip-trace(a,b,t)* +- *skip-trace(a,c,t)* + +implies *b.Header = c.Header*. + +> Observe that *sign-skip-match* is defined via an implication. If it +> evaluates to false this means that the left-hand-side of the +> implication evaluates to true, and the right-hand-side evaluates to +> false. In particular, there are two **different** headers *b* and +> *c* that both can be verified from a common block *a* from the +> chain. Thus, the following describes an attack. + +#### **[TMBC-ATTACK.1]** + +If there exists three light blocks a, b, and c, with +*sign-skip-match(a,b,c,t) = false* then we have an *attack*. We say +we have **an attack at height** *b.Header.Height* and write +*attack(a,b,c,t)*. + +> The lightblock *a* need not be unique, that is, there may be +> several blocks that satisfy the above requirement for the same +> blocks *b* and *c*. + +[[TMBC-ATTACK.1]](#TMBC-ATTACK1) is a formalization of the violation +of the agreement property based on the result of consensus, that is, +the generated blocks. + +**Remark.** +Violation of agreement is only possible if more than 1/3 of the validators (or +next validators) of some previous block deviated from the protocol. The +upcoming "accountability" specification will describe how to compute +a set of at least 1/3 faulty nodes from two conflicting blocks. [] + +There are different ways to characterize forks +and attack scenarios. This specification uses the "node-based +characterization of attacks" which focuses on what kinds of nodes are +affected (light nodes vs. full nodes). For future reference and +discussion we also provide a +"block-based characterization of attacks" below. + +### Node-based characterization of attacks + +#### **[TMBC-MC-FORK.1]** + +We say there is a (main chain) fork at time *t* if + +- there are two correct full nodes *i* and *j* and +- *i* is different from *j* and +- *i* has decided on *b* and +- *j* has decided on *c* and +- there exist *a* such that *attack(a,b,c,t)*. + +#### **[TMBC-LC-ATTACK.1]** + +We say there is a light client attack at time *t*, if + +- there is **no** (main chain) fork [[TMBC-MC-FORK.1]](#TMBC-MC-FORK1), and +- there exist nodes that have computed light blocks *b* and *c* and +- there exist *a* such that *attack(a,b,c,t)*. + +We say the attack is at height *a.Header.Height*. + +> In this specification we consider detection of light client +> attacks. Intuitively, the case we consider is that +> light block *b* is the one from the +> blockchain, and some attacker has computed *c* and tries to wrongly +> convince +> the light client that *c* is the block from the chain. + +#### **[TMBC-LC-ATTACK-EVIDENCE.1]** + +We consider the following case of a light client attack +[[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1): + +- *attack(a,b,c,t)* +- there is a peer p1 that has a sequence *chain* of blocks from *a* to *b* +- *skip-trace(a,c,t)*: by [[TMBC-SKIP-TRACE.1]](#TMBC-SKIP-TRACE1) there is a + verification trace *v* of the form *a = v(1)*, ... *v(h) = c* + +Evidence for p1 (that proves an attack) consists for index i +of v(i) and v(i+1) such that + +- E1(i). v(i) is equal to the block of *chain* at height v(i).Height, and +- E2(i). v(i+1) that is different from the block of *chain* at + height v(i+1).height + +> Observe p1 can +> +> - check that v(i+1) differs from its block at that height, and +> - verify v(i+1) in one step from v(i) as v is a verification trace. + +**Proposition.** In the case of attack, evidence exists. +*Proof.* First observe that + +- (A). (NOT E2(i)) implies E1(i+1) + +Now by contradiction assume there is no evidence. Thus + +- for all i, we have NOT E1(i) or NOT E2(i) +- for i = 1 we have E1(1) and thus NOT E2(1) + thus by induction on i, by (A) we have for all i that **E1(i)** +- from attack we have E2(h-1), and as there is no evidence for + i = h - 1 we get **NOT E1(h-1)**. Contradiction. +QED. + +#### **[TMBC-LC-EVIDENCE-DATA.1]** + +To prove the attack to p1, because of Point E1, it is sufficient to +submit + +- v(i).Height (rather than v(i)). +- v(i+1) + +This information is *evidence for height v(i).Height*. + +### Block-based characterization of attacks + +In this section we provide a different characterization of attacks. It +is not defined on the nodes that are affected but purely on the +content of the blocks. In that sense these definitions are less +operational. + +> They might be relevant for a closer analysis of fork scenarios on the +> chain, which is out of the scope of this specification. + +#### **[TMBC-SIGN-UNIQUE.1]** + +Let *b* and *c* be light blocks, we define the predicate +*sign-unique(b,c)* to evaluate to true iff the following implication +evaluates to true: + +- *b.Header.Height = c.Header.Height* and +- *sequ-rooted(b)* and +- *sequ-rooted(c)* + +implies *b = c*. + +#### **[TMBC-BLOCKS-MCFORK.1]** + +If there exists two light blocks b and c, with *sign-unique(b,c) = +false* then we have a *fork*. + +> The difference of the above definition to +> [[TMBC-MC-FORK.1]](#TMBC-MC-FORK1) is subtle. The latter requires a +> full node being affected by a bad block while +> [[TMBC-BLOCKS-MCFORK.1]](#TMBC-BLOCKS-MCFORK1) just requires that a +> bad block exists, possibly in memory of an attacker. +> The following captures a light client fork. There is no fork up to +> the height of block b. However, c is of that height, is different, +> and passes skipping verification. It is a stricter property than +> [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1), as +> [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1) requires that no correct full +> node is affected. + +#### **[TMBC-BLOCKS-LCFORK.1]** + +Let *a*, *b*, *c*, be light blocks and *t* a time. We define +*light-client-fork(a,b,c,t)* iff + +- *sign-skip-match(a,b,c,t) = false* and +- *sequ-rooted(b)* and +- *b* is "unique", that is, for all *d*, *sequ-rooted(d)* and + *d.Header.Height = b.Header.Height* implies *d = b* + +> Finally, let us also define bogus blocks that have no support. +> Observe that bogus is even defined if there is a fork. +> Also, for the definition it would be sufficient to restrict *a* to +> *a.height < b.height* (which is implied by the definitions which +> unfold until *supports()*). + +#### **[TMBC-BOGUS.1]** + +Let *b* be a light block and *t* a time. We define *bogus(b,t)* iff + +- *sequ-rooted(b) = false* and +- for all *a*, *sequ-rooted(a)* implies *skip-trace(a,b,t) = false* + +### Informal Problem statement + +There is no sequential specification: the detector only makes sense +in a distributed systems where some nodes misbehave. + +We work under the assumption that full nodes and validators are +responsible for detecting attacks on the main chain, and the evidence +reactor takes care of broadcasting evidence to communicate +misbehaving nodes via ABCI to the application, and halt the chain in +case of a fork. The point of this specification is to shield a light +clients against attacks that cannot be detected by full nodes, and +are fully addressed at light clients (and consequently IBC relayers, +which use the light client protocols to observe the state of a +blockchain). In order to provide full nodes the incentive to follow +the protocols when communicating with the light client, this +specification also considers the generation of evidence that will +also be processed by the Tendermint blockchain. + +#### **[LCD-IP-MODEL.1]** + +The detector is designed under the assumption that + +- [[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link) may be violated +- there is no fork on the main chain. + +> As a result some faulty full nodes may launch an attack on a light +> client. + +The following requirements are operational in that they describe how +things should be done, rather than what should be done. However, they +do not constitute temporal logic verification conditions. For those, +see [LCD-DIST-*] below. + +The detector is called in the [supervisor](supervisor) as follows + +```go +Evidences := AttackDetector(root_of_trust, verifiedLS);` +``` + +where + +- `root-of-trust` is a light block that is trusted (that is, +except upon initialization, the primary and the secondaries +agreed on in the past), and +- `verifiedLS` is a lightstore that contains a verification trace that + starts from a lightblock that can be verified with the + `root-of-trust` in one step and ends with a lightblock of the height + requested by the user +- `Evidences` is a list of evidences for misbehavior + +#### **[LCD-IP-STATEMENT.1]** + +Whenever AttackDetector is called, the detector should for each +secondary try to replay the verification trace `verifiedLS` with the +secondary + +- in case replaying leads to detection of a light client attack + (one of the lightblocks differ from the one in verifiedLS with + the same height), we should return evidence +- if the secondary cannot provide a verification trace, we have no + proof for an attack. Block *b* may be bogus. In this case the + secondary is faulty and it should be replaced. + +## Assumptions/Incentives/Environment + +It is not in the interest of faulty full nodes to talk to the +detector as long as the detector is connected to at least one +correct full node. This would only increase the likelihood of +misbehavior being detected. Also we cannot punish them easily +(cheaply). The absence of a response need not be the fault of the full +node. + +Correct full nodes have the incentive to respond, because the +detector may help them to understand whether their header is a good +one. We can thus base liveness arguments of the detector on +the assumptions that correct full nodes reliably talk to the +detector. + +### Assumptions + +#### **[LCD-A-CorrFull.1]** + +At all times there is at least one correct full +node among the primary and the secondaries. + +> For this version of the detection we take this assumption. It +> allows us to establish the invariant that the lightblock +> `root-of-trust` is always the one from the blockchain, and we can +> use it as starting point for the evidence computation. Moreover, it +> allows us to establish the invariant at the supervisor that any +> lightblock in the (top-level) lightstore is from the blockchain. +> In the future we might design a lightclient based on the assumption +> that at least in regular intervals the lightclient is connected to a +> correct full node. This will require the detector to reconsider +> `root-of-trust`, and remove lightblocks from the top-level +> lightstore. + +#### **[LCD-A-RelComm.1]** + +Communication between the detector and a correct full node is +reliable and bounded in time. Reliable communication means that +messages are not lost, not duplicated, and eventually delivered. There +is a (known) end-to-end delay *Delta*, such that if a message is sent +at time *t* then it is received and processed by time *t + Delta*. +This implies that we need a timeout of at least *2 Delta* for remote +procedure calls to ensure that the response of a correct peer arrives +before the timeout expires. + +## Definitions + +### Evidence + +Following the definition of +[[TMBC-LC-ATTACK-EVIDENCE.1]](#TMBC-LC-ATTACK-EVIDENCE1), by evidence +we refer to a variable of the following type + +#### **[LC-DATA-EVIDENCE.1]** + +```go +type LightClientAttackEvidence struct { + ConflictingBlock LightBlock + CommonHeight int64 +} +``` + +As the above data is computed for a specific peer, the following +data structure wraps the evidence and adds the peerID. + +#### **[LC-DATA-EVIDENCE-INT.1]** + +```go +type InternalEvidence struct { + Evidence LightClientAttackEvidence + Peer PeerID +} +``` + +#### **[LC-SUMBIT-EVIDENCE.1]** + +```go +func submitEvidence(Evidences []InternalEvidence) +``` + +- Expected postcondition + - for each `ev` in `Evidences`: submit `ev.Evidence` to `ev.Peer` + +--- + +### LightStore + +Lightblocks and LightStores are defined in the verification +specification [LCV-DATA-LIGHTBLOCK.1] and [LCV-DATA-LIGHTSTORE.1]. See +the [verification specification][verification] for details. + +## (Distributed) Problem statement + +> As the attack detector is there to reduce the impact of faulty +> nodes, and faulty nodes imply that there is a distributed system, +> there is no sequential specification to which this distributed +> problem statement may refer to. + +The detector gets as input a trusted lightblock called *root* and an +auxiliary lightstore called *primary_trace* with lightblocks that have +been verified before, and that were provided by the primary. + +#### **[LCD-DIST-INV-ATTACK.1]** + +If the detector returns evidence for height *h* +[[TMBC-LC-EVIDENCE-DATA.1]](#TMBC-LC-EVIDENCE-DATA1), then there is an +attack at height *h*. [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1) + +#### **[LCD-DIST-INV-STORE.1]** + +If the detector does not return evidence, then *primary_trace* +contains only blocks from the blockchain. + +#### **[LCD-DIST-LIVE.1]** + +The detector eventually terminates. + +#### **[LCD-DIST-TERM-NORMAL.1]** + +If + +- the *primary_trace* contains only blocks from the blockchain, and +- there is no attack, and +- *Secondaries* is always non-empty, and +- the age of *root* is always less than the trusting period, + +then the detector does not return evidence. + +#### **[LCD-DIST-TERM-ATTACK.1]** + +If + +- there is an attack, and +- a secondary reports a block that conflicts + with one of the blocks in *primary_trace*, and +- *Secondaries* is always non-empty, and +- the age of *root* is always less than the trusting period, + +then the detector returns evidence. + +> Observe that above we require that "a secondary reports a block that +> conflicts". If there is an attack, but no secondary tries to launch +> it against the detector (or the message from the secondary is lost +> by the network), then there is nothing to detect for us. + +#### **[LCD-DIST-SAFE-SECONDARY.1]** + +No correct secondary is ever replaced. + +#### **[LCD-DIST-SAFE-BOGUS.1]** + +If + +- a secondary reports a bogus lightblock, +- the age of *root* is always less than the trusting period, + +then the secondary is replaced before the detector terminates. + +> The above property is quite operational ("reports"), but it captures +> quite closely the requirement. As the +> detector only makes sense in a distributed setting, and does +> not have a sequential specification, less "pure" +> specification are acceptable. + +# Protocol + +## Functions and Data defined in other Specifications + +### From the supervisor + +```go +Replace_Secondary(addr Address, root-of-trust LightBlock) +``` + +### From the verifier + +```go +func VerifyToTarget(primary PeerID, root LightBlock, + targetHeight Height) (LightStore, Result) +``` + +> Note: the above differs from the current version in the second +> parameter. verification will be revised. + +Observe that `VerifyToTarget` does communication with the secondaries +via the function [FetchLightBlock](fetch). + +### Shared data of the light client + +- a pool of full nodes *FullNodes* that have not been contacted before +- peer set called *Secondaries* +- primary + +> Note that the lightStore is not needed to be shared. + +## Outline + +The problem laid out is solved by calling the function `AttackDetector` +with a lightstore that contains a light block that has just been +verified by the verifier. + +Then `AttackDetector` downloads headers from the secondaries. In case +a conflicting header is downloaded from a secondary, +`CreateEvidenceForPeer` which computes evidence in the case that +indeed an attack is confirmed. It could be that the secondary reports +a bogus block, which means that there need not be an attack, and the +secondary is replaced. + +## Details of the functions + +#### **[LCD-FUNC-DETECTOR.1]:** + +```go +func AttackDetector(root LightBlock, primary_trace []LightBlock) + ([]InternalEvidence) { + + Evidences := new []InternalEvidence; + + for each secondary in Secondaries { + // we replay the primary trace with the secondary, in + // order to generate evidence that we can submit to the + // secodary. We return the evidence + the trace the + // secondary told us that spans the evidence at its local store + + EvidenceForSecondary, newroot, secondary_trace, result := + CreateEvidenceForPeer(secondary, + root, + primary_trace); + if result == FaultyPeer { + Replace_Secondary(root); + } + else if result == FoundEvidence { + // the conflict is not bogus + Evidences.Add(EvidenceForSecondary); + // we replay the secondary trace with the primary, ... + EvidenceForPrimary, _, result := + CreateEvidenceForPeer(primary, + newroot, + secondary_trace); + if result == FoundEvidence { + Evidences.Add(EvidenceForPrimary); + } + // At this point we do not care about the other error + // codes. We already have generated evidence for an + // attack and need to stop the lightclient. It does not + // help to call replace_primary. Also we will use the + // same primary to check with other secondaries in + // later iterations of the loop + } + // In the case where the secondary reports NoEvidence + // we do nothing + } + return Evidences; +} +``` + +- Expected precondition + - root and primary trace are a verification trace +- Expected postcondition + - solves the problem statement (if attack found, then evidence is reported) +- Error condition + - `ErrorTrustExpired`: fails if root expires (outside trusting + period) [[LCV-INV-TP.1]](LCV-INV-TP1-link) + - `ErrorNoPeers`: if no peers are left to replace secondaries, and + no evidence was found before that happened + +--- + +```go +func CreateEvidenceForPeer(peer PeerID, root LightBlock, trace LightStore) + (Evidence, LightBlock, LightStore, result) { + + common := root; + + for i in 1 .. len(trace) { + auxLS, result := VerifyToTarget(peer, common, trace[i].Header.Height) + + if result != ResultSuccess { + // something went wrong; peer did not provide a verifyable block + return (nil, nil, nil, FaultyPeer) + } + else { + if auxLS.LatestVerified().Header != trace[i].Header { + // the header reported by the peer differs from the + // reference header in trace but both could be + // verified from common in one step. + // we can create evidence for submission to the secondary + ev := new InternalEvidence; + ev.Evidence.ConflictingBlock := trace[i]; + ev.Evidence.CommonHeight := common.Height; + ev.Peer := peer + return (ev, common, auxLS, FoundEvidence) + } + else { + // the peer agrees with the trace, we move common forward + // we could delete auxLS as it will be overwritten in + // the next iteration + common := trace[i] + } + } + } + return (nil, nil, nil, NoEvidence) +} +``` + +- Expected precondition + - root and trace are a verification trace +- Expected postcondition + - finds evidence where trace and peer diverge +- Error condition + - `ErrorTrustExpired`: fails if root expires (outside trusting + period) [[LCV-INV-TP.1]](LCV-INV-TP1-link) + - If `VerifyToTarget` returns error but root is not expired then return + `FaultyPeer` + +--- + +## Correctness arguments + +#### Argument for [[LCD-DIST-INV-ATTACK.1]](#LCD-DIST-INV-ATTACK1) + +Under the assumption that root and trace are a verification trace, +when in `CreateEvidenceForPeer` the detector the detector creates +evidence, then the lightclient has seen two different headers (one via +`trace` and one via `VerifyToTarget` for the same height that can both +be verified in one step. + +#### Argument for [[LCD-DIST-INV-STORE.1]](#LCD-DIST-INV-STORE1) + +We assume that there is at least one correct peer, and there is no +fork. As a result the correct peer has the correct sequence of +blocks. Since the primary_trace is checked block-by-block also against +each secondary, and at no point evidence was generated that means at +no point there were conflicting blocks. + +#### Argument for [[LCD-DIST-LIVE.1]](#LCD-DIST-LIVE1) + +At the latest when [[LCV-INV-TP.1]](LCV-INV-TP1-link) is violated, +`AttackDetector` terminates. + +#### Argument for [[LCD-DIST-TERM-NORMAL.1]](#LCD-DIST-TERM-NORMAL1) + +As there are finitely many peers, eventually the main loop +terminates. As there is no attack no evidence can be generated. + +#### Argument for [[LCD-DIST-TERM-ATTACK.1]](#LCD-DIST-TERM-ATTACK1) + +Argument similar to [[LCD-DIST-TERM-NORMAL.1]](#LCD-DIST-TERM-NORMAL1) + +#### Argument for [[LCD-DIST-SAFE-SECONDARY.1]](#LCD-DIST-SAFE-SECONDARY1) + +Secondaries are only replaced if they time-out or if they report bogus +blocks. The former is ruled out by the timing assumption, the latter +by correct peers only reporting blocks from the chain. + +#### Argument for [[LCD-DIST-SAFE-BOGUS.1]](#LCD-DIST-SAFE-BOGUS1) + +Once a bogus block is recognized as such the secondary is removed. + +# References + +> links to other specifications/ADRs this document refers to + +[[verification]] The specification of the light client verification. + +[[supervisor]] The specification of the light client supervisor. + +[verification]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification.md + +[supervisor]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/supervisor/supervisor.md + +[block]: https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md + +[TMBC-FM-2THIRDS-link]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification.md#tmbc-fm-2thirds1 + +[TMBC-SOUND-DISTR-POSS-COMMIT-link]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification.md#tmbc-sound-distr-poss-commit1 + +[LCV-SEQ-SAFE-link]:https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification.md#lcv-seq-safe1 + +[TMBC-VAL-CONTAINS-CORR-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification.md#tmbc-val-contains-corr1 + +[fetch]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification.md#lcv-func-fetch1 + +[LCV-INV-TP1-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification.md#lcv-inv-tp1 diff --git a/spec/light-client/detection/detection_003_reviewed.md b/spec/light-client/detection/detection_003_reviewed.md new file mode 100644 index 000000000..4cd65c1c3 --- /dev/null +++ b/spec/light-client/detection/detection_003_reviewed.md @@ -0,0 +1,839 @@ +# Light Client Attack Detector + +In this specification, we strengthen the light client to be resistant +against so-called light client attacks. In a light client attack, all +the correct Tendermint full nodes agree on the sequence of generated +blocks (no fork), but a set of faulty full nodes attack a light client +by generating (signing) a block that deviates from the block of the +same height on the blockchain. In order to do so, some of these faulty +full nodes must have been validators before and violate the assumption +of more than two thirds of "correct voting power" +[[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link], as otherwise, if +[[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link] would hold, +[verification][verification] would satisfy +[[LCV-SEQ-SAFE.1]][LCV-SEQ-SAFE-link]. + +An attack detector (or detector for short) is a mechanism that is used +by the light client [supervisor][supervisor] after +[verification][verification] of a new light block +with the primary, to cross-check the newly learned light block with +other peers (secondaries). It expects as input a light block with some +height *root* (that serves as a root of trust), and a verification +trace (a sequence of lightblocks) that the primary provided. + +In case the detector observes a light client attack, it computes +evidence data that can be used by Tendermint full nodes to isolate a +set of faulty full nodes that are still within the unbonding period +(more than 1/3 of the voting power of the validator set at some block +of the chain), and report them via ABCI (application/blockchain +interface) +to the application of a +Tendermint blockchain in order to punish faulty nodes. + +## Context of this document + +The light client [verification][verification] specification is +designed for the Tendermint failure model (1/3 assumption) +[[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link]. It is safe under this +assumption, and live if it can reliably (that is, no message loss, no +duplication, and eventually delivered) and timely communicate with a +correct full node. If [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link] +assumption is violated, the light client can be fooled to trust a +light block that was not generated by Tendermint consensus. + +This specification, the attack detector, is a "second line of +defense", in case the 1/3 assumption is violated. Its goal is to +detect a light client attack (conflicting light blocks) and collect +evidence. However, it is impractical to probe all full nodes. At this +time we consider a simple scheme of maintaining an address book of +known full nodes from which a small subset (e.g., 4) are chosen +initially to communicate with. More involved book keeping with +probabilistic guarantees can be considered at later stages of the +project. + +The light client maintains a simple address book containing addresses +of full nodes that it can pick as primary and secondaries. To obtain +a new light block, the light client first does +[verification][verification] with the primary, and then cross-checks +the light block (and the trace of light blocks that led to it) with +the secondaries using this specification. + +# Outline + +- [Part I](#part-i---Tendermint-Consensus-and-Light-Client-Attacks): + Formal definitions of lightclient attacks, based on basic + properties of Tendermint consensus. + - [Node-based characterization of + attacks](#Node-based-characterization-of-attacks). The + definition of attacks used in the problem statement of + this specification. + + - [Block-based characterization of attacks](#Block-based-characterization-of-attacks). Alternative definitions + provided for future reference. + +- [Part II](#part-ii---problem-statement): Problem statement of + lightclient attack detection + + - [Informal Problem Statement](#informal-problem-statement) + - [Assumptions](#Assumptions) + - [Definitions](#definitions) + - [Distributed Problem statement](#Distributed-Problem-statement) + +- [Part III](#part-iii---protocol): The protocol + + - [Functions and Data defined in other Specifications](#Functions-and-Data-defined-in-other-Specifications) + - [Outline of Solution](#Outline-of-solution) + - [Details of the functions](#Details-of-the-functions) + - [Correctness arguments](#Correctness-arguments) + +# Part I - Tendermint Consensus and Light Client Attacks + +In this section we will give some mathematical definitions of what we +mean by light client attacks (that are considered in this +specification) and how they differ from main-chain forks. To this end, +we start by defining some properties of the sequence of blocks that is +decided upon by Tendermint consensus in normal operation (if the +Tendermint failure model holds +[[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link]), +and then define different +deviations that correspond to attack scenarios. We consider the notion +of [light blocks][LCV-LB-link] and [headers][LVC-HD-link]. + +#### **[TMBC-GENESIS.1]** + +Let *Genesis* be the agreed-upon initial block (file). + +#### **[TMBC-FUNC-SIGN.1]** + +Let *b* and *c* be two light blocks with *b.Header.Height + 1 = +c.Header.Height*. We define the predicate **signs(b,c)** to hold +iff *c.Header.LastCommit* is in *PossibleCommit(b)*. +[[TMBC-SOUND-DISTR-POSS-COMMIT.1]][TMBC-SOUND-DISTR-POSS-COMMIT-link]. + +> The above encodes sequential verification, that is, intuitively, +> b.Header.NextValidators = c.Header.Validators and 2/3 of +> these Validators signed c. + +#### **[TMBC-FUNC-SUPPORT.1]** + +Let *b* and *c* be two light blocks. We define the predicate +**supports(b,c,t)** to hold iff + +- *t - trustingPeriod < b.Header.Time < t* +- the voting power in *b.NextValidators* of nodes in *c.Commit* + is more than 1/3 of *TotalVotingPower(b.Header.NextValidators)* + +> That is, if the [Tendermint failure model][TMBC-FM-2THIRDS-link] +> holds, then *c* has been signed by at least one correct full node, cf. +> [[TMBC-VAL-CONTAINS-CORR.1]][TMBC-VAL-CONTAINS-CORR-link]. +> The following formalizes that *b* was properly generated by +> Tendermint; *b* can be traced back to genesis. + +#### **[TMBC-SEQ-ROOTED.1]** + +Let *b* be a light block. +We define *sequ-rooted(b)* iff for all *i*, *1 <= i < h = b.Header.Height*, +there exist light blocks *a(i)* s.t. + +- *a(1) = Genesis* and +- *a(h) = b* and +- *signs( a(i) , a(i+1) )*. + +> The following formalizes that *c* is trusted based on *b* in +> skipping verification. Observe that we do not require here (yet) +> that *b* was properly generated. + +#### **[TMBC-SKIP-TRACE.1]** + +Let *b* and *c* be light blocks. We define *skip-trace(b,c,t)* if at +time t there exists an integer *h* and a sequence *a(1)*, ... *a(h)* s.t. + +- *a(1) = b* and +- *a(h) = c* and +- *supports( a(i), a(i+1), t)*, for all i, *1 <= i < h*. + +We call such a sequence *a(1)*, ... *a(h)* a **verification trace**. + +> The following formalizes that two light blocks of the same height +> should agree on the content of the header. Observe that *b* and *c* +> may disagree on the Commit. This is a special case if the canonical +> commit has not been decided on yet, that is, if b.Header.Height is the +> maximum height of all blocks decided upon by Tendermint at this +> moment. + +#### **[TMBC-SIGN-SKIP-MATCH.1]** + +Let *a*, *b*, *c*, be light blocks and *t* a time, we define +*sign-skip-match(a,b,c,t) = true* iff the following implication +evaluates to true: + +- *sequ-rooted(a)* and +- *b.Header.Height = c.Header.Height* and +- *skip-trace(a,b,t)* +- *skip-trace(a,c,t)* + +implies *b.Header = c.Header*. + +> Observe that *sign-skip-match* is defined via an implication. If it +> evaluates to false this means that the left-hand-side of the +> implication evaluates to true, and the right-hand-side evaluates to +> false. In particular, there are two **different** headers *b* and +> *c* that both can be verified from a common block *a* from the +> chain. Thus, the following describes an attack. + +#### **[TMBC-ATTACK.1]** + +If there exists three light blocks a, b, and c, with +*sign-skip-match(a,b,c,t) = false* then we have an *attack*. We say +we have **an attack at height** *b.Header.Height* and write +*attack(a,b,c,t)*. + +> The lightblock *a* need not be unique, that is, there may be +> several blocks that satisfy the above requirement for the same +> blocks *b* and *c*. + +[[TMBC-ATTACK.1]](#TMBC-ATTACK1) is a formalization of the violation +of the agreement property based on the result of consensus, that is, +the generated blocks. + +**Remark.** +Violation of agreement is only possible if more than 1/3 of the validators (or +next validators) of some previous block deviated from the protocol. The +upcoming "accountability" specification will describe how to compute +a set of at least 1/3 faulty nodes from two conflicting blocks. [] + +There are different ways to characterize forks +and attack scenarios. This specification uses the "node-based +characterization of attacks" which focuses on what kinds of nodes are +affected (light nodes vs. full nodes). For future reference and +discussion we also provide a +"block-based characterization of attacks" below. + +## Node-based characterization of attacks + +#### **[TMBC-MC-FORK.1]** + +We say there is a (main chain) fork at time *t* if + +- there are two correct full nodes *i* and *j* and +- *i* is different from *j* and +- *i* has decided on *b* and +- *j* has decided on *c* and +- there exist *a* such that *attack(a,b,c,t)*. + +#### **[TMBC-LC-ATTACK.1]** + +We say there is a light client attack at time *t*, if + +- there is **no** (main chain) fork [[TMBC-MC-FORK.1]](#TMBC-MC-FORK1), and +- there exist nodes that have computed light blocks *b* and *c* and +- there exist *a* such that *attack(a,b,c,t)*. + +We say the attack is at height *a.Header.Height*. + +> In this specification we consider detection of light client +> attacks. Intuitively, the case we consider is that +> light block *b* is the one from the +> blockchain, and some attacker has computed *c* and tries to wrongly +> convince +> the light client that *c* is the block from the chain. + +#### **[TMBC-LC-ATTACK-EVIDENCE.1]** + +We consider the following case of a light client attack +[[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1): + +- *attack(a,b,c,t)* +- there is a peer p1 that has a sequence *chain* of blocks from *a* to *b* +- *skip-trace(a,c,t)*: by [[TMBC-SKIP-TRACE.1]](#TMBC-SKIP-TRACE1) there is a + verification trace *v* of the form *a = v(1)*, ... *v(h) = c* + +Evidence for p1 (that proves an attack to p1) consists for index i +of v(i) and v(i+1) such that + +- E1(i). v(i) is equal to the block of *chain* at height v(i).Height, and +- E2(i). v(i+1) that is different from the block of *chain* at + height v(i+1).height + +> Observe p1 can +> +> - check that v(i+1) differs from its block at that height, and +> - verify v(i+1) in one step from v(i) as v is a verification trace. + +#### **[TMBC-LC-EVIDENCE-DATA.1]** + +To prove the attack to p1, because of Point E1, it is sufficient to +submit + +- v(i).Height (rather than v(i)). +- v(i+1) + +This information is *evidence for height v(i).Height*. + +## Block-based characterization of attacks + +In this section we provide a different characterization of attacks. It +is not defined on the nodes that are affected but purely on the +content of the blocks. In that sense these definitions are less +operational. + +> They might be relevant for a closer analysis of fork scenarios on the +> chain, which is out of the scope of this specification. + +#### **[TMBC-SIGN-UNIQUE.1]** + +Let *b* and *c* be light blocks, we define the predicate +*sign-unique(b,c)* to evaluate to true iff the following implication +evaluates to true: + +- *b.Header.Height = c.Header.Height* and +- *sequ-rooted(b)* and +- *sequ-rooted(c)* + +implies *b = c*. + +#### **[TMBC-BLOCKS-MCFORK.1]** + +If there exists two light blocks b and c, with *sign-unique(b,c) = +false* then we have a *fork*. + +> The difference of the above definition to +> [[TMBC-MC-FORK.1]](#TMBC-MC-FORK1) is subtle. The latter requires a +> full node being affected by a bad block while +> [[TMBC-BLOCKS-MCFORK.1]](#TMBC-BLOCKS-MCFORK1) just requires that a +> bad block exists, possibly in memory of an attacker. +> The following captures a light client fork. There is no fork up to +> the height of block b. However, c is of that height, is different, +> and passes skipping verification. It is a stricter property than +> [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1), as +> [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1) requires that no correct full +> node is affected. + +#### **[TMBC-BLOCKS-LCFORK.1]** + +Let *a*, *b*, *c*, be light blocks and *t* a time. We define +*light-client-fork(a,b,c,t)* iff + +- *sign-skip-match(a,b,c,t) = false* and +- *sequ-rooted(b)* and +- *b* is "unique", that is, for all *d*, *sequ-rooted(d)* and + *d.Header.Height = b.Header.Height* implies *d = b* + +> Finally, let us also define bogus blocks that have no support. +> Observe that bogus is even defined if there is a fork. +> Also, for the definition it would be sufficient to restrict *a* to +> *a.height < b.height* (which is implied by the definitions which +> unfold until *supports()*). + +#### **[TMBC-BOGUS.1]** + +Let *b* be a light block and *t* a time. We define *bogus(b,t)* iff + +- *sequ-rooted(b) = false* and +- for all *a*, *sequ-rooted(a)* implies *skip-trace(a,b,t) = false* + +# Part II - Problem Statement + +## Informal Problem statement + +There is no sequential specification: the detector only makes sense +in a distributed systems where some nodes misbehave. + +We work under the assumption that full nodes and validators are +responsible for detecting attacks on the main chain, and the evidence +reactor takes care of broadcasting evidence to communicate +misbehaving nodes via ABCI to the application, and halt the chain in +case of a fork. The point of this specification is to shield a light +clients against attacks that cannot be detected by full nodes, and +are fully addressed at light clients (and consequently IBC relayers, +which use the light client protocols to observe the state of a +blockchain). In order to provide full nodes the incentive to follow +the protocols when communicating with the light client, this +specification also considers the generation of evidence that will +also be processed by the Tendermint blockchain. + +#### **[LCD-IP-MODEL.1]** + +The detector is designed under the assumption that + +- [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link] may be violated +- there is no fork on the main chain. + +> As a result some faulty full nodes may launch an attack on a light +> client. + +The following requirements are operational in that they describe how +things should be done, rather than what should be done. However, they +do not constitute temporal logic verification conditions. For those, +see [LCD-DIST-*] below. + +The detector is called in the [supervisor][supervisor] as follows + +```go +Evidences := AttackDetector(root_of_trust, verifiedLS);` +``` + +where + +- `root-of-trust` is a light block that is trusted (that is, +except upon initialization, the primary and the secondaries +agreed on in the past), and +- `verifiedLS` is a lightstore that contains a verification trace that + starts from a lightblock that can be verified with the + `root-of-trust` in one step and ends with a lightblock of the height + requested by the user +- `Evidences` is a list of evidences for misbehavior + +#### **[LCD-IP-STATEMENT.1]** + +Whenever AttackDetector is called, the detector should for each +secondary cross check the largest header in verifiedLS with the +corresponding header of the same height provided by the secondary. If +there is a deviation, the detector should +try to replay the verification trace `verifiedLS` with the +secondary + +- in case replaying leads to detection of a light client attack + (one of the lightblocks differ from the one in verifiedLS with + the same height), we should return evidence +- if the secondary cannot provide a verification trace, we have no + proof for an attack. Block *b* may be bogus. In this case the + secondary is faulty and it should be replaced. + +## Assumptions + +It is not in the interest of faulty full nodes to talk to the +detector as long as the detector is connected to at least one +correct full node. This would only increase the likelihood of +misbehavior being detected. Also we cannot punish them easily +(cheaply). The absence of a response need not be the fault of the full +node. + +Correct full nodes have the incentive to respond, because the +detector may help them to understand whether their header is a good +one. We can thus base liveness arguments of the detector on +the assumptions that correct full nodes reliably talk to the +detector. + +#### **[LCD-A-CorrFull.1]** + +At all times there is at least one correct full +node among the primary and the secondaries. + +> For this version of the detection we take this assumption. It +> allows us to establish the invariant that the lightblock +> `root-of-trust` is always the one from the blockchain, and we can +> use it as starting point for the evidence computation. Moreover, it +> allows us to establish the invariant at the supervisor that any +> lightblock in the (top-level) lightstore is from the blockchain. +> In the future we might design a lightclient based on the assumption +> that at least in regular intervals the lightclient is connected to a +> correct full node. This will require the detector to reconsider +> `root-of-trust`, and remove lightblocks from the top-level +> lightstore. + +#### **[LCD-A-RelComm.1]** + +Communication between the detector and a correct full node is +reliable and bounded in time. Reliable communication means that +messages are not lost, not duplicated, and eventually delivered. There +is a (known) end-to-end delay *Delta*, such that if a message is sent +at time *t* then it is received and processed by time *t + Delta*. +This implies that we need a timeout of at least *2 Delta* for remote +procedure calls to ensure that the response of a correct peer arrives +before the timeout expires. + +## Definitions + +### Evidence + +Following the definition of +[[TMBC-LC-ATTACK-EVIDENCE.1]](#TMBC-LC-ATTACK-EVIDENCE1), by evidence +we refer to a variable of the following type + +#### **[LC-DATA-EVIDENCE.1]** + +```go +type LightClientAttackEvidence struct { + ConflictingBlock LightBlock + CommonHeight int64 + + // Evidence also includes application specific data which is not + // part of verification but is sent to the application once the + // evidence gets committed on chain. +} +``` + +As the above data is computed for a specific peer, the following +data structure wraps the evidence and adds the peerID. + +#### **[LC-DATA-EVIDENCE-INT.1]** + +```go +type InternalEvidence struct { + Evidence LightClientAttackEvidence + Peer PeerID +} +``` + +#### **[LC-SUMBIT-EVIDENCE.1]** + +```go +func submitEvidence(Evidences []InternalEvidence) +``` + +- Expected postcondition + - for each `ev` in `Evidences`: submit `ev.Evidence` to `ev.Peer` + +--- + +### LightStore + +Lightblocks and LightStores are defined in the verification +specification [[LCV-DATA-LIGHTBLOCK.1]][LCV-LB-link] +and [[LCV-DATA-LIGHTSTORE.2]][LCV-LS-link]. See +the [verification specification][verification] for details. + +## Distributed Problem statement + +> As the attack detector is there to reduce the impact of faulty +> nodes, and faulty nodes imply that there is a distributed system, +> there is no sequential specification to which this distributed +> problem statement may refer to. + +The detector gets as input a trusted lightblock called *root* and an +auxiliary lightstore called *primary_trace* with lightblocks that have +been verified before, and that were provided by the primary. + +#### **[LCD-DIST-INV-ATTACK.1]** + +If the detector returns evidence for height *h* +[[TMBC-LC-EVIDENCE-DATA.1]](#TMBC-LC-EVIDENCE-DATA1), then there is an +attack at height *h*. [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1) + +#### **[LCD-DIST-INV-STORE.1]** + +If the detector does not return evidence, then *primary_trace* +contains only blocks from the blockchain. + +#### **[LCD-DIST-LIVE.1]** + +The detector eventually terminates. + +#### **[LCD-DIST-TERM-NORMAL.1]** + +If + +- the *primary_trace* contains only blocks from the blockchain, and +- there is no attack, and +- *Secondaries* is always non-empty, and +- the age of *root* is always less than the trusting period, + +then the detector does not return evidence. + +#### **[LCD-DIST-TERM-ATTACK.1]** + +If + +- there is an attack, and +- a secondary reports a block that conflicts + with one of the blocks in *primary_trace*, and +- *Secondaries* is always non-empty, and +- the age of *root* is always less than the trusting period, + +then the detector returns evidence. + +> Observe that above we require that "a secondary reports a block that +> conflicts". If there is an attack, but no secondary tries to launch +> it against the detector (or the message from the secondary is lost +> by the network), then there is nothing to detect for us. + +#### **[LCD-DIST-SAFE-SECONDARY.1]** + +No correct secondary is ever replaced. + +#### **[LCD-DIST-SAFE-BOGUS.1]** + +If + +- a secondary reports a bogus lightblock, +- the age of *root* is always less than the trusting period, + +then the secondary is replaced before the detector terminates. + +> The above property is quite operational (e.g., the usage of +> "reports"), but it captures closely the requirement. As the +> detector only makes sense in a distributed setting, and does not +> have a sequential specification, a less "pure" specification are +> acceptable. + +# Part III - Protocol + +## Functions and Data defined in other Specifications + +### From the [supervisor][supervisor] + +[[LC-FUNC-REPLACE-SECONDARY.1]][repl] + +```go +Replace_Secondary(addr Address, root-of-trust LightBlock) +``` + +### From the [verifier][verification] + +[[LCV-FUNC-MAIN.2]][vtt] + +```go +func VerifyToTarget(primary PeerID, root LightBlock, + targetHeight Height) (LightStore, Result) +``` + +Observe that `VerifyToTarget` does communication with the secondaries +via the function [FetchLightBlock][fetch]. + +### Shared data of the light client + +- a pool of full nodes *FullNodes* that have not been contacted before +- peer set called *Secondaries* +- primary + +> Note that the lightStore is not needed to be shared. + +## Outline of solution + +The problem laid out is solved by calling the function `AttackDetector` +with a lightstore that contains a light block that has just been +verified by the verifier. + +Then `AttackDetector` downloads headers from the secondaries. In case +a conflicting header is downloaded from a secondary, it calls +`CreateEvidenceForPeer` which computes evidence in the case that +indeed an attack is confirmed. It could be that the secondary reports +a bogus block, which means that there need not be an attack, and the +secondary is replaced. + +## Details of the functions + +#### **[LCD-FUNC-DETECTOR.2]:** + +```go +func AttackDetector(root LightBlock, primary_trace []LightBlock) + ([]InternalEvidence) { + + Evidences := new []InternalEvidence; + + for each secondary in Secondaries { + lb, result := FetchLightBlock(secondary,primary_trace.Latest().Header.Height); + if result != ResultSuccess { + Replace_Secondary(root); + } + else if lb.Header != primary_trace.Latest().Header { + + // we replay the primary trace with the secondary, in + // order to generate evidence that we can submit to the + // secondary. We return the evidence + the trace the + // secondary told us that spans the evidence at its local store + + EvidenceForSecondary, newroot, secondary_trace, result := + CreateEvidenceForPeer(secondary, + root, + primary_trace); + if result == FaultyPeer { + Replace_Secondary(root); + } + else if result == FoundEvidence { + // the conflict is not bogus + Evidences.Add(EvidenceForSecondary); + // we replay the secondary trace with the primary, ... + EvidenceForPrimary, _, result := + CreateEvidenceForPeer(primary, + newroot, + secondary_trace); + if result == FoundEvidence { + Evidences.Add(EvidenceForPrimary); + } + // At this point we do not care about the other error + // codes. We already have generated evidence for an + // attack and need to stop the lightclient. It does not + // help to call replace_primary. Also we will use the + // same primary to check with other secondaries in + // later iterations of the loop + } + // In the case where the secondary reports NoEvidence + // after initially it reported a conflicting header. + // secondary is faulty + Replace_Secondary(root); + } + } + return Evidences; +} +``` + +- Expected precondition + - root and primary trace are a verification trace +- Expected postcondition + - solves the problem statement (if attack found, then evidence is reported) +- Error condition + - `ErrorTrustExpired`: fails if root expires (outside trusting + period) [[LCV-INV-TP.1]][LCV-INV-TP1-link] + - `ErrorNoPeers`: if no peers are left to replace secondaries, and + no evidence was found before that happened + +--- + +```go +func CreateEvidenceForPeer(peer PeerID, root LightBlock, trace LightStore) + (Evidence, LightBlock, LightStore, result) { + + common := root; + + for i in 1 .. len(trace) { + auxLS, result := VerifyToTarget(peer, common, trace[i].Header.Height) + + if result != ResultSuccess { + // something went wrong; peer did not provide a verifiable block + return (nil, nil, nil, FaultyPeer) + } + else { + if auxLS.LatestVerified().Header != trace[i].Header { + // the header reported by the peer differs from the + // reference header in trace but both could be + // verified from common in one step. + // we can create evidence for submission to the secondary + ev := new InternalEvidence; + ev.Evidence.ConflictingBlock := trace[i]; + // CommonHeight is used to indicate the type of attack + // if the CommonHeight != ConflictingBlock.Height this + // is by definition a lunatic attack else it is an + // equivocation attack + ev.Evidence.CommonHeight := common.Height; + ev.Peer := peer + return (ev, common, auxLS, FoundEvidence) + } + else { + // the peer agrees with the trace, we move common forward. + // we could delete auxLS as it will be overwritten in + // the next iteration + common := trace[i] + } + } + } + return (nil, nil, nil, NoEvidence) +} +``` + +- Expected precondition + - root and trace are a verification trace +- Expected postcondition + - finds evidence where trace and peer diverge +- Error condition + - `ErrorTrustExpired`: fails if root expires (outside trusting + period) [[LCV-INV-TP.1]][LCV-INV-TP1-link] + - If `VerifyToTarget` returns error but root is not expired then return + `FaultyPeer` + +--- + +## Correctness arguments + +#### On the existence of evidence + +**Proposition.** In the case of attack, +evidence [[TMBC-LC-ATTACK-EVIDENCE.1]](#TMBC-LC-ATTACK-EVIDENCE1) + exists. +*Proof.* First observe that + +- (A). (NOT E2(i)) implies E1(i+1) + +Now by contradiction assume there is no evidence. Thus + +- for all i, we have NOT E1(i) or NOT E2(i) +- for i = 1 we have E1(1) and thus NOT E2(1) + thus by induction on i, by (A) we have for all i that **E1(i)** +- from attack we have E2(h-1), and as there is no evidence for + i = h - 1 we get **NOT E1(h-1)**. Contradiction. +QED. + +#### Argument for [[LCD-DIST-INV-ATTACK.1]](#LCD-DIST-INV-ATTACK1) + +Under the assumption that root and trace are a verification trace, +when in `CreateEvidenceForPeer` the detector creates +evidence, then the lightclient has seen two different headers (one via +`trace` and one via `VerifyToTarget`) for the same height that can both +be verified in one step. + +#### Argument for [[LCD-DIST-INV-STORE.1]](#LCD-DIST-INV-STORE1) + +We assume that there is at least one correct peer, and there is no +fork. As a result, the correct peer has the correct sequence of +blocks. Since the primary_trace is checked block-by-block also against +each secondary, and at no point evidence was generated that means at +no point there were conflicting blocks. + +#### Argument for [[LCD-DIST-LIVE.1]](#LCD-DIST-LIVE1) + +At the latest when [[LCV-INV-TP.1]][LCV-INV-TP1-link] is violated, +`AttackDetector` terminates. + +#### Argument for [[LCD-DIST-TERM-NORMAL.1]](#LCD-DIST-TERM-NORMAL1) + +As there are finitely many peers, eventually the main loop +terminates. As there is no attack no evidence can be generated. + +#### Argument for [[LCD-DIST-TERM-ATTACK.1]](#LCD-DIST-TERM-ATTACK1) + +Argument similar to [[LCD-DIST-TERM-NORMAL.1]](#LCD-DIST-TERM-NORMAL1) + +#### Argument for [[LCD-DIST-SAFE-SECONDARY.1]](#LCD-DIST-SAFE-SECONDARY1) + +Secondaries are only replaced if they time-out or if they report bogus +blocks. The former is ruled out by the timing assumption, the latter +by correct peers only reporting blocks from the chain. + +#### Argument for [[LCD-DIST-SAFE-BOGUS.1]](#LCD-DIST-SAFE-BOGUS1) + +Once a bogus block is recognized as such the secondary is removed. + +# References + +> links to other specifications/ADRs this document refers to + +[[verification]] The specification of the light client verification. + +[[supervisor]] The specification of the light client supervisor. + +[verification]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md + +[supervisor]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/supervisor/supervisor_001_draft.md + +[block]: https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md + +[TMBC-FM-2THIRDS-link]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-fm-2thirds1 + +[TMBC-SOUND-DISTR-POSS-COMMIT-link]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-sound-distr-poss-commit1 + +[LCV-SEQ-SAFE-link]:https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#lcv-seq-safe1 + +[TMBC-VAL-CONTAINS-CORR-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-val-contains-corr1 + +[fetch]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#lcv-func-fetch1 + +[LCV-INV-TP1-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#lcv-inv-tp1 + +[LCV-LB-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#lcv-data-lightblock1 + +[LCV-LS-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#lcv-data-lightstore2 + +[LVC-HD-link]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-header-fields2 + +[repl]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/supervisor/supervisor_001_draft.md#lc-func-replace-secondary1 + +[vtt]: +https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/verification/verification_002_draft.md#lcv-func-main2 diff --git a/spec/light-client/detection/discussions.md b/spec/light-client/detection/discussions.md new file mode 100644 index 000000000..82702dd69 --- /dev/null +++ b/spec/light-client/detection/discussions.md @@ -0,0 +1,178 @@ +# Results of Discussions and Decisions + +- Generating a minimal proof of fork (as suggested in [Issue #5083](https://github.com/tendermint/tendermint/issues/5083)) is too costly at the light client + - we do not know all lightblocks from the primary + - therefore there are many scenarios. we might even need to ask + the primary again for additional lightblocks to isolate the + branch. + +> For instance, the light node starts with block at height 1 and the +> primary provides a block of height 10 that the light node can +> verify immediately. In cross-checking, a secondary now provides a +> conflicting header b10 of height 10 that needs another header b5 +> of height 5 to +> verify. Now, in order for the light node to convince the primary: +> +> - The light node cannot just sent b5, as it is not clear whether +> the fork happened before or after 5 +> - The light node cannot just send b10, as the primary would also +> need b5 for verification +> - In order to minimize the evidence, the light node may try to +> figure out where the branch happens, e.g., by asking the primary +> for height 5 (it might be that more queries are required, also +> to the secondary. However, assuming that in this scenario the +> primary is faulty it may not respond. + + As the main goal is to catch misbehavior of the primary, + evidence generation and punishment must not depend on their + cooperation. So the moment we have proof of fork (even if it + contains several light blocks) we should submit right away. + +- decision: "full" proof of fork consists of two traces that originate in the + same lightblock and lead to conflicting headers of the same height. + +- For submission of proof of fork, we may do some optimizations, for + instance, we might just submit a trace of lightblocks that verifies a block + different from the one the full node knows (we do not send the trace + the primary gave us back to the primary) + +- The light client attack is via the primary. Thus we try to + catch if the primary installs a bad light block + - We do not check secondary against secondary + - For each secondary, we check the primary against one secondary + +- Observe that just two blocks for the same height are not +sufficient proof of fork. +One of the blocks may be bogus [TMBC-BOGUS.1] which does +not constitute slashable behavior. +Which leads to the question whether the light node should try to do +fork detection on its initial block (from subjective +initialization). This could be done by doing backwards verification +(with the hashes) until a bifurcation block is found. +While there are scenarios where a +fork could be found, there is also the scenario where a faulty full +node feeds the light node with bogus light blocks and forces the light +node to check hashes until a bogus chain is out of the trusting period. +As a result, the light client +should not try to detect a fork for its initial header. **The initial +header must be trusted as is.** + +# Light Client Sequential Supervisor + +**TODO:** decide where (into which specification) to put the +following: + +We describe the context on which the fork detector is called by giving +a sequential version of the supervisor function. +Roughly, it alternates two phases namely: + +- Light Client Verification. As a result, a header of the required + height has been downloaded from and verified with the primary. +- Light Client Fork Detections. As a result the header has been + cross-checked with the secondaries. In case there is a fork we + submit "proof of fork" and exit. + +#### **[LC-FUNC-SUPERVISOR.1]:** + +```go +func Sequential-Supervisor () (Error) { + loop { + // get the next height + nextHeight := input(); + + // Verify + result := NoResult; + while result != ResultSuccess { + lightStore,result := VerifyToTarget(primary, lightStore, nextHeight); + if result == ResultFailure { + // pick new primary (promote a secondary to primary) + /// and delete all lightblocks above + // LastTrusted (they have not been cross-checked) + Replace_Primary(); + } + } + + // Cross-check + PoFs := Forkdetector(lightStore, PoFs); + if PoFs.Empty { + // no fork detected with secondaries, we trust the new + // lightblock + LightStore.Update(testedLB, StateTrusted); + } + else { + // there is a fork, we submit the proofs and exit + for i, p range PoFs { + SubmitProofOfFork(p); + } + return(ErrorFork); + } + } +} +``` + +**TODO:** finish conditions + +- Implementation remark +- Expected precondition + - *lightStore* initialized with trusted header + - *PoFs* empty +- Expected postcondition + - runs forever, or + - is terminated by user and satisfies LightStore invariant, or **TODO** + - has submitted proof of fork upon detecting a fork +- Error condition + - none + +---- + +# Semantics of the LightStore + +Currently, a lightblock in the lightstore can be in one of the +following states: + +- StateUnverified +- StateVerified +- StateFailed +- StateTrusted + +The intuition is that `StateVerified` captures that the lightblock has +been verified with the primary, and `StateTrusted` is the state after +successful cross-checking with the secondaries. + +Assuming there is **always one correct node among primary and +secondaries**, and there is no fork on the blockchain, lightblocks that +are in `StateTrusted` can be used by the user with the guarantee of +"finality". If a block in `StateVerified` is used, it might be that +detection later finds a fork, and a roll-back might be needed. + +**Remark:** The assumption of one correct node, does not render +verification useless. It is true that if the primary and the +secondaries return the same block we may trust it. However, if there +is a node that provides a different block, the light node still needs +verification to understand whether there is a fork, or whether the +different block is just bogus (without any support of some previous +validator set). + +**Remark:** A light node may choose the full nodes it communicates +with (the light node and the full node might even belong to the same +stakeholder) so the assumption might be justified in some cases. + +In the future, we will do the following changes + +- we assume that only from time to time, the light node is + connected to a correct full node +- this means for some limited time, the light node might have no + means to defend against light client attacks +- as a result we do not have finality +- once the light node reconnects with a correct full node, it + should detect the light client attack and submit evidence. + +Under these assumptions, `StateTrusted` loses its meaning. As a +result, it should be removed from the API. We suggest that we replace +it with a flag "trusted" that can be used + +- internally for efficiency reasons (to maintain + [LCD-INV-TRUSTED-AGREED.1] until a fork is detected) +- by light client based on the "one correct full node" assumption + +---- diff --git a/spec/light-client/detection/draft-functions.md b/spec/light-client/detection/draft-functions.md new file mode 100644 index 000000000..c56594a53 --- /dev/null +++ b/spec/light-client/detection/draft-functions.md @@ -0,0 +1,289 @@ +# Draft of Functions for Fork detection and Proof of Fork Submisstion + +This document collects drafts of function for generating and +submitting proof of fork in the IBC context + +- [IBC](#on---chain-ibc-component) + +- [Relayer](#relayer) + +## On-chain IBC Component + +> The following is a suggestions to change the function defined in ICS 007 + +#### [TAG-IBC-MISBEHAVIOR.1] + +```go +func checkMisbehaviourAndUpdateState(cs: ClientState, PoF: LightNodeProofOfFork) +``` + +**TODO:** finish conditions + +- Implementation remark +- Expected precondition + - PoF.TrustedBlock.Header is equal to lightBlock on store with + same height + - both traces end with header of same height + - headers are different + - both traces are supported by PoF.TrustedBlock (`supports` + defined in [TMBC-FUNC]), that is, for `t = currentTimestamp()` (see + ICS 024) + - supports(PoF.TrustedBlock, PoF.PrimaryTrace[1], t) + - supports(PoF.PrimaryTrace[i], PoF.PrimaryTrace[i+1], t) for + *0 < i < length(PoF.PrimaryTrace)* + - supports(PoF.TrustedBlock, PoF.SecondaryTrace[1], t) + - supports(PoF.SecondaryTrace[i], PoF.SecondaryTrace[i+1], t) for + *0 < i < length(PoF.SecondaryTrace)* +- Expected postcondition + - set cs.FrozenHeight to min(cs.FrozenHeight, PoF.TrustedBlock.Header.Height) +- Error condition + - none + +---- + +> The following is a suggestions to add functionality to ICS 002 and 007. +> I suppose the above is the most efficient way to get the required +> information. Another option is to subscribe to "header install" +> events via CosmosSDK + +#### [TAG-IBC-HEIGHTS.1] + +```go +func QueryHeightsRange(id, from, to) ([]Height) +``` + +- Expected postcondition + - returns all heights *h*, with *from <= h <= to* for which the + IBC component has a consensus state. + +---- + +> This function can be used if the relayer has no information about +> the IBC component. This allows late-joining relayers to also +> participate in fork dection and the generation in proof of +> fork. Alternatively, we may also postulate that relayers are not +> responsible to detect forks for heights before they started (and +> subscribed to the transactions reporting fresh headers being +> installed at the IBC component). + +## Relayer + +### Auxiliary Functions to be implemented in the Light Client + +#### [LCV-LS-FUNC-GET-PREV.1] + +```go +func (ls LightStore) GetPreviousVerified(height Height) (LightBlock, bool) +``` + +- Expected postcondition + - returns a verified LightBlock, whose height is maximal among all + verified lightblocks with height smaller than `height` + +---- + +### Relayer Submitting Proof of Fork to the IBC Component + +There are two ways the relayer can detect a fork + +- by the fork detector of one of its lightclients +- be checking the consensus state of the IBC component + +The following function ignores how the proof of fork was generated. +It takes a proof of fork as input and computes a proof of fork that + will be accepted by the IBC component. +The problem addressed here is that both, the relayer's light client + and the IBC component have incomplete light stores, that might + not have all light blocks in common. +Hence the relayer has to figure out what the IBC component knows + (intuitively, a meeting point between the two lightstores + computed in `commonRoot`) and compute a proof of fork + (`extendPoF`) that the IBC component will accept based on its + knowledge. + +The auxiliary functions `commonRoot` and `extendPoF` are +defined below. + +#### [TAG-SUBMIT-POF-IBC.1] + +```go +func SubmitIBCProofOfFork( + lightStore LightStore, + PoF: LightNodeProofOfFork, + ibc IBCComponent) (Error) { + if ibc.queryChainConsensusState(PoF.TrustedBlock.Height) = PoF.TrustedBlock { + // IBC component has root of PoF on store, we can just submit + ibc.submitMisbehaviourToClient(ibc.id,PoF) + return Success + // note sure about the id parameter + } + else { + // the ibc component does not have the TrustedBlock and might + // even be on yet a different branch. We have to compute a PoF + // that the ibc component can verifiy based on its current + // knowledge + + ibcLightBlock, lblock, _, result := commonRoot(lightStore, ibc, PoF.TrustedBlock) + + if result = Success { + newPoF = extendPoF(ibcLightBlock, lblock, lightStore, PoF) + ibc.submitMisbehaviourToClient(ibc.id, newPoF) + return Success + } + else{ + return CouldNotGeneratePoF + } + } +} +``` + +**TODO:** finish conditions + +- Implementation remark +- Expected precondition +- Expected postcondition +- Error condition + - none + +---- + +### Auxiliary Functions at the Relayer + +> If the relayer detects a fork, it has to compute a proof of fork that +> will convince the IBC component. That is it has to compare the +> relayer's local lightstore against the lightstore of the IBC +> component, and find common ancestor lightblocks. + +#### [TAG-COMMON-ROOT.1] + +```go +func commonRoot(lightStore LightStore, ibc IBCComponent, lblock +LightBlock) (LightBlock, LightBlock, LightStore, Result) { + + auxLS.Init + + // first we ask for the heights the ibc component is aware of + ibcHeights = ibc.QueryHeightsRange( + ibc.id, + lightStore.LowestVerified().Height, + lblock.Height - 1); + // this function does not exist yet. Alternatively, we may + // request all transactions that installed headers via CosmosSDK + + + for { + h, result = max(ibcHeights) + if result = Empty { + return (_, _, _, NoRoot) + } + ibcLightBlock = ibc.queryChainConsensusState(h) + auxLS.Update(ibcLightBlock, StateVerified); + connector, result := Connector(lightStore, ibcLightBlock, lblock.Header.Height) + if result = success { + return (ibcLightBlock, connector, auxLS, Success) + } + else{ + ibcHeights.remove(h) + } + } +} +``` + +- Expected postcondition + - returns + - a lightBlock b1 from the IBC component, and + - a lightBlock b2 + from the local lightStore with height less than + lblock.Header.Hight, s.t. b1 supports b2, and + - a lightstore with the blocks downloaded from + the ibc component + +---- + +#### [TAG-LS-FUNC-CONNECT.1] + +```go +func Connector (lightStore LightStore, lb LightBlock, h Height) (LightBlock, bool) +``` + +- Expected postcondition + - returns a verified LightBlock from lightStore with height less + than *h* that can be + verified by lb in one step. + +**TODO:** for the above to work we need an invariant that all verified +lightblocks form a chain of trust. Otherwise, we need a lightblock +that has a chain of trust to height. + +> Once the common root is found, a proof of fork that will be accepted +> by the IBC component needs to be generated. This is done in the +> following function. + +#### [TAG-EXTEND-POF.1] + +```go +func extendPoF (root LightBlock, + connector LightBlock, + lightStore LightStore, + Pof LightNodeProofofFork) (LightNodeProofofFork} +``` + +- Implementation remark + - PoF is not sufficient to convince an IBC component, so we extend + the proof of fork farther in the past +- Expected postcondition + - returns a newPOF: + - newPoF.TrustedBlock = root + - let prefix = + connector + + lightStore.Subtrace(connector.Header.Height, PoF.TrustedBlock.Header.Height-1) + + PoF.TrustedBlock + - newPoF.PrimaryTrace = prefix + PoF.PrimaryTrace + - newPoF.SecondaryTrace = prefix + PoF.SecondaryTrace + +### Detection a fork at the IBC component + +The following functions is assumed to be called regularly to check +that latest consensus state of the IBC component. Alternatively, this +logic can be executed whenever the relayer is informed (via an event) +that a new header has been installed. + +#### [TAG-HANDLER-DETECT-FORK.1] + +```go +func DetectIBCFork(ibc IBCComponent, lightStore LightStore) (LightNodeProofOfFork, Error) { + cs = ibc.queryClientState(ibc); + lb, found := lightStore.Get(cs.Header.Height) + if !found { + **TODO:** need verify to target + lb, result = LightClient.Main(primary, lightStore, cs.Header.Height) + // [LCV-FUNC-IBCMAIN.1] + **TODO** decide what to do following the outcome of Issue #499 + + // I guess here we have to get into the light client + + } + if cs != lb { + // IBC component disagrees with my primary. + // I fetch the + ibcLightBlock, lblock, ibcStore, result := commonRoot(lightStore, ibc, lb) + pof = new LightNodeProofOfFork; + pof.TrustedBlock := ibcLightBlock + pof.PrimaryTrace := ibcStore + cs + pof.SecondaryTrace := lightStore.Subtrace(lblock.Header.Height, + lb.Header.Height); + return(pof, Fork) + } + return(nil , NoFork) +} +``` + +**TODO:** finish conditions + +- Implementation remark + - we ask the handler for the lastest check. Cross-check with the + chain. In case they deviate we generate PoF. + - we assume IBC component is correct. It has verified the + consensus state +- Expected precondition +- Expected postcondition diff --git a/spec/light-client/detection/req-ibc-detection.md b/spec/light-client/detection/req-ibc-detection.md new file mode 100644 index 000000000..439ca26b6 --- /dev/null +++ b/spec/light-client/detection/req-ibc-detection.md @@ -0,0 +1,345 @@ +# Requirements for Fork Detection in the IBC Context + +## What you need to know about IBC + +In the following, I distilled what I considered relevant from + + + +### Components and their interface + +#### Tendermint Blockchains + +> I assume you know what that is. + +#### An IBC/Tendermint correspondence + +| IBC Term | Tendermint-RS Spec Term | Comment | +|----------|-------------------------| --------| +| `CommitmentRoot` | AppState | app hash | +| `ConsensusState` | Lightblock | not all fields are there. NextValidator is definitly needed | +| `ClientState` | latest light block + configuration parameters (e.g., trusting period + `frozenHeight` | NextValidators missing; what is `proofSpecs`?| +| `frozenHeight` | height of fork | set when a fork is detected | +| "would-have-been-fooled" | light node fork detection | light node may submit proof of fork to IBC component to halt it | +| `Height` | (no epochs) | (epoch,height) pair in lexicographical order (`compare`) | +| `Header` | ~signed header | validatorSet explicit (no hash); nextValidators missing | +| `Evidence` | t.b.d. | definition unclear "which the light client would have considered valid". Data structure will need to change | +| `verify` | `ValidAndVerified` | signature does not match perfectly (ClientState vs. LightBlock) + in `checkMisbehaviourAndUpdateState` it is unclear whether it uses traces or goes to h1 and h2 in one step | + +#### Some IBC links + +- [QueryConsensusState](https://github.com/cosmos/cosmos-sdk/blob/2651427ab4c6ea9f81d26afa0211757fc76cf747/x/ibc/02-client/client/utils/utils.go#L68) + +#### Required Changes in ICS 007 + +- `assert(height > 0)` in definition of `initialise` doesn't match + definition of `Height` as *(epoch,height)* pair. + +- `initialise` needs to be updated to new data structures + +- `clientState.frozenHeight` semantics seem not totally consistent in + document. E.g., `min` needs to be defined over optional value in + `checkMisbehaviourAndUpdateState`. Also, if you are frozen, why do + you accept more evidence. + +- `checkValidityAndUpdateState` + - `verify`: it needs to be clarified that checkValidityAndUpdateState + does not perform "bisection" (as currently hinted in the text) but + performs a single step of "skipping verification", called, + `ValidAndVerified` + - `assert (header.height > clientState.latestHeight)`: no old + headers can be installed. This might be OK, but we need to check + interplay with misbehavior + - clienstState needs to be updated according to complete data + structure + +- `checkMisbehaviourAndUpdateState`: as evidence will contain a trace + (or two), the assertion that uses verify will need to change. + +- ICS 002 states w.r.t. `queryChainConsensusState` that "Note that + retrieval of past consensus states by height (as opposed to just the + current consensus state) is convenient but not required." For + Tendermint fork detection, this seems to be a necessity. + +- `Header` should become a lightblock + +- `Evidence` should become `LightNodeProofOfFork` [LCV-DATA-POF.1] + +- `upgradeClientState` what is the semantics (in particular what is + `height` doing?). + +- `checkMisbehaviourAndUpdateState(cs: ClientState, PoF: + LightNodeProofOfFork)` needs to be adapted + +#### Handler + +A blockchain runs a **handler** that passively collects information about + other blockchains. It can be thought of a state machine that takes + input events. + +- the state includes a lightstore (I guess called `ConsensusState` + in IBC) + +- The following function is used to pass a header to a handler + +```go +type checkValidityAndUpdateState = (Header) => Void +``` + + For Tendermint, it will perform + `ValidandVerified`, that is, it does the trusting period check and the + +1/3 check (+2/3 for sequential headers). + If it verifies a header, it adds it to its lightstore, + if it does not pass verification it drops it. + Right now it only accepts a header more recent then the latest + header, + and drops older + ones or ones that could not be verified. + +> The above paragraph captures what I believe what is the current + logic of `checkValidityAndUpdateState`. It may be subject to + change. E.g., maintain a lightstore with state (unverified, verified) + +- The following function is used to pass "evidence" (this we + will need to make precise eventually) to a handler + +```go +type checkMisbehaviourAndUpdateState = (bytes) => Void +``` + + We have to design this, and the data that the handler can use to + check that there was some misbehavior (fork) in order react on + it, e.g., flagging a situation and + stop the protocol. + +- The following function is used to query the light store (`ConsensusState`) + +```go +type queryChainConsensusState = (height: uint64) => ConsensusState +``` + +#### Relayer + +- The active components are called **relayer**. + +- a relayer contains light clients to two (or more?) blockchains + +- the relayer send headers and data to the handler to invoke + `checkValidityAndUpdateState` and + `checkMisbehaviourAndUpdateState`. It may also query + `queryChainConsensusState`. + +- multiple relayers may talk to one handler. Some relayers might be + faulty. We assume existence of at least single correct relayer. + +## Informal Problem Statement: Fork detection in IBC + +### Relayer requirement: Evidence for Handler + +- The relayer should provide the handler with + "evidence" that there was a fork. + +- The relayer can read the handler's consensus state. Thus the relayer can + feed the handler precisely the information the handler needs to detect a + fork. + What is this + information needs to be specified. + +- The information depends on the verification the handler does. It + might be necessary to provide a bisection proof (list of + lightblocks) so that the handler can verify based on its local + lightstore a header *h* that is conflicting with a header *h'* in the + local lightstore, that is, *h != h'* and *h.Height = h'.Height* + +### Relayer requirement: Fork detection + +Let's assume there is a fork at chain A. There are two ways the +relayer can figure that out: + +1. as the relayer contains a light client for A, it also includes a fork + detector that can detect a fork. + +2. the relayer may also detect a fork by observing that the + handler for chain A (on chain B) + is on a different branch than the relayer + +- in both detection scenarios, the relayer should submit evidence to + full nodes of chain A where there is a fork. As we assume a fullnode + has a complete list of blocks, it is sufficient to send "Bucky's + evidence" (), + that is, + - two lightblocks from different branches + + - a lightblock (perhaps just a height) from which both blocks + can be verified. + +- in the scenario 2., the relayer must feed the A-handler (on chain B) + a proof of a fork on A so that chain B can react accordingly + +### Handler requirement + +- there are potentially many relayers, some correct some faulty + +- a handler cannot trust the information provided by the relayer, + but must verify + (Доверя́й, но проверя́й) + +- in case of a fork, we accept that the handler temporarily stores + headers (tagged as verified). + +- eventually, a handler should be informed + (`checkMisbehaviourAndUpdateState`) + by some relayer that it has + verified a header from a fork. Then the handler should do what is + required by IBC in this case (stop?) + +### Challenges in the handler requirement + +- handlers and relayers work on different lightstores. In principle + the lightstore need not intersect in any heights a priori + +- if a relayer sees a header *h* it doesn't know at a handler (`queryChainConsensusState`), the + relayer needs to + verify that header. If it cannot do it locally based on downloaded + and verified (trusted?) light blocks, it might need to use + `VerifyToTarget` (bisection). To call `VerifyToTarget` we might keep + *h* in the lightstore. If verification fails, we need to download the + "alternative" header of height *h.Height* to generate evidence for + the handler. + +- we have to specify what precisely `queryChainConsensusState` + returns. It cannot be the complete lightstore. Is the last header enough? + +- we would like to assume that every now and then (smaller than the + trusting period) a correct relayer checks whether the handler is on a + different branch than the relayer. + And we would like that this is enough to achieve + the Handler requirement. + + - here the correctness argument would be easy if a correct relayer is + based on a light client with a *trusted* state, that is, a light + client who never changes its opinion about trusted. Then if such a + correct relayer checks-in with a handler, it will detect a fork, and + act in time. + + - if the light client does not provide this interface, in the case of + a fork, we need some assumption about a correct relayer being on a + different branch than the handler, and we need such a relayer to + check-in not too late. Also + what happens if the relayer's light client is forced to roll-back + its lightstore? + Does it have to re-check all handlers? + +## On the interconnectedness of things + +In the broader discussion of so-called "fork accountability" there are +several subproblems + +- Fork detection + +- Evidence creation and submission + +- Isolating misbehaving nodes (and report them for punishment over abci) + +### Fork detection + +The preliminary specification ./detection.md formalizes the notion of +a fork. Roughly, a fork exists if there are two conflicting headers +for the same height, where both are supported by bonded full nodes +(that have been validators in the near past, that is, within the +trusting period). We distinguish between *fork on the chain* where two +conflicting blocks are signed by +2/3 of the validators of that +height, and a *light client fork* where one of the conflicting headers +is not signed by +2/3 of the current height, but by +1/3 of the +validators of some smaller height. + +In principle everyone can detect a fork + +- ./detection talks about the Tendermint light client with a focus on + light nodes. A relayer runs such light clients and may detect + forks in this way + +- in IBC, a relayer can see that a handler is on a conflicting branch + - the relayer should feed the handler the necessary information so + that it can halt + - the relayer should report the fork to a full node + +### Evidence creation and submission + +- the information sent from the relayer to the handler could be called + evidence, but this is perhaps a bad idea because the information sent to a + full node can also be called evidence. But this evidence might still + not be enough as the full node might need to run the "fork + accountability" protocol to generate evidence in the form of + consensus messages. So perhaps we should + introduce different terms for: + + - proof of fork for the handler (basically consisting of lightblocks) + - proof of fork for a full node (basically consisting of (fewer) lightblocks) + - proof of misbehavior (consensus messages) + +### Isolating misbehaving nodes + +- this is the job of a full node. + +- might be subjective in the future: the protocol depends on what the + full node believes is the "correct" chain. Right now we postulate + that every full node is on the correct chain, that is, there is no + fork on the chain. + +- The full node figures out which nodes are + - lunatic + - double signing + - amnesic; **using the challenge response protocol** + +- We do not punish "phantom" validators + - currently we understand a phantom validator as a node that + - signs a block for a height in which it is not in the + validator set + - the node is not part of the +1/3 of previous validators that + are used to support the header. Whether we call a validator + phantom might be subjective and depend on the header we + check against. Their formalization actually seems not so + clear. + - they can only do something if there are +1/3 faulty validators + that are either lunatic, double signing, or amnesic. + - abci requires that we only report bonded validators. So if a + node is a "phantom", we would need the check whether the node is + bonded, which currently is expensive, as it requires checking + blocks from the last three weeks. + - in the future, with state sync, a correct node might be + convinced by faulty nodes that it is in the validator set. Then + it might appear to be "phantom" although it behaves correctly + +## Next steps + +> The following points are subject to my limited knowledge of the +> state of the work on IBC. Some/most of it might already exist and we +> will just need to bring everything together. + +- "proof of fork for a full node" defines a clean interface between + fork detection and misbehavior isolation. So it should be produced + by protocols (light client, the relayer). So we should fix that + first. + +- Given the problems of not having a light client architecture spec, + for the relayer we should start with this. E.g. + + - the relayer runs light clients for two chains + - the relayer regularly queries consensus state of a handler + - the relayer needs to check the consensus state + - this involves local checks + - this involves calling the light client + - the relayer uses the light client to do IBC business (channels, + packets, connections, etc.) + - the relayer submits proof of fork to handlers and full nodes + +> the list is definitely not complete. I think part of this +> (perhaps all) is +> covered by what Anca presented recently. + +We will need to define what we expect from these components + +- for the parts where the relayer talks to the handler, we need to fix + the interface, and what the handler does + +- we write specs for these components. diff --git a/spec/light-client/experiments.png b/spec/light-client/experiments.png new file mode 100644 index 0000000000000000000000000000000000000000..94166ffa31ee93970b8b97cafe375ba580c1d319 GIT binary patch literal 83681 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 z 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 Another related line is IBC attack detection and submission at the +> relayer, as well as attack verification at the IBC handler. This +> will call for yet another spec. + +# Status + +This document is work in progress. In order to develop the +specification step-by-step, +it assumes certain details of [verification](https://informal.systems) and +[detection](https://informal.systems) that are not specified in the respective current +versions yet. This inconsistencies will be addresses over several +upcoming PRs. + +# Part I - Tendermint Blockchain + +See [verification spec](addLinksWhenDone) + +# Part II - Sequential Problem Definition + +#### **[LC-SEQ-INIT-LIVE.1]** + +Upon initialization, the light client gets as input a header of the +blockchain, or the genesis file of the blockchain, and eventually +stores a header of the blockchain. + +#### **[LC-SEQ-LIVE.1]** + +The light client gets a sequence of heights as inputs. For each input +height *targetHeight*, it eventually stores the header of height +*targetHeight*. + +#### **[LC-SEQ-SAFE.1]** + +The light client never stores a header which is not in the blockchain. + +# Part III - Light Client as Distributed System + +## Computational Model + +The light client communicates with remote processes only via the +[verification](TODO) and the [detection](TODO) protocols. The +respective assumptions are given there. + +## Distributed Problem Statement + +### Two Kinds of Liveness + +In case of light client attacks, the sequential problem statement +cannot always be satisfied. The lightclient cannot decide which block +is from the chain and which is not. As a result, the light client just +creates evidence, submits it, and terminates. +For the liveness property, we thus add the +possibility that instead of adding a lightblock, we also might terminate +in case there is an attack. + +#### **[LC-DIST-TERM.1]** + +The light client either runs forever or it *terminates on attack*. + +### Design choices + +#### [LC-DIST-STORE.1] + +The light client has a local data structure called LightStore +that contains light blocks (that contain a header). + +> The light store exposes functions to query and update it. They are +> specified [here](TODO:onceVerificationIsMerged). + +**TODO:** reference light store invariant [LCV-INV-LS-ROOT.2] once +verification is merged + +#### **[LC-DIST-SAFE.1]** + +It is always the case that every header in *LightStore* was +generated by an instance of Tendermint consensus. + +#### **[LC-DIST-LIVE.1]** + +Whenever the light client gets a new height *h* as input, + +- and there is +no light client attack up to height *h*, then the lightclient +eventually puts the lightblock of height *h* in the lightstore and +wait for another input. +- otherwise, that is, if there +is a light client attack on height *h*, then the light client +must perform one of the following: + - it terminates on attack. + - it eventually puts the lightblock of height *h* in the lightstore and +wait for another input. + +> Observe that the "existence of a lightclient attack" just means that some node has generated a conflicting block. It does not necessarily mean that a (faulty) peer sends such a block to "our" lightclient. Thus, even if there is an attack somewhere in the system, our lightclient might still continue to operate normally. + +### Solving the sequential specification + +[LC-DIST-SAFE.1] is guaranteed by the detector; in particular it +follows from +[[LCD-DIST-INV-STORE.1]](TODO) +[[LCD-DIST-LIVE.1]](TODO) + +# Part IV - Light Client Supervisor Protocol + +We provide a specification for a sequential Light Client Supervisor. +The local code for verification is presented by a sequential function +`Sequential-Supervisor` to highlight the control flow of this +functionality. Each lightblock is first verified with a primary, and then +cross-checked with secondaries, and if all goes well, the lightblock +is +added (with the attribute "trusted") to the +lightstore. Intermiate lightblocks that were used to verify the target +block but were not cross-checked are stored as "verified" + +> We note that if a different concurrency model is considered +> for an implementation, the semantics of the lightstore might change: +> In a concurrent implementation, we might do verification for some +> height *h*, add the +> lightblock to the lightstore, and start concurrent threads that +> +> - do verification for the next height *h' != h* +> - do cross-checking for height *h*. If we find an attack, we remove +> *h* from the lightstore. +> - the user might already start to use *h* +> +> Thus, this concurrency model changes the semantics of the +> lightstore (not all lightblocks that are read by the user are +> trusted; they may be removed if +> we find a problem). Whether this is desirable, and whether the gain in +> performance is worth it, we keep for future versions/discussion of +> lightclient protocols. + +## Definitions + +### Peers + +#### **[LC-DATA-PEERS.1]:** + +A fixed set of full nodes is provided in the configuration upon +initialization. Initially this set is partitioned into + +- one full node that is the *primary* (singleton set), +- a set *Secondaries* (of fixed size, e.g., 3), +- a set *FullNodes*; it excludes *primary* and *Secondaries* nodes. +- A set *FaultyNodes* of nodes that the light client suspects of + being faulty; it is initially empty + +#### **[LC-INV-NODES.1]:** + +The detector shall maintain the following invariants: + +- *FullNodes \intersect Secondaries = {}* +- *FullNodes \intersect FaultyNodes = {}* +- *Secondaries \intersect FaultyNodes = {}* + +and the following transition invariant + +- *FullNodes' \union Secondaries' \union FaultyNodes' = FullNodes + \union Secondaries \union FaultyNodes* + +#### **[LC-FUNC-REPLACE-PRIMARY.1]:** + +```go +Replace_Primary(root-of-trust LightBlock) +``` + +- Implementation remark + - the primary is replaced by a secondary + - to maintain a constant size of secondaries, need to + - pick a new secondary *nsec* while ensuring [LC-INV-ROOT-AGREED.1] + - that is, we need to ensure that root-of-trust = FetchLightBlock(nsec, root-of-trust.Header.Height) +- Expected precondition + - *FullNodes* is nonempty +- Expected postcondition + - *primary* is moved to *FaultyNodes* + - a secondary *s* is moved from *Secondaries* to primary +- Error condition + - if precondition is violated + +#### **[LC-FUNC-REPLACE-SECONDARY.1]:** + +```go +Replace_Secondary(addr Address, root-of-trust LightBlock) +``` + +- Implementation remark + - maintain [LC-INV-ROOT-AGREED.1], that is, + ensure root-of-trust = FetchLightBlock(nsec, root-of-trust.Header.Height) +- Expected precondition + - *FullNodes* is nonempty +- Expected postcondition + - addr is moved from *Secondaries* to *FaultyNodes* + - an address *nsec* is moved from *FullNodes* to *Secondaries* +- Error condition + - if precondition is violated + +### Data Types + +The core data structure of the protocol is the LightBlock. + +#### **[LC-DATA-LIGHTBLOCK.1]** + +```go +type LightBlock struct { + Header Header + Commit Commit + Validators ValidatorSet + NextValidators ValidatorSet + Provider PeerID +} +``` + +#### **[LC-DATA-LIGHTSTORE.1]** + +LightBlocks are stored in a structure which stores all LightBlock from +initialization or received from peers. + +```go +type LightStore struct { + ... +} + +``` + +We use the functions that the LightStore exposes, which +are defined in the [verification specification](TODO). + +### Inputs + +The lightclient is initialized with LCInitData + +#### **[LC-DATA-INIT.1]** + +```go +type LCInitData struct { + lightBlock LightBlock + genesisDoc GenesisDoc +} +``` + +where only one of the components must be provided. `GenesisDoc` is +defined in the [Tendermint +Types](https://github.com/tendermint/tendermint/blob/master/types/genesis.go). + +#### **[LC-DATA-GENESIS.1]** + +```go +type GenesisDoc struct { + GenesisTime time.Time `json:"genesis_time"` + ChainID string `json:"chain_id"` + InitialHeight int64 `json:"initial_height"` + ConsensusParams *tmproto.ConsensusParams `json:"consensus_params,omitempty"` + Validators []GenesisValidator `json:"validators,omitempty"` + AppHash tmbytes.HexBytes `json:"app_hash"` + AppState json.RawMessage `json:"app_state,omitempty"` +} +``` + +We use the following function +`makeblock` so that we create a lightblock from the genesis +file in order to do verification based on the data from the genesis +file using the same verification function we use in normal operation. + +#### **[LC-FUNC-MAKEBLOCK.1]** + +```go +func makeblock (genesisDoc GenesisDoc) (lightBlock LightBlock)) +``` + +- Implementation remark + - none +- Expected precondition + - none +- Expected postcondition + - lightBlock.Header.Height = genesisDoc.InitialHeight + - lightBlock.Header.Time = genesisDoc.GenesisTime + - lightBlock.Header.LastBlockID = nil + - lightBlock.Header.LastCommit = nil + - lightBlock.Header.Validators = genesisDoc.Validators + - lightBlock.Header.NextValidators = genesisDoc.Validators + - lightBlock.Header.Data = nil + - lightBlock.Header.AppState = genesisDoc.AppState + - lightBlock.Header.LastResult = nil + - lightBlock.Commit = nil + - lightBlock.Validators = genesisDoc.Validators + - lightBlock.NextValidators = genesisDoc.Validators + - lightBlock.Provider = nil +- Error condition + - none + +---- + +### Configuration Parameters + +#### **[LC-INV-ROOT-AGREED.1]** + +In the Sequential-Supervisor, it is always the case that the primary +and all secondaries agree on lightStore.Latest(). + +### Assumptions + +We have to assume that the initialization data (the lightblock or the +genesis file) are consistent with the blockchain. This is subjective +initialization and it cannot be checked locally. + +### Invariants + +#### **[LC-INV-PEERLIST.1]:** + +The peer list contains a primary and a secondary. + +> If the invariant is violated, the light client does not have enough +> peers to download headers from. As a result, the light client +> needs to terminate in case this invariant is violated. + +## Supervisor + +### Outline + +The supervisor implements the functionality of the lightclient. It is +initialized with a genesis file or with a lightblock the user +trusts. This initialization is subjective, that is, the security of +the lightclient is based on the validity of the input. If the genesis +file or the lightblock deviate from the actual ones on the blockchain, +the lightclient provides no guarantees. + +After initialization, the supervisor awaits an input, that is, the +height of the next lightblock that should be obtained. Then it +downloads, verifies, and cross-checks a lightblock, and if all tests +go through, the light block (and possibly other lightblocks) are added +to the lightstore, which is returned in an output event to the user. + +The following main loop does the interaction with the user (input, +output) and calls the following two functions: + +- `InitLightClient`: it initializes the lightstore either with the + provided lightblock or with the lightblock that corresponds to the + first block generated by the blockchain (by the validators defined + by the genesis file) +- `VerifyAndDetect`: takes as input a lightstore and a height and + returns the updated lightstore. + +#### **[LC-FUNC-SUPERVISOR.1]:** + +```go +func Sequential-Supervisor (initdata LCInitData) (Error) { + + lightStore,result := InitLightClient(initData); + if result != OK { + return result; + } + + loop { + // get the next height + nextHeight := input(); + + lightStore,result := VerifyAndDetect(lightStore, nextHeight); + + if result == OK { + output(LightStore.Get(targetHeight)); + // we only output a trusted lightblock + } + else { + return result + } + // QUESTION: is it OK to generate output event in normal case, + // and terminate with failure in the (light client) attack case? + } +} +``` + +- Implementation remark + - infinite loop unless a light client attack is detected + - In typical implementations (e.g., the one in Rust), + there are mutliple input actions: + `VerifytoLatest`, `LatestTrusted`, and `GetStatus`. The + information can be easily obtained from the lightstore, so that + we do not treat these requests explicitly here but just consider + the request for a block of a given height which requires more + involved computation and communication. +- Expected precondition + - *LCInitData* contains a genesis file or a lightblock. +- Expected postcondition + - if a light client attack is detected: it stops and submits + evidence (in `InitLightClient` or `VerifyAndDetect`) + - otherwise: non. It runs forever. +- Invariant: *lightStore* contains trusted lightblocks only. +- Error condition + - if `InitLightClient` or `VerifyAndDetect` fails (if a attack is + detected, or if [LCV-INV-TP.1] is violated) + +---- + +### Details of the Functions + +#### Initialization + +The light client is based on subjective initialization. It has to +trust the initial data given to it by the user. It cannot do any +detection of attack. So either upon initialization we obtain a +lightblock and just initialize the lightstore with it. Or in case of a +genesis file, we download, verify, and cross-check the first block, to +initialize the lightstore with this first block. The reason is that +we want to maintain [LCV-INV-TP.1] from the beginning. + +> If the lightclient is initialized with a lightblock, one might think +> it may increase trust, when one cross-checks the initial light +> block. However, if a peer provides a conflicting +> lightblock, the question is to distinguish the case of a +> [bogus](https://informal.systems) block (upon which operation should proceed) from a +> [light client attack](https://informal.systems) (upon which operation should stop). In +> case of a bogus block, the lightclient might be forced to do +> backwards verification until the blocks are out of the trusting +> period, to make sure no previous validator set could have generated +> the bogus block, which effectively opens up a DoS attack on the lightclient +> without adding effective robustness. + +#### **[LC-FUNC-INIT.1]:** + +```go +func InitLightClient (initData LCInitData) (LightStore, Error) { + + if LCInitData.LightBlock != nil { + // we trust the provided initial block. + newblock := LCInitData.LightBlock + } + else { + genesisBlock := makeblock(initData.genesisDoc); + + result := NoResult; + while result != ResultSuccess { + current = FetchLightBlock(PeerList.primary(), genesisBlock.Header.Height + 1) + // QUESTION: is the height with "+1" OK? + + if CANNOT_VERIFY = ValidAndVerify(genesisBlock, current) { + Replace_Primary(); + } + else { + result = ResultSuccess + } + } + + // cross-check + auxLS := new LightStore + auxLS.Add(current) + Evidences := AttackDetector(genesisBlock, auxLS) + if Evidences.Empty { + newBlock := current + } + else { + // [LC-SUMBIT-EVIDENCE.1] + submitEvidence(Evidences); + return(nil, ErrorAttack); + } + } + + lightStore := new LightStore; + lightStore.Add(newBlock); + return (lightStore, OK); +} + +``` + +- Implementation remark + - none +- Expected precondition + - *LCInitData* contains either a genesis file of a lightblock + - if genesis it passes `ValidateAndComplete()` see [Tendermint](https://informal.systems) +- Expected postcondition + - *lightStore* initialized with trusted lightblock. It has either been + cross-checked (from genesis) or it has initial trust from the + user. +- Error condition + - if precondition is violated + - empty peerList + +---- + +#### Main verification and detection logic + +#### **[LC-FUNC-MAIN-VERIF-DETECT.1]:** + +```go +func VerifyAndDetect (lightStore LightStore, targetHeight Height) + (LightStore, Result) { + + b1, r1 = lightStore.Get(targetHeight) + if r1 == true { + if b1.State == StateTrusted { + // block already there and trusted + return (lightStore, ResultSuccess) + } + else { + // We have a lightblock in the store, but it has not been + // cross-checked by now. We do that now. + root_of_trust, auxLS := lightstore.TraceTo(b1); + + // Cross-check + Evidences := AttackDetector(root_of_trust, auxLS); + if Evidences.Empty { + // no attack detected, we trust the new lightblock + lightStore.Update(auxLS.Latest(), + StateTrusted, + verfiedLS.Latest().verification-root); + return (lightStore, OK); + } + else { + // there is an attack, we exit + submitEvidence(Evidences); + return(lightStore, ErrorAttack); + } + } + } + + // get the lightblock with maximum height smaller than targetHeight + // would typically be the heighest, if we always move forward + root_of_trust, r2 = lightStore.LatestPrevious(targetHeight); + + if r2 = false { + // there is no lightblock from which we can do forward + // (skipping) verification. Thus we have to go backwards. + // No cross-check needed. We trust hashes. Therefore, we + // directly return the result + return Backwards(primary, lightStore.Lowest(), targetHeight) + } + else { + // Forward verification + detection + result := NoResult; + while result != ResultSuccess { + verifiedLS,result := VerifyToTarget(primary, + root_of_trust, + nextHeight); + if result == ResultFailure { + // pick new primary (promote a secondary to primary) + Replace_Primary(root_of_trust); + } + else if result == ResultExpired { + return (lightStore, result) + } + } + + // Cross-check + Evidences := AttackDetector(root_of_trust, verifiedLS); + if Evidences.Empty { + // no attack detected, we trust the new lightblock + verifiedLS.Update(verfiedLS.Latest(), + StateTrusted, + verfiedLS.Latest().verification-root); + lightStore.store_chain(verifidLS); + return (lightStore, OK); + } + else { + // there is an attack, we exit + return(lightStore, ErrorAttack); + } + } +} +``` + +- Implementation remark + - none +- Expected precondition + - none +- Expected postcondition + - lightblock of height *targetHeight* (and possibly additional blocks) added to *lightStore* +- Error condition + - an attack is detected + - [LC-DATA-PEERLIST-INV.1] is violated + +---- \ No newline at end of file diff --git a/spec/light-client/supervisor/supervisor_001_draft.tla b/spec/light-client/supervisor/supervisor_001_draft.tla new file mode 100644 index 000000000..949a7c020 --- /dev/null +++ b/spec/light-client/supervisor/supervisor_001_draft.tla @@ -0,0 +1,71 @@ +------------------------- MODULE supervisor_001_draft ------------------------ +(* +This is the beginning of a spec that will eventually use verification and detector API +*) + +EXTENDS Integers, FiniteSets + +VARIABLES + state, + output + +vars == <> + +CONSTANT + INITDATA + +Init == + /\ state = "Init" + /\ output = "none" + +NextInit == + /\ state = "Init" + /\ \/ state' = "EnterLoop" + \/ state' = "FailedToInitialize" + /\ UNCHANGED output + +NextVerifyToTarget == + /\ state = "EnterLoop" + /\ \/ state' = "EnterLoop" \* replace primary + \/ state' = "EnterDetect" + \/ state' = "ExhaustedPeersPrimary" + /\ UNCHANGED output + +NextAttackDetector == + /\ state = "EnterDetect" + /\ \/ state' = "NoEvidence" + \/ state' = "EvidenceFound" + \/ state' = "ExhaustedPeersSecondaries" + /\ UNCHANGED output + +NextVerifyAndDetect == + \/ NextVerifyToTarget + \/ NextAttackDetector + +NextOutput == + /\ state = "NoEvidence" + /\ state' = "EnterLoop" + /\ output' = "data" \* to generate a trace + +NextTerminated == + /\ \/ state = "FailedToInitialize" + \/ state = "ExhaustedPeersPrimary" + \/ state = "EvidenceFound" + \/ state = "ExhaustedPeersSecondaries" + /\ UNCHANGED vars + +Next == + \/ NextInit + \/ NextVerifyAndDetect + \/ NextOutput + \/ NextTerminated + +InvEnoughPeers == + /\ state /= "ExhaustedPeersPrimary" + /\ state /= "ExhaustedPeersSecondaries" + + +============================================================================= +\* Modification History +\* Last modified Sun Oct 18 11:48:45 CEST 2020 by widder +\* Created Sun Oct 18 11:18:53 CEST 2020 by widder diff --git a/spec/light-client/supervisor/supervisor_002_draft.md b/spec/light-client/supervisor/supervisor_002_draft.md new file mode 100644 index 000000000..0c28de393 --- /dev/null +++ b/spec/light-client/supervisor/supervisor_002_draft.md @@ -0,0 +1,131 @@ +# Draft of Light Client Supervisor for discussion + +## Modification to the initialization + +The lightclient is initialized with LCInitData + +### **[LC-DATA-INIT.2]** + +```go +type LCInitData struct { + TrustedBlock LightBlock + Genesis GenesisDoc + TrustedHash []byte + TrustedHeight int64 +} +``` + +where only one of the components must be provided. `GenesisDoc` is +defined in the [Tendermint +Types](https://github.com/tendermint/tendermint/blob/master/types/genesis.go). + + +### Initialization + +The light client is based on subjective initialization. It has to +trust the initial data given to it by the user. It cannot perform any +detection of an attack yet instead requires an initial point of trust. +There are three forms of initial data which are used to obtain the +first trusted block: + +- A trusted block from a prior initialization +- A trusted height and hash +- A genesis file + +The golang light client implementation checks this initial data in that +order; first attempting to find a trusted block from the trusted store, +then acquiring a light block from the primary at the trusted height and matching +the hash, or finally checking for a genesis file to verify the initial header. + +The light client doesn't need to check if the trusted block is within the +trusted period because it already trusts it, however, if the light block is +outside the trust period, there is a higher chance the light client won't be +able to verify anything. + +Cross-checking this trusted block with providers upon initialization is helpful +for ensuring that the node is responsive and correctly configured but does not +increase trust since proving a conflicting block is a +[light client attack](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/light-client/detection/detection_003_reviewed.md#tmbc-lc-attack1) +and not just a [bogus](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/light-client/detection/detection_003_reviewed.md#tmbc-bogus1) block could result in +performing backwards verification beyond the trusted period, thus a fruitless +endeavour. + +However, with the notion of it's better to fail earlier than later, the golang +light client implementation will perform a consistency check on all providers +and will error if one returns a different header, allowing the user +the opportunity to reinitialize. + +#### **[LC-FUNC-INIT.2]:** + +```go +func InitLightClient(initData LCInitData) (LightStore, Error) { + var initialBlock LightBlock + + switch { + case LCInitData.TrustedBlock != nil: + // we trust the block from a prior initialization + initialBlock = LCInitData.TrustedBlock + + case LCInitData.TrustedHash != nil: + untrustedBlock := FetchLightBlock(PeerList.Primary(), LCInitData.TrustedHeight) + + + // verify that the hashes match + if untrustedBlock.Hash() != LCInitData.TrustedHash { + return nil, Error("Primary returned block with different hash") + } + // after checking the hash we now trust the block + initialBlock = untrustedBlock + } + case LCInitData.Genesis != nil: + untrustedBlock := FetchLightBlock(PeerList.Primary(), LCInitData.Genesis.InitialHeight) + + // verify that 2/3+ of the validator set signed the untrustedBlock + if err := VerifyCommitFull(untrustedBlock.Commit, LCInitData.Genesis.Validators); err != nil { + return nil, err + } + + // we can now trust the block + initialBlock = untrustedBlock + default: + return nil, Error("No initial data was provided") + + // This is done in the golang version but is optional and not strictly part of the protocol + if err := CrossCheck(initialBlock, PeerList.Witnesses()); err != nil { + return nil, err + } + + // initialize light store + lightStore := new LightStore; + lightStore.Add(newBlock); + return (lightStore, OK); +} + +func CrossCheck(lb LightBlock, witnesses []Provider) error { + for _, witness := range witnesses { + witnessBlock := FetchLightBlock(witness, lb.Height) + + if witnessBlock.Hash() != lb.Hash() { + return Error("Witness has different block") + } + } + return OK +} + +``` + +- Implementation remark + - none +- Expected precondition + - *LCInitData* contains either a genesis file of a lightblock + - if genesis it passes `ValidateAndComplete()` see [Tendermint](https://informal.systems) +- Expected postcondition + - *lightStore* initialized with trusted lightblock. It has either been + cross-checked (from genesis) or it has initial trust from the + user. +- Error condition + - if precondition is violated + - empty peerList + +---- + diff --git a/spec/light-client/verification/001bmc-apalache.csv b/spec/light-client/verification/001bmc-apalache.csv new file mode 100644 index 000000000..8d5ad8ea3 --- /dev/null +++ b/spec/light-client/verification/001bmc-apalache.csv @@ -0,0 +1,49 @@ +no,filename,tool,timeout,init,inv,next,args +1,MC4_3_correct.tla,apalache,1h,,PositiveBeforeTrustedHeaderExpires,,--length=30 +2,MC4_3_correct.tla,apalache,1h,,CorrectnessInv,,--length=30 +3,MC4_3_correct.tla,apalache,1h,,PrecisionInv,,--length=30 +4,MC4_3_correct.tla,apalache,1h,,SuccessOnCorrectPrimaryAndChainOfTrust,,--length=30 +5,MC4_3_correct.tla,apalache,1h,,NoFailedBlocksOnSuccessInv,,--length=30 +6,MC4_3_correct.tla,apalache,1h,,StoredHeadersAreVerifiedOrNotTrustedInv,,--length=30 +7,MC4_3_correct.tla,apalache,1h,,CorrectPrimaryAndTimeliness,,--length=30 +8,MC4_3_correct.tla,apalache,1h,,Complexity,,--length=30 +9,MC4_3_faulty.tla,apalache,1h,,PositiveBeforeTrustedHeaderExpires,,--length=30 +10,MC4_3_faulty.tla,apalache,1h,,CorrectnessInv,,--length=30 +11,MC4_3_faulty.tla,apalache,1h,,PrecisionInv,,--length=30 +12,MC4_3_faulty.tla,apalache,1h,,SuccessOnCorrectPrimaryAndChainOfTrust,,--length=30 +13,MC4_3_faulty.tla,apalache,1h,,NoFailedBlocksOnSuccessInv,,--length=30 +14,MC4_3_faulty.tla,apalache,1h,,StoredHeadersAreVerifiedOrNotTrustedInv,,--length=30 +15,MC4_3_faulty.tla,apalache,1h,,CorrectPrimaryAndTimeliness,,--length=30 +16,MC4_3_faulty.tla,apalache,1h,,Complexity,,--length=30 +17,MC5_5_correct.tla,apalache,1h,,PositiveBeforeTrustedHeaderExpires,,--length=30 +18,MC5_5_correct.tla,apalache,1h,,CorrectnessInv,,--length=30 +19,MC5_5_correct.tla,apalache,1h,,PrecisionInv,,--length=30 +20,MC5_5_correct.tla,apalache,1h,,SuccessOnCorrectPrimaryAndChainOfTrust,,--length=30 +21,MC5_5_correct.tla,apalache,1h,,NoFailedBlocksOnSuccessInv,,--length=30 +22,MC5_5_correct.tla,apalache,1h,,StoredHeadersAreVerifiedOrNotTrustedInv,,--length=30 +23,MC5_5_correct.tla,apalache,1h,,CorrectPrimaryAndTimeliness,,--length=30 +24,MC5_5_correct.tla,apalache,1h,,Complexity,,--length=30 +25,MC5_5_faulty.tla,apalache,1h,,PositiveBeforeTrustedHeaderExpires,,--length=30 +26,MC5_5_faulty.tla,apalache,1h,,CorrectnessInv,,--length=30 +27,MC5_5_faulty.tla,apalache,1h,,PrecisionInv,,--length=30 +28,MC5_5_faulty.tla,apalache,1h,,SuccessOnCorrectPrimaryAndChainOfTrust,,--length=30 +29,MC5_5_faulty.tla,apalache,1h,,NoFailedBlocksOnSuccessInv,,--length=30 +30,MC5_5_faulty.tla,apalache,1h,,StoredHeadersAreVerifiedOrNotTrustedInv,,--length=30 +31,MC5_5_faulty.tla,apalache,1h,,CorrectPrimaryAndTimeliness,,--length=30 +32,MC5_5_faulty.tla,apalache,1h,,Complexity,,--length=30 +33,MC7_5_faulty.tla,apalache,10h,,PositiveBeforeTrustedHeaderExpires,,--length=30 +34,MC7_5_faulty.tla,apalache,10h,,CorrectnessInv,,--length=30 +35,MC7_5_faulty.tla,apalache,10h,,PrecisionInv,,--length=30 +36,MC7_5_faulty.tla,apalache,10h,,SuccessOnCorrectPrimaryAndChainOfTrust,,--length=30 +37,MC7_5_faulty.tla,apalache,10h,,NoFailedBlocksOnSuccessInv,,--length=30 +38,MC7_5_faulty.tla,apalache,10h,,StoredHeadersAreVerifiedOrNotTrustedInv,,--length=30 +39,MC7_5_faulty.tla,apalache,10h,,CorrectPrimaryAndTimeliness,,--length=30 +40,MC7_5_faulty.tla,apalache,10h,,Complexity,,--length=30 +41,MC4_7_faulty.tla,apalache,10h,,PositiveBeforeTrustedHeaderExpires,,--length=30 +42,MC4_7_faulty.tla,apalache,10h,,CorrectnessInv,,--length=30 +43,MC4_7_faulty.tla,apalache,10h,,PrecisionInv,,--length=30 +44,MC4_7_faulty.tla,apalache,10h,,SuccessOnCorrectPrimaryAndChainOfTrust,,--length=30 +45,MC4_7_faulty.tla,apalache,10h,,NoFailedBlocksOnSuccessInv,,--length=30 +46,MC4_7_faulty.tla,apalache,10h,,StoredHeadersAreVerifiedOrNotTrustedInv,,--length=30 +47,MC4_7_faulty.tla,apalache,10h,,CorrectPrimaryAndTimeliness,,--length=30 +48,MC4_7_faulty.tla,apalache,10h,,Complexity,,--length=30 diff --git a/spec/light-client/verification/002bmc-apalache-ok.csv b/spec/light-client/verification/002bmc-apalache-ok.csv new file mode 100644 index 000000000..eb26aa89e --- /dev/null +++ b/spec/light-client/verification/002bmc-apalache-ok.csv @@ -0,0 +1,55 @@ +no;filename;tool;timeout;init;inv;next;args +1;MC4_3_correct.tla;apalache;1h;;TargetHeightOnSuccessInv;;--length=5 +2;MC4_3_correct.tla;apalache;1h;;StoredHeadersAreVerifiedOrNotTrustedInv;;--length=5 +3;MC4_3_correct.tla;apalache;1h;;CorrectnessInv;;--length=5 +4;MC4_3_correct.tla;apalache;1h;;NoTrustOnFaultyBlockInv;;--length=5 +5;MC4_3_correct.tla;apalache;1h;;ProofOfChainOfTrustInv;;--length=5 +6;MC4_3_correct.tla;apalache;1h;;NoFailedBlocksOnSuccessInv;;--length=5 +7;MC4_3_correct.tla;apalache;1h;;Complexity;;--length=5 +8;MC4_3_correct.tla;apalache;1h;;ApiPostInv;;--length=5 +9;MC4_4_correct.tla;apalache;1h;;TargetHeightOnSuccessInv;;--length=7 +10;MC4_4_correct.tla;apalache;1h;;CorrectnessInv;;--length=7 +11;MC4_4_correct.tla;apalache;1h;;NoTrustOnFaultyBlockInv;;--length=7 +12;MC4_4_correct.tla;apalache;1h;;ProofOfChainOfTrustInv;;--length=7 +13;MC4_4_correct.tla;apalache;1h;;NoFailedBlocksOnSuccessInv;;--length=7 +14;MC4_4_correct.tla;apalache;1h;;Complexity;;--length=7 +15;MC4_4_correct.tla;apalache;1h;;ApiPostInv;;--length=7 +16;MC4_5_correct.tla;apalache;1h;;TargetHeightOnSuccessInv;;--length=11 +17;MC4_5_correct.tla;apalache;1h;;CorrectnessInv;;--length=11 +18;MC4_5_correct.tla;apalache;1h;;NoTrustOnFaultyBlockInv;;--length=11 +19;MC4_5_correct.tla;apalache;1h;;ProofOfChainOfTrustInv;;--length=11 +20;MC4_5_correct.tla;apalache;1h;;NoFailedBlocksOnSuccessInv;;--length=11 +21;MC4_5_correct.tla;apalache;1h;;Complexity;;--length=11 +22;MC4_5_correct.tla;apalache;1h;;ApiPostInv;;--length=11 +23;MC5_5_correct.tla;apalache;1h;;TargetHeightOnSuccessInv;;--length=11 +24;MC5_5_correct.tla;apalache;1h;;CorrectnessInv;;--length=11 +25;MC5_5_correct.tla;apalache;1h;;NoTrustOnFaultyBlockInv;;--length=11 +26;MC5_5_correct.tla;apalache;1h;;ProofOfChainOfTrustInv;;--length=11 +27;MC5_5_correct.tla;apalache;1h;;NoFailedBlocksOnSuccessInv;;--length=11 +28;MC5_5_correct.tla;apalache;1h;;Complexity;;--length=11 +29;MC5_5_correct.tla;apalache;1h;;ApiPostInv;;--length=11 +30;MC4_3_faulty.tla;apalache;1h;;TargetHeightOnSuccessInv;;--length=5 +31;MC4_3_faulty.tla;apalache;1h;;StoredHeadersAreVerifiedOrNotTrustedInv;;--length=5 +32;MC4_3_faulty.tla;apalache;1h;;CorrectnessInv;;--length=5 +33;MC4_3_faulty.tla;apalache;1h;;NoTrustOnFaultyBlockInv;;--length=5 +34;MC4_3_faulty.tla;apalache;1h;;ProofOfChainOfTrustInv;;--length=5 +35;MC4_3_faulty.tla;apalache;1h;;NoFailedBlocksOnSuccessInv;;--length=5 +36;MC4_3_faulty.tla;apalache;1h;;Complexity;;--length=5 +37;MC4_3_faulty.tla;apalache;1h;;ApiPostInv;;--length=5 +38;MC4_4_faulty.tla;apalache;1h;;TargetHeightOnSuccessInv;;--length=7 +39;MC4_4_faulty.tla;apalache;1h;;StoredHeadersAreVerifiedOrNotTrustedInv;;--length=7 +40;MC4_4_faulty.tla;apalache;1h;;CorrectnessInv;;--length=7 +41;MC4_4_faulty.tla;apalache;1h;;NoTrustOnFaultyBlockInv;;--length=7 +42;MC4_4_faulty.tla;apalache;1h;;ProofOfChainOfTrustInv;;--length=7 +43;MC4_4_faulty.tla;apalache;1h;;NoFailedBlocksOnSuccessInv;;--length=7 +44;MC4_4_faulty.tla;apalache;1h;;Complexity;;--length=7 +45;MC4_4_faulty.tla;apalache;1h;;ApiPostInv;;--length=7 +46;MC4_5_faulty.tla;apalache;1h;;TargetHeightOnSuccessInv;;--length=11 +47;MC4_5_faulty.tla;apalache;1h;;StoredHeadersAreVerifiedOrNotTrustedInv;;--length=11 +48;MC4_5_faulty.tla;apalache;1h;;CorrectnessInv;;--length=11 +49;MC4_5_faulty.tla;apalache;1h;;NoTrustOnFaultyBlockInv;;--length=11 +50;MC4_5_faulty.tla;apalache;1h;;ProofOfChainOfTrustInv;;--length=11 +51;MC4_5_faulty.tla;apalache;1h;;NoFailedBlocksOnSuccessInv;;--length=11 +52;MC4_5_faulty.tla;apalache;1h;;Complexity;;--length=11 +53;MC4_5_faulty.tla;apalache;1h;;ApiPostInv;;--length=11 + diff --git a/spec/light-client/verification/003bmc-apalache-error.csv b/spec/light-client/verification/003bmc-apalache-error.csv new file mode 100644 index 000000000..ad5ef9654 --- /dev/null +++ b/spec/light-client/verification/003bmc-apalache-error.csv @@ -0,0 +1,45 @@ +no;filename;tool;timeout;init;inv;next;args +1;MC4_3_correct.tla;apalache;1h;;StoredHeadersAreVerifiedInv;;--length=5 +2;MC4_3_correct.tla;apalache;1h;;PositiveBeforeTrustedHeaderExpires;;--length=5 +3;MC4_3_correct.tla;apalache;1h;;CorrectPrimaryAndTimeliness;;--length=5 +4;MC4_3_correct.tla;apalache;1h;;PrecisionInv;;--length=5 +5;MC4_3_correct.tla;apalache;1h;;PrecisionBuggyInv;;--length=5 +6;MC4_3_correct.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustGlobal;;--length=5 +7;MC4_3_correct.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustLocal;;--length=5 +8;MC4_4_correct.tla;apalache;1h;;StoredHeadersAreVerifiedInv;;--length=7 +9;MC4_4_correct.tla;apalache;1h;;PositiveBeforeTrustedHeaderExpires;;--length=7 +10;MC4_4_correct.tla;apalache;1h;;CorrectPrimaryAndTimeliness;;--length=7 +11;MC4_4_correct.tla;apalache;1h;;PrecisionInv;;--length=7 +12;MC4_4_correct.tla;apalache;1h;;PrecisionBuggyInv;;--length=7 +13;MC4_4_correct.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustGlobal;;--length=7 +14;MC4_4_correct.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustLocal;;--length=7 +15;MC4_5_correct.tla;apalache;1h;;StoredHeadersAreVerifiedInv;;--length=11 +16;MC4_5_correct.tla;apalache;1h;;PositiveBeforeTrustedHeaderExpires;;--length=11 +17;MC4_5_correct.tla;apalache;1h;;CorrectPrimaryAndTimeliness;;--length=11 +18;MC4_5_correct.tla;apalache;1h;;PrecisionInv;;--length=11 +19;MC4_5_correct.tla;apalache;1h;;PrecisionBuggyInv;;--length=11 +20;MC4_5_correct.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustGlobal;;--length=11 +21;MC4_5_correct.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustLocal;;--length=11 +22;MC4_5_correct.tla;apalache;1h;;StoredHeadersAreVerifiedOrNotTrustedInv;;--length=11 +23;MC4_3_faulty.tla;apalache;1h;;StoredHeadersAreVerifiedInv;;--length=5 +24;MC4_3_faulty.tla;apalache;1h;;PositiveBeforeTrustedHeaderExpires;;--length=5 +25;MC4_3_faulty.tla;apalache;1h;;CorrectPrimaryAndTimeliness;;--length=5 +26;MC4_3_faulty.tla;apalache;1h;;PrecisionInv;;--length=5 +27;MC4_3_faulty.tla;apalache;1h;;PrecisionBuggyInv;;--length=5 +28;MC4_3_faulty.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustGlobal;;--length=5 +29;MC4_3_faulty.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustLocal;;--length=5 +30;MC4_4_faulty.tla;apalache;1h;;StoredHeadersAreVerifiedInv;;--length=7 +31;MC4_4_faulty.tla;apalache;1h;;PositiveBeforeTrustedHeaderExpires;;--length=7 +32;MC4_4_faulty.tla;apalache;1h;;CorrectPrimaryAndTimeliness;;--length=7 +33;MC4_4_faulty.tla;apalache;1h;;PrecisionInv;;--length=7 +34;MC4_4_faulty.tla;apalache;1h;;PrecisionBuggyInv;;--length=7 +35;MC4_4_faulty.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustGlobal;;--length=7 +36;MC4_4_faulty.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustLocal;;--length=7 +37;MC4_5_faulty.tla;apalache;1h;;StoredHeadersAreVerifiedInv;;--length=11 +38;MC4_5_faulty.tla;apalache;1h;;PositiveBeforeTrustedHeaderExpires;;--length=11 +39;MC4_5_faulty.tla;apalache;1h;;CorrectPrimaryAndTimeliness;;--length=11 +40;MC4_5_faulty.tla;apalache;1h;;PrecisionInv;;--length=11 +41;MC4_5_faulty.tla;apalache;1h;;PrecisionBuggyInv;;--length=11 +42;MC4_5_faulty.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustGlobal;;--length=11 +43;MC4_5_faulty.tla;apalache;1h;;SuccessOnCorrectPrimaryAndChainOfTrustLocal;;--length=11 +44;MC4_5_faulty.tla;apalache;1h;;StoredHeadersAreVerifiedOrNotTrustedInv;;--length=11 diff --git a/spec/light-client/verification/004bmc-apalache-ok.csv b/spec/light-client/verification/004bmc-apalache-ok.csv new file mode 100644 index 000000000..bf4f53ea2 --- /dev/null +++ b/spec/light-client/verification/004bmc-apalache-ok.csv @@ -0,0 +1,10 @@ +no;filename;tool;timeout;init;inv;next;args +1;LCD_MC3_3_faulty.tla;apalache;1h;;CommonHeightOnEvidenceInv;;--length=10 +2;LCD_MC3_3_faulty.tla;apalache;1h;;AccuracyInv;;--length=10 +3;LCD_MC3_3_faulty.tla;apalache;1h;;PrecisionInvLocal;;--length=10 +4;LCD_MC3_4_faulty.tla;apalache;1h;;CommonHeightOnEvidenceInv;;--length=10 +5;LCD_MC3_4_faulty.tla;apalache;1h;;AccuracyInv;;--length=10 +6;LCD_MC3_4_faulty.tla;apalache;1h;;PrecisionInvLocal;;--length=10 +7;LCD_MC4_4_faulty.tla;apalache;1h;;CommonHeightOnEvidenceInv;;--length=10 +8;LCD_MC4_4_faulty.tla;apalache;1h;;AccuracyInv;;--length=10 +9;LCD_MC4_4_faulty.tla;apalache;1h;;PrecisionInvLocal;;--length=10 diff --git a/spec/light-client/verification/005bmc-apalache-error.csv b/spec/light-client/verification/005bmc-apalache-error.csv new file mode 100644 index 000000000..1b9dd05ca --- /dev/null +++ b/spec/light-client/verification/005bmc-apalache-error.csv @@ -0,0 +1,4 @@ +no;filename;tool;timeout;init;inv;next;args +1;LCD_MC3_3_faulty.tla;apalache;1h;;PrecisionInvGrayZone;;--length=10 +2;LCD_MC3_4_faulty.tla;apalache;1h;;PrecisionInvGrayZone;;--length=10 +3;LCD_MC4_4_faulty.tla;apalache;1h;;PrecisionInvGrayZone;;--length=10 diff --git a/spec/light-client/verification/Blockchain_002_draft.tla b/spec/light-client/verification/Blockchain_002_draft.tla new file mode 100644 index 000000000..f2ca5aba5 --- /dev/null +++ b/spec/light-client/verification/Blockchain_002_draft.tla @@ -0,0 +1,171 @@ +------------------------ MODULE Blockchain_002_draft ----------------------------- +(* + This is a high-level specification of Tendermint blockchain + that is designed specifically for the light client. + Validators have the voting power of one. If you like to model various + voting powers, introduce multiple copies of the same validator + (do not forget to give them unique names though). + *) +EXTENDS Integers, FiniteSets + +Min(a, b) == IF a < b THEN a ELSE b + +CONSTANT + AllNodes, + (* a set of all nodes that can act as validators (correct and faulty) *) + ULTIMATE_HEIGHT, + (* a maximal height that can be ever reached (modelling artifact) *) + TRUSTING_PERIOD + (* the period within which the validators are trusted *) + +Heights == 1..ULTIMATE_HEIGHT (* possible heights *) + +(* A commit is just a set of nodes who have committed the block *) +Commits == SUBSET AllNodes + +(* The set of all block headers that can be on the blockchain. + This is a simplified version of the Block data structure in the actual implementation. *) +BlockHeaders == [ + height: Heights, + \* the block height + time: Int, + \* the block timestamp in some integer units + lastCommit: Commits, + \* the nodes who have voted on the previous block, the set itself instead of a hash + (* in the implementation, only the hashes of V and NextV are stored in a block, + as V and NextV are stored in the application state *) + VS: SUBSET AllNodes, + \* the validators of this bloc. We store the validators instead of the hash. + NextVS: SUBSET AllNodes + \* the validators of the next block. We store the next validators instead of the hash. +] + +(* A signed header is just a header together with a set of commits *) +LightBlocks == [header: BlockHeaders, Commits: Commits] + +VARIABLES + now, + (* the current global time in integer units *) + blockchain, + (* A sequence of BlockHeaders, which gives us a bird view of the blockchain. *) + Faulty + (* A set of faulty nodes, which can act as validators. We assume that the set + of faulty processes is non-decreasing. If a process has recovered, it should + connect using a different id. *) + +(* all variables, to be used with UNCHANGED *) +vars == <> + +(* The set of all correct nodes in a state *) +Corr == AllNodes \ Faulty + +(* APALACHE annotations *) +a <: b == a \* type annotation + +NT == STRING +NodeSet(S) == S <: {NT} +EmptyNodeSet == NodeSet({}) + +BT == [height |-> Int, time |-> Int, lastCommit |-> {NT}, VS |-> {NT}, NextVS |-> {NT}] + +LBT == [header |-> BT, Commits |-> {NT}] +(* end of APALACHE annotations *) + +(****************************** BLOCKCHAIN ************************************) + +(* the header is still within the trusting period *) +InTrustingPeriod(header) == + now < header.time + TRUSTING_PERIOD + +(* + Given a function pVotingPower \in D -> Powers for some D \subseteq AllNodes + and pNodes \subseteq D, test whether the set pNodes \subseteq AllNodes has + more than 2/3 of voting power among the nodes in D. + *) +TwoThirds(pVS, pNodes) == + LET TP == Cardinality(pVS) + SP == Cardinality(pVS \intersect pNodes) + IN + 3 * SP > 2 * TP \* when thinking in real numbers, not integers: SP > 2.0 / 3.0 * TP + +(* + Given a set of FaultyNodes, test whether the voting power of the correct nodes in D + is more than 2/3 of the voting power of the faulty nodes in D. + *) +IsCorrectPower(pFaultyNodes, pVS) == + LET FN == pFaultyNodes \intersect pVS \* faulty nodes in pNodes + CN == pVS \ pFaultyNodes \* correct nodes in pNodes + CP == Cardinality(CN) \* power of the correct nodes + FP == Cardinality(FN) \* power of the faulty nodes + IN + \* CP + FP = TP is the total voting power, so we write CP > 2.0 / 3 * TP as follows: + CP > 2 * FP \* Note: when FP = 0, this implies CP > 0. + +(* This is what we believe is the assumption about failures in Tendermint *) +FaultAssumption(pFaultyNodes, pNow, pBlockchain) == + \A h \in Heights: + pBlockchain[h].time + TRUSTING_PERIOD > pNow => + IsCorrectPower(pFaultyNodes, pBlockchain[h].NextVS) + +(* Can a block be produced by a correct peer, or an authenticated Byzantine peer *) +IsLightBlockAllowedByDigitalSignatures(ht, block) == + \/ block.header = blockchain[ht] \* signed by correct and faulty (maybe) + \/ block.Commits \subseteq Faulty /\ block.header.height = ht /\ block.header.time >= 0 \* signed only by faulty + +(* + Initialize the blockchain to the ultimate height right in the initial states. + We pick the faulty validators statically, but that should not affect the light client. + *) +InitToHeight == + /\ Faulty \in SUBSET AllNodes \* some nodes may fail + \* pick the validator sets and last commits + /\ \E vs, lastCommit \in [Heights -> SUBSET AllNodes]: + \E timestamp \in [Heights -> Int]: + \* now is at least as early as the timestamp in the last block + /\ \E tm \in Int: now = tm /\ tm >= timestamp[ULTIMATE_HEIGHT] + \* the genesis starts on day 1 + /\ timestamp[1] = 1 + /\ vs[1] = AllNodes + /\ lastCommit[1] = EmptyNodeSet + /\ \A h \in Heights \ {1}: + /\ lastCommit[h] \subseteq vs[h - 1] \* the non-validators cannot commit + /\ TwoThirds(vs[h - 1], lastCommit[h]) \* the commit has >2/3 of validator votes + /\ IsCorrectPower(Faulty, vs[h]) \* the correct validators have >2/3 of power + /\ timestamp[h] > timestamp[h - 1] \* the time grows monotonically + /\ timestamp[h] < timestamp[h - 1] + TRUSTING_PERIOD \* but not too fast + \* form the block chain out of validator sets and commits (this makes apalache faster) + /\ blockchain = [h \in Heights |-> + [height |-> h, + time |-> timestamp[h], + VS |-> vs[h], + NextVS |-> IF h < ULTIMATE_HEIGHT THEN vs[h + 1] ELSE AllNodes, + lastCommit |-> lastCommit[h]] + ] \****** + + +(* is the blockchain in the faulty zone where the Tendermint security model does not apply *) +InFaultyZone == + ~FaultAssumption(Faulty, now, blockchain) + +(********************* BLOCKCHAIN ACTIONS ********************************) +(* + Advance the clock by zero or more time units. + *) +AdvanceTime == + \E tm \in Int: tm >= now /\ now' = tm + /\ UNCHANGED <> + +(* + One more process fails. As a result, the blockchain may move into the faulty zone. + The light client is not using this action, as the faults are picked in the initial state. + However, this action may be useful when reasoning about fork detection. + *) +OneMoreFault == + /\ \E n \in AllNodes \ Faulty: + /\ Faulty' = Faulty \cup {n} + /\ Faulty' /= AllNodes \* at least process remains non-faulty + /\ UNCHANGED <> +============================================================================= +\* Modification History +\* Last modified Wed Jun 10 14:10:54 CEST 2020 by igor +\* Created Fri Oct 11 15:45:11 CEST 2019 by igor diff --git a/spec/light-client/verification/Blockchain_003_draft.tla b/spec/light-client/verification/Blockchain_003_draft.tla new file mode 100644 index 000000000..2b37c1b18 --- /dev/null +++ b/spec/light-client/verification/Blockchain_003_draft.tla @@ -0,0 +1,164 @@ +------------------------ MODULE Blockchain_003_draft ----------------------------- +(* + This is a high-level specification of Tendermint blockchain + that is designed specifically for the light client. + Validators have the voting power of one. If you like to model various + voting powers, introduce multiple copies of the same validator + (do not forget to give them unique names though). + *) +EXTENDS Integers, FiniteSets + +Min(a, b) == IF a < b THEN a ELSE b + +CONSTANT + AllNodes, + (* a set of all nodes that can act as validators (correct and faulty) *) + ULTIMATE_HEIGHT, + (* a maximal height that can be ever reached (modelling artifact) *) + TRUSTING_PERIOD + (* the period within which the validators are trusted *) + +Heights == 1..ULTIMATE_HEIGHT (* possible heights *) + +(* A commit is just a set of nodes who have committed the block *) +Commits == SUBSET AllNodes + +(* The set of all block headers that can be on the blockchain. + This is a simplified version of the Block data structure in the actual implementation. *) +BlockHeaders == [ + height: Heights, + \* the block height + time: Int, + \* the block timestamp in some integer units + lastCommit: Commits, + \* the nodes who have voted on the previous block, the set itself instead of a hash + (* in the implementation, only the hashes of V and NextV are stored in a block, + as V and NextV are stored in the application state *) + VS: SUBSET AllNodes, + \* the validators of this bloc. We store the validators instead of the hash. + NextVS: SUBSET AllNodes + \* the validators of the next block. We store the next validators instead of the hash. +] + +(* A signed header is just a header together with a set of commits *) +LightBlocks == [header: BlockHeaders, Commits: Commits] + +VARIABLES + refClock, + (* the current global time in integer units as perceived by the reference chain *) + blockchain, + (* A sequence of BlockHeaders, which gives us a bird view of the blockchain. *) + Faulty + (* A set of faulty nodes, which can act as validators. We assume that the set + of faulty processes is non-decreasing. If a process has recovered, it should + connect using a different id. *) + +(* all variables, to be used with UNCHANGED *) +vars == <> + +(* The set of all correct nodes in a state *) +Corr == AllNodes \ Faulty + +(* APALACHE annotations *) +a <: b == a \* type annotation + +NT == STRING +NodeSet(S) == S <: {NT} +EmptyNodeSet == NodeSet({}) + +BT == [height |-> Int, time |-> Int, lastCommit |-> {NT}, VS |-> {NT}, NextVS |-> {NT}] + +LBT == [header |-> BT, Commits |-> {NT}] +(* end of APALACHE annotations *) + +(****************************** BLOCKCHAIN ************************************) + +(* the header is still within the trusting period *) +InTrustingPeriod(header) == + refClock < header.time + TRUSTING_PERIOD + +(* + Given a function pVotingPower \in D -> Powers for some D \subseteq AllNodes + and pNodes \subseteq D, test whether the set pNodes \subseteq AllNodes has + more than 2/3 of voting power among the nodes in D. + *) +TwoThirds(pVS, pNodes) == + LET TP == Cardinality(pVS) + SP == Cardinality(pVS \intersect pNodes) + IN + 3 * SP > 2 * TP \* when thinking in real numbers, not integers: SP > 2.0 / 3.0 * TP + +(* + Given a set of FaultyNodes, test whether the voting power of the correct nodes in D + is more than 2/3 of the voting power of the faulty nodes in D. + + Parameters: + - pFaultyNodes is a set of nodes that are considered faulty + - pVS is a set of all validators, maybe including Faulty, intersecting with it, etc. + - pMaxFaultRatio is a pair <> that limits the ratio a / b of the faulty + validators from above (exclusive) + *) +FaultyValidatorsFewerThan(pFaultyNodes, pVS, maxRatio) == + LET FN == pFaultyNodes \intersect pVS \* faulty nodes in pNodes + CN == pVS \ pFaultyNodes \* correct nodes in pNodes + CP == Cardinality(CN) \* power of the correct nodes + FP == Cardinality(FN) \* power of the faulty nodes + IN + \* CP + FP = TP is the total voting power + LET TP == CP + FP IN + FP * maxRatio[2] < TP * maxRatio[1] + +(* Can a block be produced by a correct peer, or an authenticated Byzantine peer *) +IsLightBlockAllowedByDigitalSignatures(ht, block) == + \/ block.header = blockchain[ht] \* signed by correct and faulty (maybe) + \/ /\ block.Commits \subseteq Faulty + /\ block.header.height = ht + /\ block.header.time >= 0 \* signed only by faulty + +(* + Initialize the blockchain to the ultimate height right in the initial states. + We pick the faulty validators statically, but that should not affect the light client. + + Parameters: + - pMaxFaultyRatioExclusive is a pair <> that bound the number of + faulty validators in each block by the ratio a / b (exclusive) + *) +InitToHeight(pMaxFaultyRatioExclusive) == + /\ Faulty \in SUBSET AllNodes \* some nodes may fail + \* pick the validator sets and last commits + /\ \E vs, lastCommit \in [Heights -> SUBSET AllNodes]: + \E timestamp \in [Heights -> Int]: + \* refClock is at least as early as the timestamp in the last block + /\ \E tm \in Int: refClock = tm /\ tm >= timestamp[ULTIMATE_HEIGHT] + \* the genesis starts on day 1 + /\ timestamp[1] = 1 + /\ vs[1] = AllNodes + /\ lastCommit[1] = EmptyNodeSet + /\ \A h \in Heights \ {1}: + /\ lastCommit[h] \subseteq vs[h - 1] \* the non-validators cannot commit + /\ TwoThirds(vs[h - 1], lastCommit[h]) \* the commit has >2/3 of validator votes + \* the faulty validators have the power below the threshold + /\ FaultyValidatorsFewerThan(Faulty, vs[h], pMaxFaultyRatioExclusive) + /\ timestamp[h] > timestamp[h - 1] \* the time grows monotonically + /\ timestamp[h] < timestamp[h - 1] + TRUSTING_PERIOD \* but not too fast + \* form the block chain out of validator sets and commits (this makes apalache faster) + /\ blockchain = [h \in Heights |-> + [height |-> h, + time |-> timestamp[h], + VS |-> vs[h], + NextVS |-> IF h < ULTIMATE_HEIGHT THEN vs[h + 1] ELSE AllNodes, + lastCommit |-> lastCommit[h]] + ] \****** + +(********************* BLOCKCHAIN ACTIONS ********************************) +(* + Advance the clock by zero or more time units. + *) +AdvanceTime == + /\ \E tm \in Int: tm >= refClock /\ refClock' = tm + /\ UNCHANGED <> + +============================================================================= +\* Modification History +\* Last modified Wed Jun 10 14:10:54 CEST 2020 by igor +\* Created Fri Oct 11 15:45:11 CEST 2019 by igor diff --git a/spec/light-client/verification/Blockchain_A_1.tla b/spec/light-client/verification/Blockchain_A_1.tla new file mode 100644 index 000000000..70f59bf97 --- /dev/null +++ b/spec/light-client/verification/Blockchain_A_1.tla @@ -0,0 +1,171 @@ +------------------------ MODULE Blockchain_A_1 ----------------------------- +(* + This is a high-level specification of Tendermint blockchain + that is designed specifically for the light client. + Validators have the voting power of one. If you like to model various + voting powers, introduce multiple copies of the same validator + (do not forget to give them unique names though). + *) +EXTENDS Integers, FiniteSets + +Min(a, b) == IF a < b THEN a ELSE b + +CONSTANT + AllNodes, + (* a set of all nodes that can act as validators (correct and faulty) *) + ULTIMATE_HEIGHT, + (* a maximal height that can be ever reached (modelling artifact) *) + TRUSTING_PERIOD + (* the period within which the validators are trusted *) + +Heights == 1..ULTIMATE_HEIGHT (* possible heights *) + +(* A commit is just a set of nodes who have committed the block *) +Commits == SUBSET AllNodes + +(* The set of all block headers that can be on the blockchain. + This is a simplified version of the Block data structure in the actual implementation. *) +BlockHeaders == [ + height: Heights, + \* the block height + time: Int, + \* the block timestamp in some integer units + lastCommit: Commits, + \* the nodes who have voted on the previous block, the set itself instead of a hash + (* in the implementation, only the hashes of V and NextV are stored in a block, + as V and NextV are stored in the application state *) + VS: SUBSET AllNodes, + \* the validators of this bloc. We store the validators instead of the hash. + NextVS: SUBSET AllNodes + \* the validators of the next block. We store the next validators instead of the hash. +] + +(* A signed header is just a header together with a set of commits *) +LightBlocks == [header: BlockHeaders, Commits: Commits] + +VARIABLES + now, + (* the current global time in integer units *) + blockchain, + (* A sequence of BlockHeaders, which gives us a bird view of the blockchain. *) + Faulty + (* A set of faulty nodes, which can act as validators. We assume that the set + of faulty processes is non-decreasing. If a process has recovered, it should + connect using a different id. *) + +(* all variables, to be used with UNCHANGED *) +vars == <> + +(* The set of all correct nodes in a state *) +Corr == AllNodes \ Faulty + +(* APALACHE annotations *) +a <: b == a \* type annotation + +NT == STRING +NodeSet(S) == S <: {NT} +EmptyNodeSet == NodeSet({}) + +BT == [height |-> Int, time |-> Int, lastCommit |-> {NT}, VS |-> {NT}, NextVS |-> {NT}] + +LBT == [header |-> BT, Commits |-> {NT}] +(* end of APALACHE annotations *) + +(****************************** BLOCKCHAIN ************************************) + +(* the header is still within the trusting period *) +InTrustingPeriod(header) == + now <= header.time + TRUSTING_PERIOD + +(* + Given a function pVotingPower \in D -> Powers for some D \subseteq AllNodes + and pNodes \subseteq D, test whether the set pNodes \subseteq AllNodes has + more than 2/3 of voting power among the nodes in D. + *) +TwoThirds(pVS, pNodes) == + LET TP == Cardinality(pVS) + SP == Cardinality(pVS \intersect pNodes) + IN + 3 * SP > 2 * TP \* when thinking in real numbers, not integers: SP > 2.0 / 3.0 * TP + +(* + Given a set of FaultyNodes, test whether the voting power of the correct nodes in D + is more than 2/3 of the voting power of the faulty nodes in D. + *) +IsCorrectPower(pFaultyNodes, pVS) == + LET FN == pFaultyNodes \intersect pVS \* faulty nodes in pNodes + CN == pVS \ pFaultyNodes \* correct nodes in pNodes + CP == Cardinality(CN) \* power of the correct nodes + FP == Cardinality(FN) \* power of the faulty nodes + IN + \* CP + FP = TP is the total voting power, so we write CP > 2.0 / 3 * TP as follows: + CP > 2 * FP \* Note: when FP = 0, this implies CP > 0. + +(* This is what we believe is the assumption about failures in Tendermint *) +FaultAssumption(pFaultyNodes, pNow, pBlockchain) == + \A h \in Heights: + pBlockchain[h].time + TRUSTING_PERIOD > pNow => + IsCorrectPower(pFaultyNodes, pBlockchain[h].NextVS) + +(* Can a block be produced by a correct peer, or an authenticated Byzantine peer *) +IsLightBlockAllowedByDigitalSignatures(ht, block) == + \/ block.header = blockchain[ht] \* signed by correct and faulty (maybe) + \/ block.Commits \subseteq Faulty /\ block.header.height = ht \* signed only by faulty + +(* + Initialize the blockchain to the ultimate height right in the initial states. + We pick the faulty validators statically, but that should not affect the light client. + *) +InitToHeight == + /\ Faulty \in SUBSET AllNodes \* some nodes may fail + \* pick the validator sets and last commits + /\ \E vs, lastCommit \in [Heights -> SUBSET AllNodes]: + \E timestamp \in [Heights -> Int]: + \* now is at least as early as the timestamp in the last block + /\ \E tm \in Int: now = tm /\ tm >= timestamp[ULTIMATE_HEIGHT] + \* the genesis starts on day 1 + /\ timestamp[1] = 1 + /\ vs[1] = AllNodes + /\ lastCommit[1] = EmptyNodeSet + /\ \A h \in Heights \ {1}: + /\ lastCommit[h] \subseteq vs[h - 1] \* the non-validators cannot commit + /\ TwoThirds(vs[h - 1], lastCommit[h]) \* the commit has >2/3 of validator votes + /\ IsCorrectPower(Faulty, vs[h]) \* the correct validators have >2/3 of power + /\ timestamp[h] > timestamp[h - 1] \* the time grows monotonically + /\ timestamp[h] < timestamp[h - 1] + TRUSTING_PERIOD \* but not too fast + \* form the block chain out of validator sets and commits (this makes apalache faster) + /\ blockchain = [h \in Heights |-> + [height |-> h, + time |-> timestamp[h], + VS |-> vs[h], + NextVS |-> IF h < ULTIMATE_HEIGHT THEN vs[h + 1] ELSE AllNodes, + lastCommit |-> lastCommit[h]] + ] \****** + + +(* is the blockchain in the faulty zone where the Tendermint security model does not apply *) +InFaultyZone == + ~FaultAssumption(Faulty, now, blockchain) + +(********************* BLOCKCHAIN ACTIONS ********************************) +(* + Advance the clock by zero or more time units. + *) +AdvanceTime == + \E tm \in Int: tm >= now /\ now' = tm + /\ UNCHANGED <> + +(* + One more process fails. As a result, the blockchain may move into the faulty zone. + The light client is not using this action, as the faults are picked in the initial state. + However, this action may be useful when reasoning about fork detection. + *) +OneMoreFault == + /\ \E n \in AllNodes \ Faulty: + /\ Faulty' = Faulty \cup {n} + /\ Faulty' /= AllNodes \* at least process remains non-faulty + /\ UNCHANGED <> +============================================================================= +\* Modification History +\* Last modified Wed Jun 10 14:10:54 CEST 2020 by igor +\* Created Fri Oct 11 15:45:11 CEST 2019 by igor diff --git a/spec/light-client/verification/LCVerificationApi_003_draft.tla b/spec/light-client/verification/LCVerificationApi_003_draft.tla new file mode 100644 index 000000000..909eab92b --- /dev/null +++ b/spec/light-client/verification/LCVerificationApi_003_draft.tla @@ -0,0 +1,192 @@ +-------------------- MODULE LCVerificationApi_003_draft -------------------------- +(** + * The common interface of the light client verification and detection. + *) +EXTENDS Integers, FiniteSets + +\* the parameters of Light Client +CONSTANTS + TRUSTING_PERIOD, + (* the period within which the validators are trusted *) + CLOCK_DRIFT, + (* the assumed precision of the clock *) + REAL_CLOCK_DRIFT, + (* the actual clock drift, which under normal circumstances should not + be larger than CLOCK_DRIFT (otherwise, there will be a bug) *) + FAULTY_RATIO + (* a pair <> that limits that ratio of faulty validator in the blockchain + from above (exclusive). Tendermint security model prescribes 1 / 3. *) + +VARIABLES + localClock (* current time as measured by the light client *) + +(* the header is still within the trusting period *) +InTrustingPeriodLocal(header) == + \* note that the assumption about the drift reduces the period of trust + localClock < header.time + TRUSTING_PERIOD - CLOCK_DRIFT + +(* the header is still within the trusting period, even if the clock can go backwards *) +InTrustingPeriodLocalSurely(header) == + \* note that the assumption about the drift reduces the period of trust + localClock < header.time + TRUSTING_PERIOD - 2 * CLOCK_DRIFT + +(* ensure that the local clock does not drift far away from the global clock *) +IsLocalClockWithinDrift(local, global) == + /\ global - REAL_CLOCK_DRIFT <= local + /\ local <= global + REAL_CLOCK_DRIFT + +(** + * Check that the commits in an untrusted block form 1/3 of the next validators + * in a trusted header. + *) +SignedByOneThirdOfTrusted(trusted, untrusted) == + LET TP == Cardinality(trusted.header.NextVS) + SP == Cardinality(untrusted.Commits \intersect trusted.header.NextVS) + IN + 3 * SP > TP + +(** + The first part of the precondition of ValidAndVerified, which does not take + the current time into account. + + [LCV-FUNC-VALID.1::TLA-PRE-UNTIMED.1] + *) +ValidAndVerifiedPreUntimed(trusted, untrusted) == + LET thdr == trusted.header + uhdr == untrusted.header + IN + /\ thdr.height < uhdr.height + \* the trusted block has been created earlier + /\ thdr.time < uhdr.time + /\ untrusted.Commits \subseteq uhdr.VS + /\ LET TP == Cardinality(uhdr.VS) + SP == Cardinality(untrusted.Commits) + IN + 3 * SP > 2 * TP + /\ thdr.height + 1 = uhdr.height => thdr.NextVS = uhdr.VS + (* As we do not have explicit hashes we ignore these three checks of the English spec: + + 1. "trusted.Commit is a commit is for the header trusted.Header, + i.e. it contains the correct hash of the header". + 2. untrusted.Validators = hash(untrusted.Header.Validators) + 3. untrusted.NextValidators = hash(untrusted.Header.NextValidators) + *) + +(** + Check the precondition of ValidAndVerified, including the time checks. + + [LCV-FUNC-VALID.1::TLA-PRE.1] + *) +ValidAndVerifiedPre(trusted, untrusted, checkFuture) == + LET thdr == trusted.header + uhdr == untrusted.header + IN + /\ InTrustingPeriodLocal(thdr) + \* The untrusted block is not from the future (modulo clock drift). + \* Do the check, if it is required. + /\ checkFuture => uhdr.time < localClock + CLOCK_DRIFT + /\ ValidAndVerifiedPreUntimed(trusted, untrusted) + + +(** + Check, whether an untrusted block is valid and verifiable w.r.t. a trusted header. + This test does take current time into account, but only looks at the block structure. + + [LCV-FUNC-VALID.1::TLA-UNTIMED.1] + *) +ValidAndVerifiedUntimed(trusted, untrusted) == + IF ~ValidAndVerifiedPreUntimed(trusted, untrusted) + THEN "INVALID" + ELSE IF untrusted.header.height = trusted.header.height + 1 + \/ SignedByOneThirdOfTrusted(trusted, untrusted) + THEN "SUCCESS" + ELSE "NOT_ENOUGH_TRUST" + +(** + Check, whether an untrusted block is valid and verifiable w.r.t. a trusted header. + + [LCV-FUNC-VALID.1::TLA.1] + *) +ValidAndVerified(trusted, untrusted, checkFuture) == + IF ~ValidAndVerifiedPre(trusted, untrusted, checkFuture) + THEN "INVALID" + ELSE IF ~InTrustingPeriodLocal(untrusted.header) + (* We leave the following test for the documentation purposes. + The implementation should do this test, as signature verification may be slow. + In the TLA+ specification, ValidAndVerified happens in no time. + *) + THEN "FAILED_TRUSTING_PERIOD" + ELSE IF untrusted.header.height = trusted.header.height + 1 + \/ SignedByOneThirdOfTrusted(trusted, untrusted) + THEN "SUCCESS" + ELSE "NOT_ENOUGH_TRUST" + + +(** + The invariant of the light store that is not related to the blockchain + *) +LightStoreInv(fetchedLightBlocks, lightBlockStatus) == + \A lh, rh \in DOMAIN fetchedLightBlocks: + \* for every pair of stored headers that have been verified + \/ lh >= rh + \/ lightBlockStatus[lh] /= "StateVerified" + \/ lightBlockStatus[rh] /= "StateVerified" + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh /\ lightBlockStatus[mh] = "StateVerified" + \* or the left header is outside the trusting period, so no guarantees + \/ LET lhdr == fetchedLightBlocks[lh] + rhdr == fetchedLightBlocks[rh] + IN + \* we can verify the right one using the left one + "SUCCESS" = ValidAndVerifiedUntimed(lhdr, rhdr) + +(** + Correctness states that all the obtained headers are exactly like in the blockchain. + + It is always the case that every verified header in LightStore was generated by + an instance of Tendermint consensus. + + [LCV-DIST-SAFE.1::CORRECTNESS-INV.1] + *) +CorrectnessInv(blockchain, fetchedLightBlocks, lightBlockStatus) == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] = "StateVerified" => + fetchedLightBlocks[h].header = blockchain[h] + +(** + * When the light client terminates, there are no failed blocks. + * (Otherwise, someone lied to us.) + *) +NoFailedBlocksOnSuccessInv(fetchedLightBlocks, lightBlockStatus) == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] /= "StateFailed" + +(** + The expected post-condition of VerifyToTarget. + *) +VerifyToTargetPost(blockchain, isPeerCorrect, + fetchedLightBlocks, lightBlockStatus, + trustedHeight, targetHeight, finalState) == + LET trustedHeader == fetchedLightBlocks[trustedHeight].header IN + \* The light client is not lying us on the trusted block. + \* It is straightforward to detect. + /\ lightBlockStatus[trustedHeight] = "StateVerified" + /\ trustedHeight \in DOMAIN fetchedLightBlocks + /\ trustedHeader = blockchain[trustedHeight] + \* the invariants we have found in the light client verification + \* there is a problem with trusting period + /\ isPeerCorrect + => CorrectnessInv(blockchain, fetchedLightBlocks, lightBlockStatus) + \* a correct peer should fail the light client, + \* if the trusted block is in the trusting period + /\ isPeerCorrect /\ InTrustingPeriodLocalSurely(trustedHeader) + => finalState = "finishedSuccess" + /\ finalState = "finishedSuccess" => + /\ lightBlockStatus[targetHeight] = "StateVerified" + /\ targetHeight \in DOMAIN fetchedLightBlocks + /\ NoFailedBlocksOnSuccessInv(fetchedLightBlocks, lightBlockStatus) + /\ LightStoreInv(fetchedLightBlocks, lightBlockStatus) + + +================================================================================== diff --git a/spec/light-client/verification/Lightclient_002_draft.tla b/spec/light-client/verification/Lightclient_002_draft.tla new file mode 100644 index 000000000..32c807f6e --- /dev/null +++ b/spec/light-client/verification/Lightclient_002_draft.tla @@ -0,0 +1,465 @@ +-------------------------- MODULE Lightclient_002_draft ---------------------------- +(** + * A state-machine specification of the lite client, following the English spec: + * + * https://github.com/informalsystems/tendermint-rs/blob/master/docs/spec/lightclient/verification.md + *) + +EXTENDS Integers, FiniteSets + +\* the parameters of Light Client +CONSTANTS + TRUSTED_HEIGHT, + (* an index of the block header that the light client trusts by social consensus *) + TARGET_HEIGHT, + (* an index of the block header that the light client tries to verify *) + TRUSTING_PERIOD, + (* the period within which the validators are trusted *) + IS_PRIMARY_CORRECT + (* is primary correct? *) + +VARIABLES (* see TypeOK below for the variable types *) + state, (* the current state of the light client *) + nextHeight, (* the next height to explore by the light client *) + nprobes (* the lite client iteration, or the number of block tests *) + +(* the light store *) +VARIABLES + fetchedLightBlocks, (* a function from heights to LightBlocks *) + lightBlockStatus, (* a function from heights to block statuses *) + latestVerified (* the latest verified block *) + +(* the variables of the lite client *) +lcvars == <> + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevNow, + prevVerdict + +InitMonitor(verified, current, now, verdict) == + /\ prevVerified = verified + /\ prevCurrent = current + /\ prevNow = now + /\ prevVerdict = verdict + +NextMonitor(verified, current, now, verdict) == + /\ prevVerified' = verified + /\ prevCurrent' = current + /\ prevNow' = now + /\ prevVerdict' = verdict + + +(******************* Blockchain instance ***********************************) + +\* the parameters that are propagated into Blockchain +CONSTANTS + AllNodes + (* a set of all nodes that can act as validators (correct and faulty) *) + +\* the state variables of Blockchain, see Blockchain.tla for the details +VARIABLES now, blockchain, Faulty + +\* All the variables of Blockchain. For some reason, BC!vars does not work +bcvars == <> + +(* Create an instance of Blockchain. + We could write EXTENDS Blockchain, but then all the constants and state variables + would be hidden inside the Blockchain module. + *) +ULTIMATE_HEIGHT == TARGET_HEIGHT + 1 + +BC == INSTANCE Blockchain_002_draft WITH + now <- now, blockchain <- blockchain, Faulty <- Faulty + +(************************** Lite client ************************************) + +(* the heights on which the light client is working *) +HEIGHTS == TRUSTED_HEIGHT..TARGET_HEIGHT + +(* the control states of the lite client *) +States == { "working", "finishedSuccess", "finishedFailure" } + +(** + Check the precondition of ValidAndVerified. + + [LCV-FUNC-VALID.1::TLA-PRE.1] + *) +ValidAndVerifiedPre(trusted, untrusted) == + LET thdr == trusted.header + uhdr == untrusted.header + IN + /\ BC!InTrustingPeriod(thdr) + /\ thdr.height < uhdr.height + \* the trusted block has been created earlier (no drift here) + /\ thdr.time < uhdr.time + \* the untrusted block is not from the future + /\ uhdr.time < now + /\ untrusted.Commits \subseteq uhdr.VS + /\ LET TP == Cardinality(uhdr.VS) + SP == Cardinality(untrusted.Commits) + IN + 3 * SP > 2 * TP + /\ thdr.height + 1 = uhdr.height => thdr.NextVS = uhdr.VS + (* As we do not have explicit hashes we ignore these three checks of the English spec: + + 1. "trusted.Commit is a commit is for the header trusted.Header, + i.e. it contains the correct hash of the header". + 2. untrusted.Validators = hash(untrusted.Header.Validators) + 3. untrusted.NextValidators = hash(untrusted.Header.NextValidators) + *) + +(** + * Check that the commits in an untrusted block form 1/3 of the next validators + * in a trusted header. + *) +SignedByOneThirdOfTrusted(trusted, untrusted) == + LET TP == Cardinality(trusted.header.NextVS) + SP == Cardinality(untrusted.Commits \intersect trusted.header.NextVS) + IN + 3 * SP > TP + +(** + Check, whether an untrusted block is valid and verifiable w.r.t. a trusted header. + + [LCV-FUNC-VALID.1::TLA.1] + *) +ValidAndVerified(trusted, untrusted) == + IF ~ValidAndVerifiedPre(trusted, untrusted) + THEN "INVALID" + ELSE IF ~BC!InTrustingPeriod(untrusted.header) + (* We leave the following test for the documentation purposes. + The implementation should do this test, as signature verification may be slow. + In the TLA+ specification, ValidAndVerified happens in no time. + *) + THEN "FAILED_TRUSTING_PERIOD" + ELSE IF untrusted.header.height = trusted.header.height + 1 + \/ SignedByOneThirdOfTrusted(trusted, untrusted) + THEN "SUCCESS" + ELSE "NOT_ENOUGH_TRUST" + +(* + Initial states of the light client. + Initially, only the trusted light block is present. + *) +LCInit == + /\ state = "working" + /\ nextHeight = TARGET_HEIGHT + /\ nprobes = 0 \* no tests have been done so far + /\ LET trustedBlock == blockchain[TRUSTED_HEIGHT] + trustedLightBlock == [header |-> trustedBlock, Commits |-> AllNodes] + IN + \* initially, fetchedLightBlocks is a function of one element, i.e., TRUSTED_HEIGHT + /\ fetchedLightBlocks = [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock] + \* initially, lightBlockStatus is a function of one element, i.e., TRUSTED_HEIGHT + /\ lightBlockStatus = [h \in {TRUSTED_HEIGHT} |-> "StateVerified"] + \* the latest verified block the the trusted block + /\ latestVerified = trustedLightBlock + /\ InitMonitor(trustedLightBlock, trustedLightBlock, now, "SUCCESS") + +\* block should contain a copy of the block from the reference chain, with a matching commit +CopyLightBlockFromChain(block, height) == + LET ref == blockchain[height] + lastCommit == + IF height < ULTIMATE_HEIGHT + THEN blockchain[height + 1].lastCommit + \* for the ultimate block, which we never use, as ULTIMATE_HEIGHT = TARGET_HEIGHT + 1 + ELSE blockchain[height].VS + IN + block = [header |-> ref, Commits |-> lastCommit] + +\* Either the primary is correct and the block comes from the reference chain, +\* or the block is produced by a faulty primary. +\* +\* [LCV-FUNC-FETCH.1::TLA.1] +FetchLightBlockInto(block, height) == + IF IS_PRIMARY_CORRECT + THEN CopyLightBlockFromChain(block, height) + ELSE BC!IsLightBlockAllowedByDigitalSignatures(height, block) + +\* add a block into the light store +\* +\* [LCV-FUNC-UPDATE.1::TLA.1] +LightStoreUpdateBlocks(lightBlocks, block) == + LET ht == block.header.height IN + [h \in DOMAIN lightBlocks \union {ht} |-> + IF h = ht THEN block ELSE lightBlocks[h]] + +\* update the state of a light block +\* +\* [LCV-FUNC-UPDATE.1::TLA.1] +LightStoreUpdateStates(statuses, ht, blockState) == + [h \in DOMAIN statuses \union {ht} |-> + IF h = ht THEN blockState ELSE statuses[h]] + +\* Check, whether newHeight is a possible next height for the light client. +\* +\* [LCV-FUNC-SCHEDULE.1::TLA.1] +CanScheduleTo(newHeight, pLatestVerified, pNextHeight, pTargetHeight) == + LET ht == pLatestVerified.header.height IN + \/ /\ ht = pNextHeight + /\ ht < pTargetHeight + /\ pNextHeight < newHeight + /\ newHeight <= pTargetHeight + \/ /\ ht < pNextHeight + /\ ht < pTargetHeight + /\ ht < newHeight + /\ newHeight < pNextHeight + \/ /\ ht = pTargetHeight + /\ newHeight = pTargetHeight + +\* The loop of VerifyToTarget. +\* +\* [LCV-FUNC-MAIN.1::TLA-LOOP.1] +VerifyToTargetLoop == + \* the loop condition is true + /\ latestVerified.header.height < TARGET_HEIGHT + \* pick a light block, which will be constrained later + /\ \E current \in BC!LightBlocks: + \* Get next LightBlock for verification + /\ IF nextHeight \in DOMAIN fetchedLightBlocks + THEN \* copy the block from the light store + /\ current = fetchedLightBlocks[nextHeight] + /\ UNCHANGED fetchedLightBlocks + ELSE \* retrieve a light block and save it in the light store + /\ FetchLightBlockInto(current, nextHeight) + /\ fetchedLightBlocks' = LightStoreUpdateBlocks(fetchedLightBlocks, current) + \* Record that one more probe has been done (for complexity and model checking) + /\ nprobes' = nprobes + 1 + \* Verify the current block + /\ LET verdict == ValidAndVerified(latestVerified, current) IN + NextMonitor(latestVerified, current, now, verdict) /\ + \* Decide whether/how to continue + CASE verdict = "SUCCESS" -> + /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateVerified") + /\ latestVerified' = current + /\ state' = + IF latestVerified'.header.height < TARGET_HEIGHT + THEN "working" + ELSE "finishedSuccess" + /\ \E newHeight \in HEIGHTS: + /\ CanScheduleTo(newHeight, current, nextHeight, TARGET_HEIGHT) + /\ nextHeight' = newHeight + + [] verdict = "NOT_ENOUGH_TRUST" -> + (* + do nothing: the light block current passed validation, but the validator + set is too different to verify it. We keep the state of + current at StateUnverified. For a later iteration, Schedule + might decide to try verification of that light block again. + *) + /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateUnverified") + /\ \E newHeight \in HEIGHTS: + /\ CanScheduleTo(newHeight, latestVerified, nextHeight, TARGET_HEIGHT) + /\ nextHeight' = newHeight + /\ UNCHANGED <> + + [] OTHER -> + \* verdict is some error code + /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateFailed") + /\ state' = "finishedFailure" + /\ UNCHANGED <> + +\* The terminating condition of VerifyToTarget. +\* +\* [LCV-FUNC-MAIN.1::TLA-LOOPCOND.1] +VerifyToTargetDone == + /\ latestVerified.header.height >= TARGET_HEIGHT + /\ state' = "finishedSuccess" + /\ UNCHANGED <> + /\ UNCHANGED <> + +(********************* Lite client + Blockchain *******************) +Init == + \* the blockchain is initialized immediately to the ULTIMATE_HEIGHT + /\ BC!InitToHeight + \* the light client starts + /\ LCInit + +(* + The system step is very simple. + The light client is either executing VerifyToTarget, or it has terminated. + (In the latter case, a model checker reports a deadlock.) + Simultaneously, the global clock may advance. + *) +Next == + /\ state = "working" + /\ VerifyToTargetLoop \/ VerifyToTargetDone + /\ BC!AdvanceTime \* the global clock is advanced by zero or more time units + +(************************* Types ******************************************) +TypeOK == + /\ state \in States + /\ nextHeight \in HEIGHTS + /\ latestVerified \in BC!LightBlocks + /\ \E HS \in SUBSET HEIGHTS: + /\ fetchedLightBlocks \in [HS -> BC!LightBlocks] + /\ lightBlockStatus + \in [HS -> {"StateVerified", "StateUnverified", "StateFailed"}] + +(************************* Properties ******************************************) + +(* The properties to check *) +\* this invariant candidate is false +NeverFinish == + state = "working" + +\* this invariant candidate is false +NeverFinishNegative == + state /= "finishedFailure" + +\* This invariant holds true, when the primary is correct. +\* This invariant candidate is false when the primary is faulty. +NeverFinishNegativeWhenTrusted == + (*(minTrustedHeight <= TRUSTED_HEIGHT)*) + BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) + => state /= "finishedFailure" + +\* this invariant candidate is false +NeverFinishPositive == + state /= "finishedSuccess" + +(** + Correctness states that all the obtained headers are exactly like in the blockchain. + + It is always the case that every verified header in LightStore was generated by + an instance of Tendermint consensus. + + [LCV-DIST-SAFE.1::CORRECTNESS-INV.1] + *) +CorrectnessInv == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] = "StateVerified" => + fetchedLightBlocks[h].header = blockchain[h] + +(** + Check that the sequence of the headers in storedLightBlocks satisfies ValidAndVerified = "SUCCESS" pairwise + This property is easily violated, whenever a header cannot be trusted anymore. + *) +StoredHeadersAreVerifiedInv == + state = "finishedSuccess" + => + \A lh, rh \in DOMAIN fetchedLightBlocks: \* for every pair of different stored headers + \/ lh >= rh + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh + \* or we can verify the right one using the left one + \/ "SUCCESS" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh]) + +\* An improved version of StoredHeadersAreSound, assuming that a header may be not trusted. +\* This invariant candidate is also violated, +\* as there may be some unverified blocks left in the middle. +StoredHeadersAreVerifiedOrNotTrustedInv == + state = "finishedSuccess" + => + \A lh, rh \in DOMAIN fetchedLightBlocks: \* for every pair of different stored headers + \/ lh >= rh + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh + \* or we can verify the right one using the left one + \/ "SUCCESS" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh]) + \* or the left header is outside the trusting period, so no guarantees + \/ ~BC!InTrustingPeriod(fetchedLightBlocks[lh].header) + +(** + * An improved version of StoredHeadersAreSoundOrNotTrusted, + * checking the property only for the verified headers. + * This invariant holds true. + *) +ProofOfChainOfTrustInv == + state = "finishedSuccess" + => + \A lh, rh \in DOMAIN fetchedLightBlocks: + \* for every pair of stored headers that have been verified + \/ lh >= rh + \/ lightBlockStatus[lh] = "StateUnverified" + \/ lightBlockStatus[rh] = "StateUnverified" + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh /\ lightBlockStatus[mh] = "StateVerified" + \* or the left header is outside the trusting period, so no guarantees + \/ ~(BC!InTrustingPeriod(fetchedLightBlocks[lh].header)) + \* or we can verify the right one using the left one + \/ "SUCCESS" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh]) + +(** + * When the light client terminates, there are no failed blocks. (Otherwise, someone lied to us.) + *) +NoFailedBlocksOnSuccessInv == + state = "finishedSuccess" => + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] /= "StateFailed" + +\* This property states that whenever the light client finishes with a positive outcome, +\* the trusted header is still within the trusting period. +\* We expect this property to be violated. And Apalache shows us a counterexample. +PositiveBeforeTrustedHeaderExpires == + (state = "finishedSuccess") => BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) + +\* If the primary is correct and the initial trusted block has not expired, +\* then whenever the algorithm terminates, it reports "success" +CorrectPrimaryAndTimeliness == + (BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) + /\ state /= "working" /\ IS_PRIMARY_CORRECT) => + state = "finishedSuccess" + +(** + If the primary is correct and there is a trusted block that has not expired, + then whenever the algorithm terminates, it reports "success". + + [LCV-DIST-LIVE.1::SUCCESS-CORR-PRIMARY-CHAIN-OF-TRUST.1] + *) +SuccessOnCorrectPrimaryAndChainOfTrust == + (\E h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] = "StateVerified" /\ BC!InTrustingPeriod(blockchain[h]) + /\ state /= "working" /\ IS_PRIMARY_CORRECT) => + state = "finishedSuccess" + +\* Lite Client Completeness: If header h was correctly generated by an instance +\* of Tendermint consensus (and its age is less than the trusting period), +\* then the lite client should eventually set trust(h) to true. +\* +\* Note that Completeness assumes that the lite client communicates with a correct full node. +\* +\* We decompose completeness into Termination (liveness) and Precision (safety). +\* Once again, Precision is an inverse version of the safety property in Completeness, +\* as A => B is logically equivalent to ~B => ~A. +PrecisionInv == + (state = "finishedFailure") + => \/ ~BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) \* outside of the trusting period + \/ \E h \in DOMAIN fetchedLightBlocks: + LET lightBlock == fetchedLightBlocks[h] IN + \* the full node lied to the lite client about the block header + \/ lightBlock.header /= blockchain[h] + \* the full node lied to the lite client about the commits + \/ lightBlock.Commits /= lightBlock.header.VS + +\* the old invariant that was found to be buggy by TLC +PrecisionBuggyInv == + (state = "finishedFailure") + => \/ ~BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) \* outside of the trusting period + \/ \E h \in DOMAIN fetchedLightBlocks: + LET lightBlock == fetchedLightBlocks[h] IN + \* the full node lied to the lite client about the block header + lightBlock.header /= blockchain[h] + +\* the worst complexity +Complexity == + LET N == TARGET_HEIGHT - TRUSTED_HEIGHT + 1 IN + state /= "working" => + (2 * nprobes <= N * (N - 1)) + +(* + We omit termination, as the algorithm deadlocks in the end. + So termination can be demonstrated by finding a deadlock. + Of course, one has to analyze the deadlocked state and see that + the algorithm has indeed terminated there. +*) +============================================================================= +\* Modification History +\* Last modified Fri Jun 26 12:08:28 CEST 2020 by igor +\* Created Wed Oct 02 16:39:42 CEST 2019 by igor diff --git a/spec/light-client/verification/Lightclient_003_draft.tla b/spec/light-client/verification/Lightclient_003_draft.tla new file mode 100644 index 000000000..e17a88491 --- /dev/null +++ b/spec/light-client/verification/Lightclient_003_draft.tla @@ -0,0 +1,493 @@ +-------------------------- MODULE Lightclient_003_draft ---------------------------- +(** + * A state-machine specification of the lite client verification, + * following the English spec: + * + * https://github.com/informalsystems/tendermint-rs/blob/master/docs/spec/lightclient/verification.md + *) + +EXTENDS Integers, FiniteSets + +\* the parameters of Light Client +CONSTANTS + TRUSTED_HEIGHT, + (* an index of the block header that the light client trusts by social consensus *) + TARGET_HEIGHT, + (* an index of the block header that the light client tries to verify *) + TRUSTING_PERIOD, + (* the period within which the validators are trusted *) + CLOCK_DRIFT, + (* the assumed precision of the clock *) + REAL_CLOCK_DRIFT, + (* the actual clock drift, which under normal circumstances should not + be larger than CLOCK_DRIFT (otherwise, there will be a bug) *) + IS_PRIMARY_CORRECT, + (* is primary correct? *) + FAULTY_RATIO + (* a pair <> that limits that ratio of faulty validator in the blockchain + from above (exclusive). Tendermint security model prescribes 1 / 3. *) + +VARIABLES (* see TypeOK below for the variable types *) + localClock, (* the local clock of the light client *) + state, (* the current state of the light client *) + nextHeight, (* the next height to explore by the light client *) + nprobes (* the lite client iteration, or the number of block tests *) + +(* the light store *) +VARIABLES + fetchedLightBlocks, (* a function from heights to LightBlocks *) + lightBlockStatus, (* a function from heights to block statuses *) + latestVerified (* the latest verified block *) + +(* the variables of the lite client *) +lcvars == <> + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +InitMonitor(verified, current, pLocalClock, verdict) == + /\ prevVerified = verified + /\ prevCurrent = current + /\ prevLocalClock = pLocalClock + /\ prevVerdict = verdict + +NextMonitor(verified, current, pLocalClock, verdict) == + /\ prevVerified' = verified + /\ prevCurrent' = current + /\ prevLocalClock' = pLocalClock + /\ prevVerdict' = verdict + + +(******************* Blockchain instance ***********************************) + +\* the parameters that are propagated into Blockchain +CONSTANTS + AllNodes + (* a set of all nodes that can act as validators (correct and faulty) *) + +\* the state variables of Blockchain, see Blockchain.tla for the details +VARIABLES refClock, blockchain, Faulty + +\* All the variables of Blockchain. For some reason, BC!vars does not work +bcvars == <> + +(* Create an instance of Blockchain. + We could write EXTENDS Blockchain, but then all the constants and state variables + would be hidden inside the Blockchain module. + *) +ULTIMATE_HEIGHT == TARGET_HEIGHT + 1 + +BC == INSTANCE Blockchain_003_draft WITH + refClock <- refClock, blockchain <- blockchain, Faulty <- Faulty + +(************************** Lite client ************************************) + +(* the heights on which the light client is working *) +HEIGHTS == TRUSTED_HEIGHT..TARGET_HEIGHT + +(* the control states of the lite client *) +States == { "working", "finishedSuccess", "finishedFailure" } + +\* The verification functions are implemented in the API +API == INSTANCE LCVerificationApi_003_draft + + +(* + Initial states of the light client. + Initially, only the trusted light block is present. + *) +LCInit == + /\ \E tm \in Int: + tm >= 0 /\ API!IsLocalClockWithinDrift(tm, refClock) /\ localClock = tm + /\ state = "working" + /\ nextHeight = TARGET_HEIGHT + /\ nprobes = 0 \* no tests have been done so far + /\ LET trustedBlock == blockchain[TRUSTED_HEIGHT] + trustedLightBlock == [header |-> trustedBlock, Commits |-> AllNodes] + IN + \* initially, fetchedLightBlocks is a function of one element, i.e., TRUSTED_HEIGHT + /\ fetchedLightBlocks = [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock] + \* initially, lightBlockStatus is a function of one element, i.e., TRUSTED_HEIGHT + /\ lightBlockStatus = [h \in {TRUSTED_HEIGHT} |-> "StateVerified"] + \* the latest verified block the the trusted block + /\ latestVerified = trustedLightBlock + /\ InitMonitor(trustedLightBlock, trustedLightBlock, localClock, "SUCCESS") + +\* block should contain a copy of the block from the reference chain, with a matching commit +CopyLightBlockFromChain(block, height) == + LET ref == blockchain[height] + lastCommit == + IF height < ULTIMATE_HEIGHT + THEN blockchain[height + 1].lastCommit + \* for the ultimate block, which we never use, as ULTIMATE_HEIGHT = TARGET_HEIGHT + 1 + ELSE blockchain[height].VS + IN + block = [header |-> ref, Commits |-> lastCommit] + +\* Either the primary is correct and the block comes from the reference chain, +\* or the block is produced by a faulty primary. +\* +\* [LCV-FUNC-FETCH.1::TLA.1] +FetchLightBlockInto(block, height) == + IF IS_PRIMARY_CORRECT + THEN CopyLightBlockFromChain(block, height) + ELSE BC!IsLightBlockAllowedByDigitalSignatures(height, block) + +\* add a block into the light store +\* +\* [LCV-FUNC-UPDATE.1::TLA.1] +LightStoreUpdateBlocks(lightBlocks, block) == + LET ht == block.header.height IN + [h \in DOMAIN lightBlocks \union {ht} |-> + IF h = ht THEN block ELSE lightBlocks[h]] + +\* update the state of a light block +\* +\* [LCV-FUNC-UPDATE.1::TLA.1] +LightStoreUpdateStates(statuses, ht, blockState) == + [h \in DOMAIN statuses \union {ht} |-> + IF h = ht THEN blockState ELSE statuses[h]] + +\* Check, whether newHeight is a possible next height for the light client. +\* +\* [LCV-FUNC-SCHEDULE.1::TLA.1] +CanScheduleTo(newHeight, pLatestVerified, pNextHeight, pTargetHeight) == + LET ht == pLatestVerified.header.height IN + \/ /\ ht = pNextHeight + /\ ht < pTargetHeight + /\ pNextHeight < newHeight + /\ newHeight <= pTargetHeight + \/ /\ ht < pNextHeight + /\ ht < pTargetHeight + /\ ht < newHeight + /\ newHeight < pNextHeight + \/ /\ ht = pTargetHeight + /\ newHeight = pTargetHeight + +\* The loop of VerifyToTarget. +\* +\* [LCV-FUNC-MAIN.1::TLA-LOOP.1] +VerifyToTargetLoop == + \* the loop condition is true + /\ latestVerified.header.height < TARGET_HEIGHT + \* pick a light block, which will be constrained later + /\ \E current \in BC!LightBlocks: + \* Get next LightBlock for verification + /\ IF nextHeight \in DOMAIN fetchedLightBlocks + THEN \* copy the block from the light store + /\ current = fetchedLightBlocks[nextHeight] + /\ UNCHANGED fetchedLightBlocks + ELSE \* retrieve a light block and save it in the light store + /\ FetchLightBlockInto(current, nextHeight) + /\ fetchedLightBlocks' = LightStoreUpdateBlocks(fetchedLightBlocks, current) + \* Record that one more probe has been done (for complexity and model checking) + /\ nprobes' = nprobes + 1 + \* Verify the current block + /\ LET verdict == API!ValidAndVerified(latestVerified, current, TRUE) IN + NextMonitor(latestVerified, current, localClock, verdict) /\ + \* Decide whether/how to continue + CASE verdict = "SUCCESS" -> + /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateVerified") + /\ latestVerified' = current + /\ state' = + IF latestVerified'.header.height < TARGET_HEIGHT + THEN "working" + ELSE "finishedSuccess" + /\ \E newHeight \in HEIGHTS: + /\ CanScheduleTo(newHeight, current, nextHeight, TARGET_HEIGHT) + /\ nextHeight' = newHeight + + [] verdict = "NOT_ENOUGH_TRUST" -> + (* + do nothing: the light block current passed validation, but the validator + set is too different to verify it. We keep the state of + current at StateUnverified. For a later iteration, Schedule + might decide to try verification of that light block again. + *) + /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateUnverified") + /\ \E newHeight \in HEIGHTS: + /\ CanScheduleTo(newHeight, latestVerified, nextHeight, TARGET_HEIGHT) + /\ nextHeight' = newHeight + /\ UNCHANGED <> + + [] OTHER -> + \* verdict is some error code + /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateFailed") + /\ state' = "finishedFailure" + /\ UNCHANGED <> + +\* The terminating condition of VerifyToTarget. +\* +\* [LCV-FUNC-MAIN.1::TLA-LOOPCOND.1] +VerifyToTargetDone == + /\ latestVerified.header.height >= TARGET_HEIGHT + /\ state' = "finishedSuccess" + /\ UNCHANGED <> + /\ UNCHANGED <> + +(* + The local and global clocks can be updated. They can also drift from each other. + Note that the local clock can actually go backwards in time. + However, it still stays in the drift envelope + of [refClock - REAL_CLOCK_DRIFT, refClock + REAL_CLOCK_DRIFT]. + *) +AdvanceClocks == + /\ BC!AdvanceTime + /\ \E tm \in Int: + /\ tm >= 0 + /\ API!IsLocalClockWithinDrift(tm, refClock') + /\ localClock' = tm + \* if you like the clock to always grow monotonically, uncomment the next line: + \*/\ localClock' > localClock + +(********************* Lite client + Blockchain *******************) +Init == + \* the blockchain is initialized immediately to the ULTIMATE_HEIGHT + /\ BC!InitToHeight(FAULTY_RATIO) + \* the light client starts + /\ LCInit + +(* + The system step is very simple. + The light client is either executing VerifyToTarget, or it has terminated. + (In the latter case, a model checker reports a deadlock.) + Simultaneously, the global clock may advance. + *) +Next == + /\ state = "working" + /\ VerifyToTargetLoop \/ VerifyToTargetDone + /\ AdvanceClocks + +(************************* Types ******************************************) +TypeOK == + /\ state \in States + /\ localClock \in Nat + /\ refClock \in Nat + /\ nextHeight \in HEIGHTS + /\ latestVerified \in BC!LightBlocks + /\ \E HS \in SUBSET HEIGHTS: + /\ fetchedLightBlocks \in [HS -> BC!LightBlocks] + /\ lightBlockStatus + \in [HS -> {"StateVerified", "StateUnverified", "StateFailed"}] + +(************************* Properties ******************************************) + +(* The properties to check *) +\* this invariant candidate is false +NeverFinish == + state = "working" + +\* this invariant candidate is false +NeverFinishNegative == + state /= "finishedFailure" + +\* This invariant holds true, when the primary is correct. +\* This invariant candidate is false when the primary is faulty. +NeverFinishNegativeWhenTrusted == + BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) + => state /= "finishedFailure" + +\* this invariant candidate is false +NeverFinishPositive == + state /= "finishedSuccess" + + +(** + Check that the target height has been reached upon successful termination. + *) +TargetHeightOnSuccessInv == + state = "finishedSuccess" => + /\ TARGET_HEIGHT \in DOMAIN fetchedLightBlocks + /\ lightBlockStatus[TARGET_HEIGHT] = "StateVerified" + +(** + Correctness states that all the obtained headers are exactly like in the blockchain. + + It is always the case that every verified header in LightStore was generated by + an instance of Tendermint consensus. + + [LCV-DIST-SAFE.1::CORRECTNESS-INV.1] + *) +CorrectnessInv == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] = "StateVerified" => + fetchedLightBlocks[h].header = blockchain[h] + +(** + No faulty block was used to construct a proof. This invariant holds, + only if FAULTY_RATIO < 1/3. + *) +NoTrustOnFaultyBlockInv == + (state = "finishedSuccess" + /\ fetchedLightBlocks[TARGET_HEIGHT].header = blockchain[TARGET_HEIGHT]) + => CorrectnessInv + +(** + Check that the sequence of the headers in storedLightBlocks satisfies ValidAndVerified = "SUCCESS" pairwise + This property is easily violated, whenever a header cannot be trusted anymore. + *) +StoredHeadersAreVerifiedInv == + state = "finishedSuccess" + => + \A lh, rh \in DOMAIN fetchedLightBlocks: \* for every pair of different stored headers + \/ lh >= rh + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh + \* or we can verify the right one using the left one + \/ "SUCCESS" = API!ValidAndVerified(fetchedLightBlocks[lh], + fetchedLightBlocks[rh], FALSE) + +\* An improved version of StoredHeadersAreVerifiedInv, +\* assuming that a header may be not trusted. +\* This invariant candidate is also violated, +\* as there may be some unverified blocks left in the middle. +\* This property is violated under two conditions: +\* (1) the primary is faulty and there are at least 4 blocks, +\* (2) the primary is correct and there are at least 5 blocks. +StoredHeadersAreVerifiedOrNotTrustedInv == + state = "finishedSuccess" + => + \A lh, rh \in DOMAIN fetchedLightBlocks: \* for every pair of different stored headers + \/ lh >= rh + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh + \* or we can verify the right one using the left one + \/ "SUCCESS" = API!ValidAndVerified(fetchedLightBlocks[lh], + fetchedLightBlocks[rh], FALSE) + \* or the left header is outside the trusting period, so no guarantees + \/ ~API!InTrustingPeriodLocal(fetchedLightBlocks[lh].header) + +(** + * An improved version of StoredHeadersAreSoundOrNotTrusted, + * checking the property only for the verified headers. + * This invariant holds true if CLOCK_DRIFT <= REAL_CLOCK_DRIFT. + *) +ProofOfChainOfTrustInv == + state = "finishedSuccess" + => + \A lh, rh \in DOMAIN fetchedLightBlocks: + \* for every pair of stored headers that have been verified + \/ lh >= rh + \/ lightBlockStatus[lh] = "StateUnverified" + \/ lightBlockStatus[rh] = "StateUnverified" + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh /\ lightBlockStatus[mh] = "StateVerified" + \* or the left header is outside the trusting period, so no guarantees + \/ ~(API!InTrustingPeriodLocal(fetchedLightBlocks[lh].header)) + \* or we can verify the right one using the left one + \/ "SUCCESS" = API!ValidAndVerified(fetchedLightBlocks[lh], + fetchedLightBlocks[rh], FALSE) + +(** + * When the light client terminates, there are no failed blocks. (Otherwise, someone lied to us.) + *) +NoFailedBlocksOnSuccessInv == + state = "finishedSuccess" => + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] /= "StateFailed" + +\* This property states that whenever the light client finishes with a positive outcome, +\* the trusted header is still within the trusting period. +\* We expect this property to be violated. And Apalache shows us a counterexample. +PositiveBeforeTrustedHeaderExpires == + (state = "finishedSuccess") => + BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) + +\* If the primary is correct and the initial trusted block has not expired, +\* then whenever the algorithm terminates, it reports "success". +\* This property fails. +CorrectPrimaryAndTimeliness == + (BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) + /\ state /= "working" /\ IS_PRIMARY_CORRECT) => + state = "finishedSuccess" + +(** + If the primary is correct and there is a trusted block that has not expired, + then whenever the algorithm terminates, it reports "success". + This property only holds true, if the local clock is always growing monotonically. + If the local clock can go backwards in the envelope + [refClock - CLOCK_DRIFT, refClock + CLOCK_DRIFT], then the property fails. + + [LCV-DIST-LIVE.1::SUCCESS-CORR-PRIMARY-CHAIN-OF-TRUST.1] + *) +SuccessOnCorrectPrimaryAndChainOfTrustLocal == + (\E h \in DOMAIN fetchedLightBlocks: + /\ lightBlockStatus[h] = "StateVerified" + /\ API!InTrustingPeriodLocal(blockchain[h]) + /\ state /= "working" /\ IS_PRIMARY_CORRECT) => + state = "finishedSuccess" + +(** + Similar to SuccessOnCorrectPrimaryAndChainOfTrust, but using the blockchain clock. + It fails because the local clock of the client drifted away, so it rejects a block + that has not expired yet (according to the local clock). + *) +SuccessOnCorrectPrimaryAndChainOfTrustGlobal == + (\E h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] = "StateVerified" /\ BC!InTrustingPeriod(blockchain[h]) + /\ state /= "working" /\ IS_PRIMARY_CORRECT) => + state = "finishedSuccess" + +\* Lite Client Completeness: If header h was correctly generated by an instance +\* of Tendermint consensus (and its age is less than the trusting period), +\* then the lite client should eventually set trust(h) to true. +\* +\* Note that Completeness assumes that the lite client communicates with a correct full node. +\* +\* We decompose completeness into Termination (liveness) and Precision (safety). +\* Once again, Precision is an inverse version of the safety property in Completeness, +\* as A => B is logically equivalent to ~B => ~A. +\* +\* This property holds only when CLOCK_DRIFT = 0 and REAL_CLOCK_DRIFT = 0. +PrecisionInv == + (state = "finishedFailure") + => \/ ~BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) \* outside of the trusting period + \/ \E h \in DOMAIN fetchedLightBlocks: + LET lightBlock == fetchedLightBlocks[h] IN + \* the full node lied to the lite client about the block header + \/ lightBlock.header /= blockchain[h] + \* the full node lied to the lite client about the commits + \/ lightBlock.Commits /= lightBlock.header.VS + +\* the old invariant that was found to be buggy by TLC +PrecisionBuggyInv == + (state = "finishedFailure") + => \/ ~BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) \* outside of the trusting period + \/ \E h \in DOMAIN fetchedLightBlocks: + LET lightBlock == fetchedLightBlocks[h] IN + \* the full node lied to the lite client about the block header + lightBlock.header /= blockchain[h] + +\* the worst complexity +Complexity == + LET N == TARGET_HEIGHT - TRUSTED_HEIGHT + 1 IN + state /= "working" => + (2 * nprobes <= N * (N - 1)) + +(** + If the light client has terminated, then the expected postcondition holds true. + *) +ApiPostInv == + state /= "working" => + API!VerifyToTargetPost(blockchain, IS_PRIMARY_CORRECT, + fetchedLightBlocks, lightBlockStatus, + TRUSTED_HEIGHT, TARGET_HEIGHT, state) + +(* + We omit termination, as the algorithm deadlocks in the end. + So termination can be demonstrated by finding a deadlock. + Of course, one has to analyze the deadlocked state and see that + the algorithm has indeed terminated there. +*) +============================================================================= +\* Modification History +\* Last modified Fri Jun 26 12:08:28 CEST 2020 by igor +\* Created Wed Oct 02 16:39:42 CEST 2019 by igor diff --git a/spec/light-client/verification/Lightclient_A_1.tla b/spec/light-client/verification/Lightclient_A_1.tla new file mode 100644 index 000000000..70e6caf00 --- /dev/null +++ b/spec/light-client/verification/Lightclient_A_1.tla @@ -0,0 +1,440 @@ +-------------------------- MODULE Lightclient_A_1 ---------------------------- +(** + * A state-machine specification of the lite client, following the English spec: + * + * ./verification_001_published.md + *) + +EXTENDS Integers, FiniteSets + +\* the parameters of Light Client +CONSTANTS + TRUSTED_HEIGHT, + (* an index of the block header that the light client trusts by social consensus *) + TARGET_HEIGHT, + (* an index of the block header that the light client tries to verify *) + TRUSTING_PERIOD, + (* the period within which the validators are trusted *) + IS_PRIMARY_CORRECT + (* is primary correct? *) + +VARIABLES (* see TypeOK below for the variable types *) + state, (* the current state of the light client *) + nextHeight, (* the next height to explore by the light client *) + nprobes (* the lite client iteration, or the number of block tests *) + +(* the light store *) +VARIABLES + fetchedLightBlocks, (* a function from heights to LightBlocks *) + lightBlockStatus, (* a function from heights to block statuses *) + latestVerified (* the latest verified block *) + +(* the variables of the lite client *) +lcvars == <> + +(******************* Blockchain instance ***********************************) + +\* the parameters that are propagated into Blockchain +CONSTANTS + AllNodes + (* a set of all nodes that can act as validators (correct and faulty) *) + +\* the state variables of Blockchain, see Blockchain.tla for the details +VARIABLES now, blockchain, Faulty + +\* All the variables of Blockchain. For some reason, BC!vars does not work +bcvars == <> + +(* Create an instance of Blockchain. + We could write EXTENDS Blockchain, but then all the constants and state variables + would be hidden inside the Blockchain module. + *) +ULTIMATE_HEIGHT == TARGET_HEIGHT + 1 + +BC == INSTANCE Blockchain_A_1 WITH + now <- now, blockchain <- blockchain, Faulty <- Faulty + +(************************** Lite client ************************************) + +(* the heights on which the light client is working *) +HEIGHTS == TRUSTED_HEIGHT..TARGET_HEIGHT + +(* the control states of the lite client *) +States == { "working", "finishedSuccess", "finishedFailure" } + +(** + Check the precondition of ValidAndVerified. + + [LCV-FUNC-VALID.1::TLA-PRE.1] + *) +ValidAndVerifiedPre(trusted, untrusted) == + LET thdr == trusted.header + uhdr == untrusted.header + IN + /\ BC!InTrustingPeriod(thdr) + /\ thdr.height < uhdr.height + \* the trusted block has been created earlier (no drift here) + /\ thdr.time <= uhdr.time + /\ untrusted.Commits \subseteq uhdr.VS + /\ LET TP == Cardinality(uhdr.VS) + SP == Cardinality(untrusted.Commits) + IN + 3 * SP > 2 * TP + /\ thdr.height + 1 = uhdr.height => thdr.NextVS = uhdr.VS + (* As we do not have explicit hashes we ignore these three checks of the English spec: + + 1. "trusted.Commit is a commit is for the header trusted.Header, + i.e. it contains the correct hash of the header". + 2. untrusted.Validators = hash(untrusted.Header.Validators) + 3. untrusted.NextValidators = hash(untrusted.Header.NextValidators) + *) + +(** + * Check that the commits in an untrusted block form 1/3 of the next validators + * in a trusted header. + *) +SignedByOneThirdOfTrusted(trusted, untrusted) == + LET TP == Cardinality(trusted.header.NextVS) + SP == Cardinality(untrusted.Commits \intersect trusted.header.NextVS) + IN + 3 * SP > TP + +(** + Check, whether an untrusted block is valid and verifiable w.r.t. a trusted header. + + [LCV-FUNC-VALID.1::TLA.1] + *) +ValidAndVerified(trusted, untrusted) == + IF ~ValidAndVerifiedPre(trusted, untrusted) + THEN "FAILED_VERIFICATION" + ELSE IF ~BC!InTrustingPeriod(untrusted.header) + (* We leave the following test for the documentation purposes. + The implementation should do this test, as signature verification may be slow. + In the TLA+ specification, ValidAndVerified happens in no time. + *) + THEN "FAILED_TRUSTING_PERIOD" + ELSE IF untrusted.header.height = trusted.header.height + 1 + \/ SignedByOneThirdOfTrusted(trusted, untrusted) + THEN "OK" + ELSE "CANNOT_VERIFY" + +(* + Initial states of the light client. + Initially, only the trusted light block is present. + *) +LCInit == + /\ state = "working" + /\ nextHeight = TARGET_HEIGHT + /\ nprobes = 0 \* no tests have been done so far + /\ LET trustedBlock == blockchain[TRUSTED_HEIGHT] + trustedLightBlock == [header |-> trustedBlock, Commits |-> AllNodes] + IN + \* initially, fetchedLightBlocks is a function of one element, i.e., TRUSTED_HEIGHT + /\ fetchedLightBlocks = [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock] + \* initially, lightBlockStatus is a function of one element, i.e., TRUSTED_HEIGHT + /\ lightBlockStatus = [h \in {TRUSTED_HEIGHT} |-> "StateVerified"] + \* the latest verified block the the trusted block + /\ latestVerified = trustedLightBlock + +\* block should contain a copy of the block from the reference chain, with a matching commit +CopyLightBlockFromChain(block, height) == + LET ref == blockchain[height] + lastCommit == + IF height < ULTIMATE_HEIGHT + THEN blockchain[height + 1].lastCommit + \* for the ultimate block, which we never use, as ULTIMATE_HEIGHT = TARGET_HEIGHT + 1 + ELSE blockchain[height].VS + IN + block = [header |-> ref, Commits |-> lastCommit] + +\* Either the primary is correct and the block comes from the reference chain, +\* or the block is produced by a faulty primary. +\* +\* [LCV-FUNC-FETCH.1::TLA.1] +FetchLightBlockInto(block, height) == + IF IS_PRIMARY_CORRECT + THEN CopyLightBlockFromChain(block, height) + ELSE BC!IsLightBlockAllowedByDigitalSignatures(height, block) + +\* add a block into the light store +\* +\* [LCV-FUNC-UPDATE.1::TLA.1] +LightStoreUpdateBlocks(lightBlocks, block) == + LET ht == block.header.height IN + [h \in DOMAIN lightBlocks \union {ht} |-> + IF h = ht THEN block ELSE lightBlocks[h]] + +\* update the state of a light block +\* +\* [LCV-FUNC-UPDATE.1::TLA.1] +LightStoreUpdateStates(statuses, ht, blockState) == + [h \in DOMAIN statuses \union {ht} |-> + IF h = ht THEN blockState ELSE statuses[h]] + +\* Check, whether newHeight is a possible next height for the light client. +\* +\* [LCV-FUNC-SCHEDULE.1::TLA.1] +CanScheduleTo(newHeight, pLatestVerified, pNextHeight, pTargetHeight) == + LET ht == pLatestVerified.header.height IN + \/ /\ ht = pNextHeight + /\ ht < pTargetHeight + /\ pNextHeight < newHeight + /\ newHeight <= pTargetHeight + \/ /\ ht < pNextHeight + /\ ht < pTargetHeight + /\ ht < newHeight + /\ newHeight < pNextHeight + \/ /\ ht = pTargetHeight + /\ newHeight = pTargetHeight + +\* The loop of VerifyToTarget. +\* +\* [LCV-FUNC-MAIN.1::TLA-LOOP.1] +VerifyToTargetLoop == + \* the loop condition is true + /\ latestVerified.header.height < TARGET_HEIGHT + \* pick a light block, which will be constrained later + /\ \E current \in BC!LightBlocks: + \* Get next LightBlock for verification + /\ IF nextHeight \in DOMAIN fetchedLightBlocks + THEN \* copy the block from the light store + /\ current = fetchedLightBlocks[nextHeight] + /\ UNCHANGED fetchedLightBlocks + ELSE \* retrieve a light block and save it in the light store + /\ FetchLightBlockInto(current, nextHeight) + /\ fetchedLightBlocks' = LightStoreUpdateBlocks(fetchedLightBlocks, current) + \* Record that one more probe has been done (for complexity and model checking) + /\ nprobes' = nprobes + 1 + \* Verify the current block + /\ LET verdict == ValidAndVerified(latestVerified, current) IN + \* Decide whether/how to continue + CASE verdict = "OK" -> + /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateVerified") + /\ latestVerified' = current + /\ state' = + IF latestVerified'.header.height < TARGET_HEIGHT + THEN "working" + ELSE "finishedSuccess" + /\ \E newHeight \in HEIGHTS: + /\ CanScheduleTo(newHeight, current, nextHeight, TARGET_HEIGHT) + /\ nextHeight' = newHeight + + [] verdict = "CANNOT_VERIFY" -> + (* + do nothing: the light block current passed validation, but the validator + set is too different to verify it. We keep the state of + current at StateUnverified. For a later iteration, Schedule + might decide to try verification of that light block again. + *) + /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateUnverified") + /\ \E newHeight \in HEIGHTS: + /\ CanScheduleTo(newHeight, latestVerified, nextHeight, TARGET_HEIGHT) + /\ nextHeight' = newHeight + /\ UNCHANGED <> + + [] OTHER -> + \* verdict is some error code + /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateFailed") + /\ state' = "finishedFailure" + /\ UNCHANGED <> + +\* The terminating condition of VerifyToTarget. +\* +\* [LCV-FUNC-MAIN.1::TLA-LOOPCOND.1] +VerifyToTargetDone == + /\ latestVerified.header.height >= TARGET_HEIGHT + /\ state' = "finishedSuccess" + /\ UNCHANGED <> + +(********************* Lite client + Blockchain *******************) +Init == + \* the blockchain is initialized immediately to the ULTIMATE_HEIGHT + /\ BC!InitToHeight + \* the light client starts + /\ LCInit + +(* + The system step is very simple. + The light client is either executing VerifyToTarget, or it has terminated. + (In the latter case, a model checker reports a deadlock.) + Simultaneously, the global clock may advance. + *) +Next == + /\ state = "working" + /\ VerifyToTargetLoop \/ VerifyToTargetDone + /\ BC!AdvanceTime \* the global clock is advanced by zero or more time units + +(************************* Types ******************************************) +TypeOK == + /\ state \in States + /\ nextHeight \in HEIGHTS + /\ latestVerified \in BC!LightBlocks + /\ \E HS \in SUBSET HEIGHTS: + /\ fetchedLightBlocks \in [HS -> BC!LightBlocks] + /\ lightBlockStatus + \in [HS -> {"StateVerified", "StateUnverified", "StateFailed"}] + +(************************* Properties ******************************************) + +(* The properties to check *) +\* this invariant candidate is false +NeverFinish == + state = "working" + +\* this invariant candidate is false +NeverFinishNegative == + state /= "finishedFailure" + +\* This invariant holds true, when the primary is correct. +\* This invariant candidate is false when the primary is faulty. +NeverFinishNegativeWhenTrusted == + (*(minTrustedHeight <= TRUSTED_HEIGHT)*) + BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) + => state /= "finishedFailure" + +\* this invariant candidate is false +NeverFinishPositive == + state /= "finishedSuccess" + +(** + Correctness states that all the obtained headers are exactly like in the blockchain. + + It is always the case that every verified header in LightStore was generated by + an instance of Tendermint consensus. + + [LCV-DIST-SAFE.1::CORRECTNESS-INV.1] + *) +CorrectnessInv == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] = "StateVerified" => + fetchedLightBlocks[h].header = blockchain[h] + +(** + Check that the sequence of the headers in storedLightBlocks satisfies ValidAndVerified = "OK" pairwise + This property is easily violated, whenever a header cannot be trusted anymore. + *) +StoredHeadersAreVerifiedInv == + state = "finishedSuccess" + => + \A lh, rh \in DOMAIN fetchedLightBlocks: \* for every pair of different stored headers + \/ lh >= rh + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh + \* or we can verify the right one using the left one + \/ "OK" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh]) + +\* An improved version of StoredHeadersAreSound, assuming that a header may be not trusted. +\* This invariant candidate is also violated, +\* as there may be some unverified blocks left in the middle. +StoredHeadersAreVerifiedOrNotTrustedInv == + state = "finishedSuccess" + => + \A lh, rh \in DOMAIN fetchedLightBlocks: \* for every pair of different stored headers + \/ lh >= rh + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh + \* or we can verify the right one using the left one + \/ "OK" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh]) + \* or the left header is outside the trusting period, so no guarantees + \/ ~BC!InTrustingPeriod(fetchedLightBlocks[lh].header) + +(** + * An improved version of StoredHeadersAreSoundOrNotTrusted, + * checking the property only for the verified headers. + * This invariant holds true. + *) +ProofOfChainOfTrustInv == + state = "finishedSuccess" + => + \A lh, rh \in DOMAIN fetchedLightBlocks: + \* for every pair of stored headers that have been verified + \/ lh >= rh + \/ lightBlockStatus[lh] = "StateUnverified" + \/ lightBlockStatus[rh] = "StateUnverified" + \* either there is a header between them + \/ \E mh \in DOMAIN fetchedLightBlocks: + lh < mh /\ mh < rh /\ lightBlockStatus[mh] = "StateVerified" + \* or the left header is outside the trusting period, so no guarantees + \/ ~(BC!InTrustingPeriod(fetchedLightBlocks[lh].header)) + \* or we can verify the right one using the left one + \/ "OK" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh]) + +(** + * When the light client terminates, there are no failed blocks. (Otherwise, someone lied to us.) + *) +NoFailedBlocksOnSuccessInv == + state = "finishedSuccess" => + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] /= "StateFailed" + +\* This property states that whenever the light client finishes with a positive outcome, +\* the trusted header is still within the trusting period. +\* We expect this property to be violated. And Apalache shows us a counterexample. +PositiveBeforeTrustedHeaderExpires == + (state = "finishedSuccess") => BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) + +\* If the primary is correct and the initial trusted block has not expired, +\* then whenever the algorithm terminates, it reports "success" +CorrectPrimaryAndTimeliness == + (BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) + /\ state /= "working" /\ IS_PRIMARY_CORRECT) => + state = "finishedSuccess" + +(** + If the primary is correct and there is a trusted block that has not expired, + then whenever the algorithm terminates, it reports "success". + + [LCV-DIST-LIVE.1::SUCCESS-CORR-PRIMARY-CHAIN-OF-TRUST.1] + *) +SuccessOnCorrectPrimaryAndChainOfTrust == + (\E h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] = "StateVerified" /\ BC!InTrustingPeriod(blockchain[h]) + /\ state /= "working" /\ IS_PRIMARY_CORRECT) => + state = "finishedSuccess" + +\* Lite Client Completeness: If header h was correctly generated by an instance +\* of Tendermint consensus (and its age is less than the trusting period), +\* then the lite client should eventually set trust(h) to true. +\* +\* Note that Completeness assumes that the lite client communicates with a correct full node. +\* +\* We decompose completeness into Termination (liveness) and Precision (safety). +\* Once again, Precision is an inverse version of the safety property in Completeness, +\* as A => B is logically equivalent to ~B => ~A. +PrecisionInv == + (state = "finishedFailure") + => \/ ~BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) \* outside of the trusting period + \/ \E h \in DOMAIN fetchedLightBlocks: + LET lightBlock == fetchedLightBlocks[h] IN + \* the full node lied to the lite client about the block header + \/ lightBlock.header /= blockchain[h] + \* the full node lied to the lite client about the commits + \/ lightBlock.Commits /= lightBlock.header.VS + +\* the old invariant that was found to be buggy by TLC +PrecisionBuggyInv == + (state = "finishedFailure") + => \/ ~BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) \* outside of the trusting period + \/ \E h \in DOMAIN fetchedLightBlocks: + LET lightBlock == fetchedLightBlocks[h] IN + \* the full node lied to the lite client about the block header + lightBlock.header /= blockchain[h] + +\* the worst complexity +Complexity == + LET N == TARGET_HEIGHT - TRUSTED_HEIGHT + 1 IN + state /= "working" => + (2 * nprobes <= N * (N - 1)) + +(* + We omit termination, as the algorithm deadlocks in the end. + So termination can be demonstrated by finding a deadlock. + Of course, one has to analyze the deadlocked state and see that + the algorithm has indeed terminated there. +*) +============================================================================= +\* Modification History +\* Last modified Fri Jun 26 12:08:28 CEST 2020 by igor +\* Created Wed Oct 02 16:39:42 CEST 2019 by igor diff --git a/spec/light-client/verification/MC4_3_correct.tla b/spec/light-client/verification/MC4_3_correct.tla new file mode 100644 index 000000000..a27d8de05 --- /dev/null +++ b/spec/light-client/verification/MC4_3_correct.tla @@ -0,0 +1,26 @@ +---------------------------- MODULE MC4_3_correct --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 3 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == TRUE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================== diff --git a/spec/light-client/verification/MC4_3_faulty.tla b/spec/light-client/verification/MC4_3_faulty.tla new file mode 100644 index 000000000..74b278900 --- /dev/null +++ b/spec/light-client/verification/MC4_3_faulty.tla @@ -0,0 +1,26 @@ +---------------------------- MODULE MC4_3_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 3 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================== diff --git a/spec/light-client/verification/MC4_4_correct.tla b/spec/light-client/verification/MC4_4_correct.tla new file mode 100644 index 000000000..0a8d96b59 --- /dev/null +++ b/spec/light-client/verification/MC4_4_correct.tla @@ -0,0 +1,26 @@ +------------------------- MODULE MC4_4_correct --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 4 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == TRUE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC4_4_correct_drifted.tla b/spec/light-client/verification/MC4_4_correct_drifted.tla new file mode 100644 index 000000000..7fefe349e --- /dev/null +++ b/spec/light-client/verification/MC4_4_correct_drifted.tla @@ -0,0 +1,26 @@ +---------------------- MODULE MC4_4_correct_drifted --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 4 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 30 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == TRUE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================== diff --git a/spec/light-client/verification/MC4_4_faulty.tla b/spec/light-client/verification/MC4_4_faulty.tla new file mode 100644 index 000000000..167fa61fb --- /dev/null +++ b/spec/light-client/verification/MC4_4_faulty.tla @@ -0,0 +1,26 @@ +---------------------------- MODULE MC4_4_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 4 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================== diff --git a/spec/light-client/verification/MC4_4_faulty_drifted.tla b/spec/light-client/verification/MC4_4_faulty_drifted.tla new file mode 100644 index 000000000..e37c3cb40 --- /dev/null +++ b/spec/light-client/verification/MC4_4_faulty_drifted.tla @@ -0,0 +1,26 @@ +---------------------- MODULE MC4_4_faulty_drifted --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 4 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 30 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================== diff --git a/spec/light-client/verification/MC4_5_correct.tla b/spec/light-client/verification/MC4_5_correct.tla new file mode 100644 index 000000000..cffb22cc8 --- /dev/null +++ b/spec/light-client/verification/MC4_5_correct.tla @@ -0,0 +1,26 @@ +------------------------- MODULE MC4_5_correct --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 5 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == TRUE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC4_5_faulty.tla b/spec/light-client/verification/MC4_5_faulty.tla new file mode 100644 index 000000000..3d3a00290 --- /dev/null +++ b/spec/light-client/verification/MC4_5_faulty.tla @@ -0,0 +1,26 @@ +------------------------- MODULE MC4_5_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 5 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +IS_PRICLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +MARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC4_6_faulty.tla b/spec/light-client/verification/MC4_6_faulty.tla new file mode 100644 index 000000000..64f164854 --- /dev/null +++ b/spec/light-client/verification/MC4_6_faulty.tla @@ -0,0 +1,26 @@ +------------------------- MODULE MC4_6_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 6 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +IS_PRCLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IMARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC4_7_faulty.tla b/spec/light-client/verification/MC4_7_faulty.tla new file mode 100644 index 000000000..dc6a94eb1 --- /dev/null +++ b/spec/light-client/verification/MC4_7_faulty.tla @@ -0,0 +1,26 @@ +------------------------- MODULE MC4_7_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 7 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC5_5_correct.tla b/spec/light-client/verification/MC5_5_correct.tla new file mode 100644 index 000000000..00b4151f7 --- /dev/null +++ b/spec/light-client/verification/MC5_5_correct.tla @@ -0,0 +1,26 @@ +------------------------- MODULE MC5_5_correct --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4", "n5"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 5 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == TRUE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC5_5_correct_peer_two_thirds_faulty.tla b/spec/light-client/verification/MC5_5_correct_peer_two_thirds_faulty.tla new file mode 100644 index 000000000..d4212032f --- /dev/null +++ b/spec/light-client/verification/MC5_5_correct_peer_two_thirds_faulty.tla @@ -0,0 +1,26 @@ +------------------- MODULE MC5_5_correct_peer_two_thirds_faulty ---------------------- + +AllNodes == {"n1", "n2", "n3", "n4", "n5"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 5 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == TRUE +FAULTY_RATIO == <<2, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC5_5_faulty.tla b/spec/light-client/verification/MC5_5_faulty.tla new file mode 100644 index 000000000..f63d175a1 --- /dev/null +++ b/spec/light-client/verification/MC5_5_faulty.tla @@ -0,0 +1,26 @@ +----------------- MODULE MC5_5_faulty --------------------- + +AllNodes == {"n1", "n2", "n3", "n4", "n5"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 5 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +FAULTY_RATIO == <<2, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC5_5_faulty_peer_two_thirds_faulty.tla b/spec/light-client/verification/MC5_5_faulty_peer_two_thirds_faulty.tla new file mode 100644 index 000000000..ef9974d06 --- /dev/null +++ b/spec/light-client/verification/MC5_5_faulty_peer_two_thirds_faulty.tla @@ -0,0 +1,26 @@ +----------------- MODULE MC5_5_faulty_peer_two_thirds_faulty --------------------- + +AllNodes == {"n1", "n2", "n3", "n4", "n5"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 5 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +FAULTY_RATIO == <<2, 3>> \* < 2 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC5_7_faulty.tla b/spec/light-client/verification/MC5_7_faulty.tla new file mode 100644 index 000000000..63461b0c8 --- /dev/null +++ b/spec/light-client/verification/MC5_7_faulty.tla @@ -0,0 +1,26 @@ +------------------------- MODULE MC5_7_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4", "n5"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 7 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC7_5_faulty.tla b/spec/light-client/verification/MC7_5_faulty.tla new file mode 100644 index 000000000..860f9c0aa --- /dev/null +++ b/spec/light-client/verification/MC7_5_faulty.tla @@ -0,0 +1,26 @@ +------------------------- MODULE MC7_5_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4", "n5", "n6", "n7"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 5 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/MC7_7_faulty.tla b/spec/light-client/verification/MC7_7_faulty.tla new file mode 100644 index 000000000..79e328f14 --- /dev/null +++ b/spec/light-client/verification/MC7_7_faulty.tla @@ -0,0 +1,26 @@ +------------------------- MODULE MC7_7_faulty --------------------------- + +AllNodes == {"n1", "n2", "n3", "n4", "n5", "n6", "n7"} +TRUSTED_HEIGHT == 1 +TARGET_HEIGHT == 7 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting +REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting +IS_PRIMARY_CORRECT == FALSE +FAULTY_RATIO == <<1, 3>> \* < 1 / 3 faulty validators + +VARIABLES + state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified, + nprobes, + localClock, + refClock, blockchain, Faulty + +(* the light client previous state components, used for monitoring *) +VARIABLES + prevVerified, + prevCurrent, + prevLocalClock, + prevVerdict + +INSTANCE Lightclient_003_draft +============================================================================ diff --git a/spec/light-client/verification/README.md b/spec/light-client/verification/README.md new file mode 100644 index 000000000..8777374ac --- /dev/null +++ b/spec/light-client/verification/README.md @@ -0,0 +1,577 @@ +--- +order: 1 +parent: + title: Verification + order: 2 +--- +# Core Verification + +## Problem statement + +We assume that the light client knows a (base) header `inithead` it trusts (by social consensus or because +the light client has decided to trust the header before). The goal is to check whether another header +`newhead` can be trusted based on the data in `inithead`. + +The correctness of the protocol is based on the assumption that `inithead` was generated by an instance of +Tendermint consensus. + +### Failure Model + +For the purpose of the following definitions we assume that there exists a function +`validators` that returns the corresponding validator set for the given hash. + +The light client protocol is defined with respect to the following failure model: + +Given a known bound `TRUSTED_PERIOD`, and a block `b` with header `h` generated at time `Time` +(i.e. `h.Time = Time`), a set of validators that hold more than 2/3 of the voting power +in `validators(b.Header.NextValidatorsHash)` is correct until time `b.Header.Time + TRUSTED_PERIOD`. + +*Assumption*: "correct" is defined w.r.t. realtime (some Newtonian global notion of time, i.e., wall time), +while `Header.Time` corresponds to the [BFT time](../consensus/bft-time.md). In this note, we assume that clocks of correct processes +are synchronized (for example using NTP), and therefore there is bounded clock drift (`CLOCK_DRIFT`) between local clocks and +BFT time. More precisely, for every correct light client process and every `header.Time` (i.e. BFT Time, for a header correctly +generated by the Tendermint consensus), the following inequality holds: `Header.Time < now + CLOCK_DRIFT`, +where `now` corresponds to the system clock at the light client process. + +Furthermore, we assume that `TRUSTED_PERIOD` is (several) order of magnitude bigger than `CLOCK_DRIFT` (`TRUSTED_PERIOD >> CLOCK_DRIFT`), +as `CLOCK_DRIFT` (using NTP) is in the order of milliseconds and `TRUSTED_PERIOD` is in the order of weeks. + +We expect a light client process defined in this document to be used in the context in which there is some +larger period during which misbehaving validators can be detected and punished (we normally refer to it as `UNBONDING_PERIOD` +due to the "bonding" mechanism in modern proof of stake systems). Furthermore, we assume that +`TRUSTED_PERIOD < UNBONDING_PERIOD` and that they are normally of the same order of magnitude, for example +`TRUSTED_PERIOD = UNBONDING_PERIOD / 2`. + +The specification in this document considers an implementation of the light client under the Failure Model defined above. +Mechanisms like `fork accountability` and `evidence submission` are defined in the context of `UNBONDING_PERIOD` and +they incentivize validators to follow the protocol specification defined in this document. If they don't, +and we have 1/3 (or more) faulty validators, safety may be violated. Our approach then is +to *detect* these cases (after the fact), and take suitable repair actions (automatic and social). +This is discussed in document on [Fork accountability](./accountability.md). + +The term "trusted" above indicates that the correctness of the protocol depends on +this assumption. It is in the responsibility of the user that runs the light client to make sure that the risk +of trusting a corrupted/forged `inithead` is negligible. + +*Remark*: This failure model might change to a hybrid version that takes heights into account in the future. + +### High Level Solution + +Upon initialization, the light client is given a header `inithead` it trusts (by +social consensus). When a light clients sees a new signed header `snh`, it has to decide whether to trust the new +header. Trust can be obtained by (possibly) the combination of three methods. + +1. **Uninterrupted sequence of headers.** Given a trusted header `h` and an untrusted header `h1`, +the light client trusts a header `h1` if it trusts all headers in between `h` and `h1`. + +2. **Trusted period.** Given a trusted header `h`, an untrusted header `h1 > h` and `TRUSTED_PERIOD` during which +the failure model holds, we can check whether at least one validator, that has been continuously correct +from `h.Time` until now, has signed `h1`. If this is the case, we can trust `h1`. + +3. **Bisection.** If a check according to 2. (trusted period) fails, the light client can try to +obtain a header `hp` whose height lies between `h` and `h1` in order to check whether `h` can be used to +get trust for `hp`, and `hp` can be used to get trust for `snh`. If this is the case we can trust `h1`; +if not, we continue recursively until either we found set of headers that can build (transitively) trust relation +between `h` and `h1`, or we failed as two consecutive headers don't verify against each other. + +## Definitions + +### Data structures + +In the following, only the details of the data structures needed for this specification are given. + + ```go + type Header struct { + Height int64 + Time Time // the chain time when the header (block) was generated + + LastBlockID BlockID // prev block info + ValidatorsHash []byte // hash of the validators for the current block + NextValidatorsHash []byte // hash of the validators for the next block + } + + type SignedHeader struct { + Header Header + Commit Commit // commit for the given header + } + + type ValidatorSet struct { + Validators []Validator + TotalVotingPower int64 + } + + type Validator struct { + Address Address // validator address (we assume validator's addresses are unique) + VotingPower int64 // validator's voting power + } + + type TrustedState { + SignedHeader SignedHeader + ValidatorSet ValidatorSet + } + ``` + +### Functions + +For the purpose of this light client specification, we assume that the Tendermint Full Node +exposes the following functions over Tendermint RPC: + +```go + // returns signed header: Header with Commit, for the given height + func Commit(height int64) (SignedHeader, error) + + // returns validator set for the given height + func Validators(height int64) (ValidatorSet, error) +``` + +Furthermore, we assume the following auxiliary functions: + +```go + // returns true if the commit is for the header, ie. if it contains + // the correct hash of the header; otherwise false + func matchingCommit(header Header, commit Commit) bool + + // returns the set of validators from the given validator set that + // committed the block (that correctly signed the block) + // it assumes signature verification so it can be computationally expensive + func signers(commit Commit, validatorSet ValidatorSet) []Validator + + // returns the voting power the validators in v1 have according to their voting power in set v2 + // it does not assume signature verification + func votingPowerIn(v1 []Validator, v2 ValidatorSet) int64 + + // returns hash of the given validator set + func hash(v2 ValidatorSet) []byte +``` + +In the functions below we will be using `trustThreshold` as a parameter. For simplicity +we assume that `trustThreshold` is a float between `1/3` and `2/3` and we will not be checking it +in the pseudo-code. + +**VerifySingle.** The function `VerifySingle` attempts to validate given untrusted header and the corresponding validator sets +based on a given trusted state. It ensures that the trusted state is still within its trusted period, +and that the untrusted header is within assumed `clockDrift` bound of the passed time `now`. +Note that this function is not making external (RPC) calls to the full node; the whole logic is +based on the local (given) state. This function is supposed to be used by the IBC handlers. + +```go +func VerifySingle(untrustedSh SignedHeader, + untrustedVs ValidatorSet, + untrustedNextVs ValidatorSet, + trustedState TrustedState, + trustThreshold float, + trustingPeriod Duration, + clockDrift Duration, + now Time) (TrustedState, error) { + + if untrustedSh.Header.Time > now + clockDrift { + return (trustedState, ErrInvalidHeaderTime) + } + + trustedHeader = trustedState.SignedHeader.Header + if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { + return (state, ErrHeaderNotWithinTrustedPeriod) + } + + // we assume that time it takes to execute verifySingle function + // is several order of magnitudes smaller than trustingPeriod + error = verifySingle( + trustedState, + untrustedSh, + untrustedVs, + untrustedNextVs, + trustThreshold) + + if error != nil return (state, error) + + // the untrusted header is now trusted + newTrustedState = TrustedState(untrustedSh, untrustedNextVs) + return (newTrustedState, nil) +} + +// return true if header is within its light client trusted period; otherwise returns false +func isWithinTrustedPeriod(header Header, + trustingPeriod Duration, + now Time) bool { + + return header.Time + trustedPeriod > now +} +``` + +Note that in case `VerifySingle` returns without an error (untrusted header +is successfully verified) then we have a guarantee that the transition of the trust +from `trustedState` to `newTrustedState` happened during the trusted period of +`trustedState.SignedHeader.Header`. + +TODO: Explain what happens in case `VerifySingle` returns with an error. + +**verifySingle.** The function `verifySingle` verifies a single untrusted header +against a given trusted state. It includes all validations and signature verification. +It is not publicly exposed since it does not check for header expiry (time constraints) +and hence it's possible to use it incorrectly. + +```go +func verifySingle(trustedState TrustedState, + untrustedSh SignedHeader, + untrustedVs ValidatorSet, + untrustedNextVs ValidatorSet, + trustThreshold float) error { + + untrustedHeader = untrustedSh.Header + untrustedCommit = untrustedSh.Commit + + trustedHeader = trustedState.SignedHeader.Header + trustedVs = trustedState.ValidatorSet + + if trustedHeader.Height >= untrustedHeader.Height return ErrNonIncreasingHeight + if trustedHeader.Time >= untrustedHeader.Time return ErrNonIncreasingTime + + // validate the untrusted header against its commit, vals, and next_vals + error = validateSignedHeaderAndVals(untrustedSh, untrustedVs, untrustedNextVs) + if error != nil return error + + // check for adjacent headers + if untrustedHeader.Height == trustedHeader.Height + 1 { + if trustedHeader.NextValidatorsHash != untrustedHeader.ValidatorsHash { + return ErrInvalidAdjacentHeaders + } + } else { + error = verifyCommitTrusting(trustedVs, untrustedCommit, untrustedVs, trustThreshold) + if error != nil return error + } + + // verify the untrusted commit + return verifyCommitFull(untrustedVs, untrustedCommit) +} + +// returns nil if header and validator sets are consistent; otherwise returns error +func validateSignedHeaderAndVals(signedHeader SignedHeader, vs ValidatorSet, nextVs ValidatorSet) error { + header = signedHeader.Header + if hash(vs) != header.ValidatorsHash return ErrInvalidValidatorSet + if hash(nextVs) != header.NextValidatorsHash return ErrInvalidNextValidatorSet + if !matchingCommit(header, signedHeader.Commit) return ErrInvalidCommitValue + return nil +} + +// returns nil if at least single correst signer signed the commit; otherwise returns error +func verifyCommitTrusting(trustedVs ValidatorSet, + commit Commit, + untrustedVs ValidatorSet, + trustLevel float) error { + + totalPower := trustedVs.TotalVotingPower + signedPower := votingPowerIn(signers(commit, untrustedVs), trustedVs) + + // check that the signers account for more than max(1/3, trustLevel) of the voting power + // this ensures that there is at least single correct validator in the set of signers + if signedPower < max(1/3, trustLevel) * totalPower return ErrInsufficientVotingPower + return nil +} + +// returns nil if commit is signed by more than 2/3 of voting power of the given validator set +// return error otherwise +func verifyCommitFull(vs ValidatorSet, commit Commit) error { + totalPower := vs.TotalVotingPower; + signedPower := votingPowerIn(signers(commit, vs), vs) + + // check the signers account for +2/3 of the voting power + if signedPower * 3 <= totalPower * 2 return ErrInvalidCommit + return nil +} +``` + +**VerifyHeaderAtHeight.** The function `VerifyHeaderAtHeight` captures high level +logic, i.e., application call to the light client module to download and verify header +for some height. + +```go +func VerifyHeaderAtHeight(untrustedHeight int64, + trustedState TrustedState, + trustThreshold float, + trustingPeriod Duration, + clockDrift Duration) (TrustedState, error)) { + + trustedHeader := trustedState.SignedHeader.Header + + now := System.Time() + if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { + return (trustedState, ErrHeaderNotWithinTrustedPeriod) + } + + newTrustedState, err := VerifyBisection(untrustedHeight, + trustedState, + trustThreshold, + trustingPeriod, + clockDrift, + now) + + if err != nil return (trustedState, err) + + now = System.Time() + if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { + return (trustedState, ErrHeaderNotWithinTrustedPeriod) + } + + return (newTrustedState, err) +} +``` + +Note that in case `VerifyHeaderAtHeight` returns without an error (untrusted header +is successfully verified) then we have a guarantee that the transition of the trust +from `trustedState` to `newTrustedState` happened during the trusted period of +`trustedState.SignedHeader.Header`. + +In case `VerifyHeaderAtHeight` returns with an error, then either (i) the full node we are talking to is faulty +or (ii) the trusted header has expired (it is outside its trusted period). In case (i) the full node is faulty so +light client should disconnect and reinitialise with new peer. In the case (ii) as the trusted header has expired, +we need to reinitialise light client with a new trusted header (that is within its trusted period), +but we don't necessarily need to disconnect from the full node we are talking to (as we haven't observed full node misbehavior in this case). + +**VerifyBisection.** The function `VerifyBisection` implements +recursive logic for checking if it is possible building trust +relationship between `trustedState` and untrusted header at the given height over +finite set of (downloaded and verified) headers. + +```go +func VerifyBisection(untrustedHeight int64, + trustedState TrustedState, + trustThreshold float, + trustingPeriod Duration, + clockDrift Duration, + now Time) (TrustedState, error) { + + untrustedSh, error := Commit(untrustedHeight) + if error != nil return (trustedState, ErrRequestFailed) + + untrustedHeader = untrustedSh.Header + + // note that we pass now during the recursive calls. This is fine as + // all other untrusted headers we download during recursion will be + // for a smaller heights, and therefore should happen before. + if untrustedHeader.Time > now + clockDrift { + return (trustedState, ErrInvalidHeaderTime) + } + + untrustedVs, error := Validators(untrustedHeight) + if error != nil return (trustedState, ErrRequestFailed) + + untrustedNextVs, error := Validators(untrustedHeight + 1) + if error != nil return (trustedState, ErrRequestFailed) + + error = verifySingle( + trustedState, + untrustedSh, + untrustedVs, + untrustedNextVs, + trustThreshold) + + if fatalError(error) return (trustedState, error) + + if error == nil { + // the untrusted header is now trusted. + newTrustedState = TrustedState(untrustedSh, untrustedNextVs) + return (newTrustedState, nil) + } + + // at this point in time we need to do bisection + pivotHeight := ceil((trustedHeader.Height + untrustedHeight) / 2) + + error, newTrustedState = VerifyBisection(pivotHeight, + trustedState, + trustThreshold, + trustingPeriod, + clockDrift, + now) + if error != nil return (newTrustedState, error) + + return VerifyBisection(untrustedHeight, + newTrustedState, + trustThreshold, + trustingPeriod, + clockDrift, + now) +} + +func fatalError(err) bool { + return err == ErrHeaderNotWithinTrustedPeriod OR + err == ErrInvalidAdjacentHeaders OR + err == ErrNonIncreasingHeight OR + err == ErrNonIncreasingTime OR + err == ErrInvalidValidatorSet OR + err == ErrInvalidNextValidatorSet OR + err == ErrInvalidCommitValue OR + err == ErrInvalidCommit +} +``` + +### The case `untrustedHeader.Height < trustedHeader.Height` + +In the use case where someone tells the light client that application data that is relevant for it +can be read in the block of height `k` and the light client trusts a more recent header, we can use the +hashes to verify headers "down the chain." That is, we iterate down the heights and check the hashes in each step. + +*Remark.* For the case were the light client trusts two headers `i` and `j` with `i < k < j`, we should +discuss/experiment whether the forward or the backward method is more effective. + +```go +func VerifyHeaderBackwards(trustedHeader Header, + untrustedHeader Header, + trustingPeriod Duration, + clockDrift Duration) error { + + if untrustedHeader.Height >= trustedHeader.Height return ErrErrNonDecreasingHeight + if untrustedHeader.Time >= trustedHeader.Time return ErrNonDecreasingTime + + now := System.Time() + if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { + return ErrHeaderNotWithinTrustedPeriod + } + + old := trustedHeader + for i := trustedHeader.Height - 1; i > untrustedHeader.Height; i-- { + untrustedSh, error := Commit(i) + if error != nil return ErrRequestFailed + + if (hash(untrustedSh.Header) != old.LastBlockID.Hash) { + return ErrInvalidAdjacentHeaders + } + + old := untrustedSh.Header + } + + if hash(untrustedHeader) != old.LastBlockID.Hash { + return ErrInvalidAdjacentHeaders + } + + now := System.Time() + if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { + return ErrHeaderNotWithinTrustedPeriod + } + + return nil + } +``` + +*Assumption*: In the following, we assume that *untrusted_h.Header.height > trusted_h.Header.height*. We will quickly discuss the other case in the next section. + +We consider the following set-up: + +- the light client communicates with one full node +- the light client locally stores all the headers that has passed basic verification and that are within light client trust period. In the pseudo code below we +write *Store.Add(header)* for this. If a header failed to verify, then +the full node we are talking to is faulty and we should disconnect from it and reinitialise with new peer. +- If `CanTrust` returns *error*, then the light client has seen a forged header or the trusted header has expired (it is outside its trusted period). + - In case of forged header, the full node is faulty so light client should disconnect and reinitialise with new peer. If the trusted header has expired, + we need to reinitialise light client with new trusted header (that is within its trusted period), but we don't necessarily need to disconnect from the full node + we are talking to (as we haven't observed full node misbehavior in this case). + +## Correctness of the Light Client Protocols + +### Definitions + +- `TRUSTED_PERIOD`: trusted period +- for realtime `t`, the predicate `correct(v,t)` is true if the validator `v` + follows the protocol until time `t` (we will see about recovery later). +- Validator fields. We will write a validator as a tuple `(v,p)` such that + - `v` is the identifier (i.e., validator address; we assume identifiers are unique in each validator set) + - `p` is its voting power +- For each header `h`, we write `trust(h) = true` if the light client trusts `h`. + +### Failure Model + +If a block `b` with a header `h` is generated at time `Time` (i.e. `h.Time = Time`), then a set of validators that +hold more than `2/3` of the voting power in `validators(h.NextValidatorsHash)` is correct until time +`h.Time + TRUSTED_PERIOD`. + +Formally, +\[ +\sum_{(v,p) \in validators(h.NextValidatorsHash) \wedge correct(v,h.Time + TRUSTED_PERIOD)} p > +2/3 \sum_{(v,p) \in validators(h.NextValidatorsHash)} p +\] + +The light client communicates with a full node and learns new headers. The goal is to locally decide whether to trust a header. Our implementation needs to ensure the following two properties: + +- *Light Client Completeness*: If a header `h` was correctly generated by an instance of Tendermint consensus (and its age is less than the trusted period), +then the light client should eventually set `trust(h)` to `true`. + +- *Light Client Accuracy*: If a header `h` was *not generated* by an instance of Tendermint consensus, then the light client should never set `trust(h)` to true. + +*Remark*: If in the course of the computation, the light client obtains certainty that some headers were forged by adversaries +(that is were not generated by an instance of Tendermint consensus), it may submit (a subset of) the headers it has seen as evidence of misbehavior. + +*Remark*: In Completeness we use "eventually", while in practice `trust(h)` should be set to true before `h.Time + TRUSTED_PERIOD`. If not, the header +cannot be trusted because it is too old. + +*Remark*: If a header `h` is marked with `trust(h)`, but it is too old at some point in time we denote with `now` (`h.Time + TRUSTED_PERIOD < now`), +then the light client should set `trust(h)` to `false` again at time `now`. + +*Assumption*: Initially, the light client has a header `inithead` that it trusts, that is, `inithead` was correctly generated by the Tendermint consensus. + +To reason about the correctness, we may prove the following invariant. + +*Verification Condition: light Client Invariant.* + For each light client `l` and each header `h`: +if `l` has set `trust(h) = true`, + then validators that are correct until time `h.Time + TRUSTED_PERIOD` have more than two thirds of the voting power in `validators(h.NextValidatorsHash)`. + + Formally, + \[ + \sum_{(v,p) \in validators(h.NextValidatorsHash) \wedge correct(v,h.Time + TRUSTED_PERIOD)} p > + 2/3 \sum_{(v,p) \in validators(h.NextValidatorsHash)} p + \] + +*Remark.* To prove the invariant, we will have to prove that the light client only trusts headers that were correctly generated by Tendermint consensus. +Then the formula above follows from the failure model. + +## Details + +**Observation 1.** If `h.Time + TRUSTED_PERIOD > now`, we trust the validator set `validators(h.NextValidatorsHash)`. + +When we say we trust `validators(h.NextValidatorsHash)` we do `not` trust that each individual validator in `validators(h.NextValidatorsHash)` +is correct, but we only trust the fact that less than `1/3` of them are faulty (more precisely, the faulty ones have less than `1/3` of the total voting power). + +*`VerifySingle` correctness arguments* + +Light Client Accuracy: + +- Assume by contradiction that `untrustedHeader` was not generated correctly and the light client sets trust to true because `verifySingle` returns without error. +- `trustedState` is trusted and sufficiently new +- by the Failure Model, less than `1/3` of the voting power held by faulty validators => at least one correct validator `v` has signed `untrustedHeader`. +- as `v` is correct up to now, it followed the Tendermint consensus protocol at least up to signing `untrustedHeader` => `untrustedHeader` was correctly generated. +We arrive at the required contradiction. + +Light Client Completeness: + +- The check is successful if sufficiently many validators of `trustedState` are still validators in the height `untrustedHeader.Height` and signed `untrustedHeader`. +- If `untrustedHeader.Height = trustedHeader.Height + 1`, and both headers were generated correctly, the test passes. + +*Verification Condition:* We may need a Tendermint invariant stating that if `untrustedSignedHeader.Header.Height = trustedHeader.Height + 1` then +`signers(untrustedSignedHeader.Commit) \subseteq validators(trustedHeader.NextValidatorsHash)`. + +*Remark*: The variable `trustThreshold` can be used if the user believes that relying on one correct validator is not sufficient. +However, in case of (frequent) changes in the validator set, the higher the `trustThreshold` is chosen, the more unlikely it becomes that +`verifySingle` returns with an error for non-adjacent headers. + +- `VerifyBisection` correctness arguments (sketch)* + +Light Client Accuracy: + +- Assume by contradiction that the header at `untrustedHeight` obtained from the full node was not generated correctly and +the light client sets trust to true because `VerifyBisection` returns without an error. +- `VerifyBisection` returns without error only if all calls to `verifySingle` in the recursion return without error (return `nil`). +- Thus we have a sequence of headers that all satisfied the `verifySingle` +- again a contradiction + +light Client Completeness: + +This is only ensured if upon `Commit(pivot)` the light client is always provided with a correctly generated header. + +*Stalling* + +With `VerifyBisection`, a faulty full node could stall a light client by creating a long sequence of headers that are queried one-by-one by the light client and look OK, +before the light client eventually detects a problem. There are several ways to address this: + +- Each call to `Commit` could be issued to a different full node +- Instead of querying header by header, the light client tells a full node which header it trusts, and the height of the header it needs. The full node responds with +the header along with a proof consisting of intermediate headers that the light client can use to verify. Roughly, `VerifyBisection` would then be executed at the full node. +- We may set a timeout how long `VerifyBisection` may take. diff --git a/spec/light-client/verification/verification_001_published.md b/spec/light-client/verification/verification_001_published.md new file mode 100644 index 000000000..047b1a86b --- /dev/null +++ b/spec/light-client/verification/verification_001_published.md @@ -0,0 +1,1178 @@ +# Light Client Verification + +The light client implements a read operation of a +[header][TMBC-HEADER-link] from the [blockchain][TMBC-SEQ-link], by +communicating with full nodes. As some full nodes may be faulty, this +functionality must be implemented in a fault-tolerant way. + +In the Tendermint blockchain, the validator set may change with every +new block. The staking and unbonding mechanism induces a [security +model][TMBC-FM-2THIRDS-link]: starting at time *Time* of the +[header][TMBC-HEADER-link], +more than two-thirds of the next validators of a new block are correct +for the duration of *TrustedPeriod*. The fault-tolerant read +operation is designed for this security model. + +The challenge addressed here is that the light client might have a +block of height *h1* and needs to read the block of height *h2* +greater than *h1*. Checking all headers of heights from *h1* to *h2* +might be too costly (e.g., in terms of energy for mobile devices). +This specification tries to reduce the number of intermediate blocks +that need to be checked, by exploiting the guarantees provided by the +[security model][TMBC-FM-2THIRDS-link]. + +# Status + +This document is thoroughly reviewed, and the protocol has been +formalized in TLA+ and model checked. + +## Issues that need to be addressed + +As it is part of the larger light node, its data structures and +functions interact with the fork dectection functionality of the light +client. As a result of the work on +[Pull Request 479](https://github.com/informalsystems/tendermint-rs/pull/479) we +established the need for an update in the data structures in [Issue 499](https://github.com/informalsystems/tendermint-rs/issues/499). This +will not change the verification logic, but it will record information +about verification that can be used in fork detection (in particular +in computing more efficiently the proof of fork). + +# Outline + +- [Part I](#part-i---tendermint-blockchain): Introduction of + relevant terms of the Tendermint +blockchain. + +- [Part II](#part-ii---sequential-definition-of-the-verification-problem): Introduction +of the problem addressed by the Lightclient Verification protocol. + - [Verification Informal Problem + statement](#Verification-Informal-Problem-statement): For the general + audience, that is, engineers who want to get an overview over what + the component is doing from a bird's eye view. + - [Sequential Problem statement](#Sequential-Problem-statement): + Provides a mathematical definition of the problem statement in + its sequential form, that is, ignoring the distributed aspect of + the implementation of the blockchain. + +- [Part III](#part-iii---light-client-as-distributed-system): Distributed + aspects of the light client, system assumptions and temporal + logic specifications. + + - [Incentives](#incentives): how faulty full nodes may benefit from + misbehaving and how correct full nodes benefit from cooperating. + + - [Computational Model](#Computational-Model): + timing and correctness assumptions. + + - [Distributed Problem Statement](#Distributed-Problem-Statement): + temporal properties that formalize safety and liveness + properties in the distributed setting. + +- [Part IV](#part-iv---light-client-verification-protocol): + Specification of the protocols. + + - [Definitions](#Definitions): Describes inputs, outputs, + variables used by the protocol, auxiliary functions + + - [Core Verification](#core-verification): gives an outline of the solution, + and details of the functions used (with preconditions, + postconditions, error conditions). + + - [Liveness Scenarios](#liveness-scenarios): when the light + client makes progress depends heavily on the changes in the + validator sets of the blockchain. We discuss some typical scenarios. + +- [Part V](#part-v---supporting-the-ibc-relayer): The above parts + focus on a common case where the last verified block has height *h1* + and the + requested height *h2* satisfies *h2 > h1*. For IBC, there are + scenarios where this might not be the case. In this part, we provide + some preliminaries for supporting this. As not all details of the + IBC requirements are clear by now, we do not provide a complete + specification at this point. We mark with "Open Question" points + that need to be addressed in order to finalize this specification. + It should be noted that the technically + most challenging case is the one specified in Part IV. + +In this document we quite extensively use tags in order to be able to +reference assumptions, invariants, etc. in future communication. In +these tags we frequently use the following short forms: + +- TMBC: Tendermint blockchain +- SEQ: for sequential specifications +- LCV: Lightclient Verification +- LIVE: liveness +- SAFE: safety +- FUNC: function +- INV: invariant +- A: assumption + +# Part I - Tendermint Blockchain + +## Header Fields necessary for the Light Client + +#### **[TMBC-HEADER.1]** + +A set of blockchain transactions is stored in a data structure called +*block*, which contains a field called *header*. (The data structure +*block* is defined [here][block]). As the header contains hashes to +the relevant fields of the block, for the purpose of this +specification, we will assume that the blockchain is a list of +headers, rather than a list of blocks. + +#### **[TMBC-HASH-UNIQUENESS.1]** + +We assume that every hash in the header identifies the data it hashes. +Therefore, in this specification, we do not distinguish between hashes and the +data they represent. + +#### **[TMBC-HEADER-FIELDS.1]** + +A header contains the following fields: + +- `Height`: non-negative integer +- `Time`: time (integer) +- `LastBlockID`: Hashvalue +- `LastCommit` DomainCommit +- `Validators`: DomainVal +- `NextValidators`: DomainVal +- `Data`: DomainTX +- `AppState`: DomainApp +- `LastResults`: DomainRes + +#### **[TMBC-SEQ.1]** + +The Tendermint blockchain is a list *chain* of headers. + +#### **[TMBC-VALIDATOR-PAIR.1]** + +Given a full node, a +*validator pair* is a pair *(peerID, voting_power)*, where + +- *peerID* is the PeerID (public key) of a full node, +- *voting_power* is an integer (representing the full node's + voting power in a certain consensus instance). + +> In the Golang implementation the data type for *validator +pair* is called `Validator` + +#### **[TMBC-VALIDATOR-SET.1]** + +A *validator set* is a set of validator pairs. For a validator set +*vs*, we write *TotalVotingPower(vs)* for the sum of the voting powers +of its validator pairs. + +#### **[TMBC-VOTE.1]** + +A *vote* contains a `prevote` or `precommit` message sent and signed by +a validator node during the execution of [consensus][arXiv]. Each +message contains the following fields + +- `Type`: prevote or precommit +- `Height`: positive integer +- `Round` a positive integer +- `BlockID` a Hashvalue of a block (not necessarily a block of the chain) + +#### **[TMBC-COMMIT.1]** + +A commit is a set of `precommit` message. + +## Tendermint Failure Model + +#### **[TMBC-AUTH-BYZ.1]** + +We assume the authenticated Byzantine fault model in which no node (faulty or +correct) may break digital signatures, but otherwise, no additional +assumption is made about the internal behavior of faulty +nodes. That is, faulty nodes are only limited in that they cannot forge +messages. + +#### **[TMBC-TIME-PARAMS.1]** + +A Tendermint blockchain has the following configuration parameters: + +- *unbondingPeriod*: a time duration. +- *trustingPeriod*: a time duration smaller than *unbondingPeriod*. + +#### **[TMBC-CORRECT.1]** + +We define a predicate *correctUntil(n, t)*, where *n* is a node and *t* is a +time point. +The predicate *correctUntil(n, t)* is true if and only if the node *n* +follows all the protocols (at least) until time *t*. + +#### **[TMBC-FM-2THIRDS.1]** + +If a block *h* is in the chain, +then there exists a subset *CorrV* +of *h.NextValidators*, such that: + +- *TotalVotingPower(CorrV) > 2/3 + TotalVotingPower(h.NextValidators)*; cf. [TMBC-VALIDATOR-SET.1] +- For every validator pair *(n,p)* in *CorrV*, it holds *correctUntil(n, + h.Time + trustingPeriod)*; cf. [TMBC-CORRECT.1] + +> The definition of correct +> [**[TMBC-CORRECT.1]**][TMBC-CORRECT-link] refers to realtime, while it +> is used here with *Time* and *trustingPeriod*, which are "hardware +> times". We do not make a distinction here. + +#### **[TMBC-CORR-FULL.1]** + +Every correct full node locally stores a prefix of the +current list of headers from [**[TMBC-SEQ.1]**][TMBC-SEQ-link]. + +## What the Light Client Checks + +> From [TMBC-FM-2THIRDS.1] we directly derive the following observation: + +#### **[TMBC-VAL-CONTAINS-CORR.1]** + +Given a (trusted) block *tb* of the blockchain, a given set of full nodes +*N* contains a correct node at a real-time *t*, if + +- *t - trustingPeriod < tb.Time < t* +- the voting power in tb.NextValidators of nodes in *N* is more + than 1/3 of *TotalVotingPower(tb.NextValidators)* + +> The following describes how a commit for a given block *b* must look +> like. + +#### **[TMBC-SOUND-DISTR-POSS-COMMIT.1]** + +For a block *b*, each element *pc* of *PossibleCommit(b)* satisfies: + +- *pc* contains only votes (cf. [TMBC-VOTE.1]) + by validators from *b.Validators* +- the sum of the voting powers in *pc* is greater than 2/3 + *TotalVotingPower(b.Validators)* +- and there is an *r* such that each vote *v* in *pc* satisfies + - v.Type = precommit + - v.Height = b.Height + - v.Round = r + - v.blockID = hash(b) + +> The following property comes from the validity of the [consensus][arXiv]: A +> correct validator node only sends `prevote` or `precommit`, if +> `BlockID` of the new (to-be-decided) block is equal to the hash of +> the last block. + +#### **[TMBC-VAL-COMMIT.1]** + +If for a block *b*, a commit *c* + +- contains at least one validator pair *(v,p)* such that *v* is a + **correct** validator node, and +- is contained in *PossibleCommit(b)* + +then the block *b* is on the blockchain. + +## Context of this document + +In this document we specify the light client verification component, +called *Core Verification*. The *Core Verification* communicates with +a full node. As full nodes may be faulty, it cannot trust the +received information, but the light client has to check whether the +header it receives coincides with the one generated by Tendermint +consensus. + +The two + properties [[TMBC-VAL-CONTAINS-CORR.1]][TMBC-VAL-CONTAINS-CORR-link] and +[[TMBC-VAL-COMMIT]][TMBC-VAL-COMMIT-link] formalize the checks done + by this specification: +Given a trusted block *tb* and an untrusted block *ub* with a commit *cub*, +one has to check that *cub* is in *PossibleCommit(ub)*, and that *cub* +contains a correct node using *tb*. + +# Part II - Sequential Definition of the Verification Problem + +## Verification Informal Problem statement + +Given a height *targetHeight* as an input, the *Verifier* eventually +stores a header *h* of height *targetHeight* locally. This header *h* +is generated by the Tendermint [blockchain][block]. In +particular, a header that was not generated by the blockchain should +never be stored. + +## Sequential Problem statement + +#### **[LCV-SEQ-LIVE.1]** + +The *Verifier* gets as input a height *targetHeight*, and eventually stores the +header of height *targetHeight* of the blockchain. + +#### **[LCV-SEQ-SAFE.1]** + +The *Verifier* never stores a header which is not in the blockchain. + +# Part III - Light Client as Distributed System + +## Incentives + +Faulty full nodes may benefit from lying to the light client, by making the +light client accept a block that deviates (e.g., contains additional +transactions) from the one generated by Tendermint consensus. +Users using the light client might be harmed by accepting a forged header. + +The [fork detector][fork-detector] of the light client may help the +correct full nodes to understand whether their header is a good one. +Hence, in combination with the light client detector, the correct full +nodes have the incentive to respond. We can thus base liveness +arguments on the assumption that correct full nodes reliably talk to +the light client. + +## Computational Model + +#### **[LCV-A-PEER.1]** + +The verifier communicates with a full node called *primary*. No assumption is made about the full node (it may be correct or faulty). + +#### **[LCV-A-COMM.1]** + +Communication between the light client and a correct full node is +reliable and bounded in time. Reliable communication means that +messages are not lost, not duplicated, and eventually delivered. There +is a (known) end-to-end delay *Delta*, such that if a message is sent +at time *t* then it is received and processes by time *t + Delta*. +This implies that we need a timeout of at least *2 Delta* for remote +procedure calls to ensure that the response of a correct peer arrives +before the timeout expires. + +#### **[LCV-A-TFM.1]** + +The Tendermint blockchain satisfies the Tendermint failure model [**[TMBC-FM-2THIRDS.1]**][TMBC-FM-2THIRDS-link]. + +#### **[LCV-A-VAL.1]** + +The system satisfies [**[TMBC-AUTH-BYZ.1]**][TMBC-Auth-Byz-link] and +[**[TMBC-FM-2THIRDS.1]**][TMBC-FM-2THIRDS-link]. Thus, there is a +blockchain that satisfies the soundness requirements (that is, the +validation rules in [[block]]). + +## Distributed Problem Statement + +### Two Kinds of Termination + +We do not assume that *primary* is correct. Under this assumption no +protocol can guarantee the combination of the sequential +properties. Thus, in the (unreliable) distributed setting, we consider +two kinds of termination (successful and failure) and we will specify +below under what (favorable) conditions *Core Verification* ensures to +terminate successfully, and satisfy the requirements of the sequential +problem statement: + +#### **[LCV-DIST-TERM.1]** + +*Core Verification* either *terminates +successfully* or it *terminates with failure*. + +### Design choices + +#### **[LCV-DIST-STORE.1]** + +*Core Verification* has a local data structure called *LightStore* that +contains light blocks (that contain a header). For each light block we +record whether it is verified. + +#### **[LCV-DIST-PRIMARY.1]** + +*Core Verification* has a local variable *primary* that contains the PeerID of a full node. + +#### **[LCV-DIST-INIT.1]** + +*LightStore* is initialized with a header *trustedHeader* that was correctly +generated by the Tendermint consensus. We say *trustedHeader* is verified. + +### Temporal Properties + +#### **[LCV-DIST-SAFE.1]** + +It is always the case that every verified header in *LightStore* was +generated by an instance of Tendermint consensus. + +#### **[LCV-DIST-LIVE.1]** + +From time to time, a new instance of *Core Verification* is called with a +height *targetHeight* greater than the height of any header in *LightStore*. +Each instance must eventually terminate. + +- If + - the *primary* is correct (and locally has the block of + *targetHeight*), and + - *LightStore* always contains a verified header whose age is less than the + trusting period, + then *Core Verification* adds a verified header *hd* with height + *targetHeight* to *LightStore* and it **terminates successfully** + +> These definitions imply that if the primary is faulty, a header may or +> may not be added to *LightStore*. In any case, +> [**[LCV-DIST-SAFE.1]**](#lcv-vc-inv) must hold. +> The invariant [**[LCV-DIST-SAFE.1]**](#lcv-dist-safe) and the liveness +> requirement [**[LCV-DIST-LIVE.1]**](#lcv-dist-life) +> allow that verified headers are added to *LightStore* whose +> height was not passed +> to the verifier (e.g., intermediate headers used in bisection; see below). +> Note that for liveness, initially having a *trustedHeader* within +> the *trustinPeriod* is not sufficient. However, as this +> specification will leave some freedom with respect to the strategy +> in which order to download intermediate headers, we do not give a +> more precise liveness specification here. After giving the +> specification of the protocol, we will discuss some liveness +> scenarios [below](#liveness-scenarios). + +### Solving the sequential specification + +This specification provides a partial solution to the sequential specification. +The *Verifier* solves the invariant of the sequential part + +[**[LCV-DIST-SAFE.1]**](#lcv-vc-inv) => [**[LCV-SEQ-SAFE.1]**](#lcv-seq-inv) + +In the case the primary is correct, and there is a recent header in *LightStore*, the verifier satisfies the liveness requirements. + +⋀ *primary is correct* +⋀ always ∃ verified header in LightStore. *header.Time* > *now* - *trustingPeriod* +⋀ [**[LCV-A-Comm.1]**](#lcv-a-comm) ⋀ ( + ( [**[TMBC-CorrFull.1]**][TMBC-CorrFull-link] ⋀ + [**[LCV-DIST-LIVE.1]**](#lcv-vc-live) ) + ⟹ [**[LCV-SEQ-LIVE.1]**](#lcv-seq-live) +) + +# Part IV - Light Client Verification Protocol + +We provide a specification for Light Client Verification. The local +code for verification is presented by a sequential function +`VerifyToTarget` to highlight the control flow of this functionality. +We note that if a different concurrency model is considered for +an implementation, the sequential flow of the function may be +implemented with mutexes, etc. However, the light client verification +is partitioned into three blocks that can be implemented and tested +independently: + +- `FetchLightBlock` is called to download a light block (header) of a + given height from a peer. +- `ValidAndVerified` is a local code that checks the header. +- `Schedule` decides which height to try to verify next. We keep this + underspecified as different implementations (currently in Goland and + Rust) may implement different optimizations here. We just provide + necessary conditions on how the height may evolve. + + + + +## Definitions + +### Data Types + +The core data structure of the protocol is the LightBlock. + +#### **[LCV-DATA-LIGHTBLOCK.1]** + +```go +type LightBlock struct { + Header Header + Commit Commit + Validators ValidatorSet +} +``` + +#### **[LCV-DATA-LIGHTSTORE.1]** + +LightBlocks are stored in a structure which stores all LightBlock from +initialization or received from peers. + +```go +type LightStore struct { + ... +} + +``` + +Each LightBlock is in one of the following states: + +```go +type VerifiedState int + +const ( + StateUnverified = iota + 1 + StateVerified + StateFailed + StateTrusted +) +``` + +> Only the detector module sets a lightBlock state to `StateTrusted` +> and only if it was `StateVerified` before. + +The LightStore exposes the following functions to query stored LightBlocks. + +#### **[LCV-FUNC-GET.1]** + +```go +func (ls LightStore) Get(height Height) (LightBlock, bool) +``` + +- Expected postcondition + - returns a LightBlock at a given height or false in the second argument if + the LightStore does not contain the specified LightBlock. + +#### **[LCV-FUNC-LATEST-VERIF.1]** + +```go +func (ls LightStore) LatestVerified() LightBlock +``` + +- Expected postcondition + - returns the highest light block whose state is `StateVerified` + or `StateTrusted` + +#### **[LCV-FUNC-UPDATE.2]** + +```go +func (ls LightStore) Update(lightBlock LightBlock, + verfiedState VerifiedState + verifiedBy Height) +``` + +- Expected postcondition + - The state of the LightBlock is set to *verifiedState*. + - verifiedBy of the Lightblock is set to *Height* + +> The following function is used only in the detector specification +> listed here for completeness. + +#### **[LCV-FUNC-LATEST-TRUSTED.1]** + +```go +func (ls LightStore) LatestTrusted() LightBlock +``` + +- Expected postcondition + - returns the highest light block that has been verified and + checked by the detector. + +#### **[LCV-FUNC-FILTER.1]** + +```go +func (ls LightStore) FilterVerified() LightSTore +``` + +- Expected postcondition + - returns only the LightBlocks with state verified. + +### Inputs + +- *lightStore*: stores light blocks that have been downloaded and that + passed verification. Initially it contains a light block with + *trustedHeader*. +- *primary*: peerID +- *targetHeight*: the height of the needed header + +### Configuration Parameters + +- *trustThreshold*: a float. Can be used if correctness should not be based on more voting power and 1/3. +- *trustingPeriod*: a time duration [**[TMBC-TIME_PARAMS.1]**][TMBC-TIME_PARAMS-link]. +- *clockDrift*: a time duration. Correction parameter dealing with only approximately synchronized clocks. + +### Variables + +- *nextHeight*: initially *targetHeight* + > *nextHeight* should be thought of the "height of the next header we need + > to download and verify" + +### Assumptions + +#### **[LCV-A-INIT.1]** + +- *trustedHeader* is from the blockchain + +- *targetHeight > LightStore.LatestVerified.Header.Height* + +### Invariants + +#### **[LCV-INV-TP.1]** + +It is always the case that *LightStore.LatestTrusted.Header.Time > now - trustingPeriod*. + +> If the invariant is violated, the light client does not have a +> header it can trust. A trusted header must be obtained externally, +> its trust can only be based on social consensus. + +### Used Remote Functions + +We use the functions `commit` and `validators` that are provided +by the [RPC client for Tendermint][RPC]. + +```go +func Commit(height int64) (SignedHeader, error) +``` + +- Implementation remark + - RPC to full node *n* + - JSON sent: + +```javascript +// POST /commit +{ + "jsonrpc": "2.0", + "id": "ccc84631-dfdb-4adc-b88c-5291ea3c2cfb", // UUID v4, unique per request + "method": "commit", + "params": { + "height": 1234 + } +} +``` + +- Expected precondition + - header of `height` exists on blockchain +- Expected postcondition + - if *n* is correct: Returns the signed header of height `height` + from the blockchain if communication is timely (no timeout) + - if *n* is faulty: Returns a signed header with arbitrary content +- Error condition + - if *n* is correct: precondition violated or timeout + - if *n* is faulty: arbitrary error + +---- + +```go +func Validators(height int64) (ValidatorSet, error) +``` + +- Implementation remark + - RPC to full node *n* + - JSON sent: + +```javascript +// POST /validators +{ + "jsonrpc": "2.0", + "id": "ccc84631-dfdb-4adc-b88c-5291ea3c2cfb", // UUID v4, unique per request + "method": "validators", + "params": { + "height": 1234 + } +} +``` + +- Expected precondition + - header of `height` exists on blockchain +- Expected postcondition + - if *n* is correct: Returns the validator set of height `height` + from the blockchain if communication is timely (no timeout) + - if *n* is faulty: Returns arbitrary validator set +- Error condition + - if *n* is correct: precondition violated or timeout + - if *n* is faulty: arbitrary error + +---- + +### Communicating Function + +#### **[LCV-FUNC-FETCH.1]** + + ```go +func FetchLightBlock(peer PeerID, height Height) LightBlock +``` + +- Implementation remark + - RPC to peer at *PeerID* + - calls `Commit` for *height* and `Validators` for *height* and *height+1* +- Expected precondition + - `height` is less than or equal to height of the peer **[LCV-IO-PRE-HEIGHT.1]** +- Expected postcondition: + - if *node* is correct: + - Returns the LightBlock *lb* of height `height` + that is consistent with the blockchain + - *lb.provider = peer* **[LCV-IO-POST-PROVIDER.1]** + - *lb.Header* is a header consistent with the blockchain + - *lb.Validators* is the validator set of the blockchain at height *nextHeight* + - *lb.NextValidators* is the validator set of the blockchain at height *nextHeight + 1* + - if *node* is faulty: Returns a LightBlock with arbitrary content + [**[TMBC-AUTH-BYZ.1]**][TMBC-Auth-Byz-link] +- Error condition + - if *n* is correct: precondition violated + - if *n* is faulty: arbitrary error + - if *lb.provider != peer* + - times out after 2 Delta (by assumption *n* is faulty) + +---- + +## Core Verification + +### Outline + +The `VerifyToTarget` is the main function and uses the following functions. + +- `FetchLightBlock` is called to download the next light block. It is + the only function that communicates with other nodes +- `ValidAndVerified` checks whether header is valid and checks if a + new lightBlock should be trusted + based on a previously verified lightBlock. +- `Schedule` decides which height to try to verify next + +In the following description of `VerifyToTarget` we do not deal with error +handling. If any of the above function returns an error, VerifyToTarget just +passes the error on. + +#### **[LCV-FUNC-MAIN.1]** + +```go +func VerifyToTarget(primary PeerID, lightStore LightStore, + targetHeight Height) (LightStore, Result) { + + nextHeight := targetHeight + + for lightStore.LatestVerified.height < targetHeight { + + // Get next LightBlock for verification + current, found := lightStore.Get(nextHeight) + if !found { + current = FetchLightBlock(primary, nextHeight) + lightStore.Update(current, StateUnverified) + } + + // Verify + verdict = ValidAndVerified(lightStore.LatestVerified, current) + + // Decide whether/how to continue + if verdict == SUCCESS { + lightStore.Update(current, StateVerified) + } + else if verdict == NOT_ENOUGH_TRUST { + // do nothing + // the light block current passed validation, but the validator + // set is too different to verify it. We keep the state of + // current at StateUnverified. For a later iteration, Schedule + // might decide to try verification of that light block again. + } + else { + // verdict is some error code + lightStore.Update(current, StateFailed) + // possibly remove all LightBlocks from primary + return (lightStore, ResultFailure) + } + nextHeight = Schedule(lightStore, nextHeight, targetHeight) + } + return (lightStore, ResultSuccess) +} +``` + +- Expected precondition + - *lightStore* contains a LightBlock within the *trustingPeriod* **[LCV-PRE-TP.1]** + - *targetHeight* is greater than the height of all the LightBlocks in *lightStore* +- Expected postcondition: + - returns *lightStore* that contains a LightBlock that corresponds to a block + of the blockchain of height *targetHeight* + (that is, the LightBlock has been added to *lightStore*) **[LCV-POST-LS.1]** +- Error conditions + - if the precondition is violated + - if `ValidAndVerified` or `FetchLightBlock` report an error + - if [**[LCV-INV-TP.1]**](#LCV-INV-TP.1) is violated + +### Details of the Functions + +#### **[LCV-FUNC-VALID.1]** + +```go +func ValidAndVerified(trusted LightBlock, untrusted LightBlock) Result +``` + +- Expected precondition: + - *untrusted* is valid, that is, satisfies the soundness [checks][block] + - *untrusted* is **well-formed**, that is, + - *untrusted.Header.Time < now + clockDrift* + - *untrusted.Validators = hash(untrusted.Header.Validators)* + - *untrusted.NextValidators = hash(untrusted.Header.NextValidators)* + - *trusted.Header.Time > now - trustingPeriod* + - *trusted.Commit* is a commit for the header + *trusted.Header*, i.e., it contains + the correct hash of the header, and +2/3 of signatures + - the `Height` and `Time` of `trusted` are smaller than the Height and + `Time` of `untrusted`, respectively + - the *untrusted.Header* is well-formed (passes the tests from + [[block]]), and in particular + - if the untrusted header `unstrusted.Header` is the immediate + successor of `trusted.Header`, then it holds that + - *trusted.Header.NextValidators = + untrusted.Header.Validators*, and + moreover, + - *untrusted.Header.Commit* + - contains signatures by more than two-thirds of the validators + - contains no signature from nodes that are not in *trusted.Header.NextValidators* +- Expected postcondition: + - Returns `SUCCESS`: + - if *untrusted* is the immediate successor of *trusted*, or otherwise, + - if the signatures of a set of validators that have more than + *max(1/3,trustThreshold)* of voting power in + *trusted.Nextvalidators* is contained in + *untrusted.Commit* (that is, header passes the tests + [**[TMBC-VAL-CONTAINS-CORR.1]**][TMBC-VAL-CONTAINS-CORR-link] + and [**[TMBC-VAL-COMMIT.1]**][TMBC-VAL-COMMIT-link]) + - Returns `NOT_ENOUGH_TRUST` if: + - *untrusted* is *not* the immediate successor of + *trusted* + and the *max(1/3,trustThreshold)* threshold is not reached + (that is, if + [**[TMBC-VAL-CONTAINS-CORR.1]**][TMBC-VAL-CONTAINS-CORR-link] + fails and header is does not violate the soundness + checks [[block]]). +- Error condition: + - if precondition violated + +---- + +#### **[LCV-FUNC-SCHEDULE.1]** + +```go +func Schedule(lightStore, nextHeight, targetHeight) Height +``` + +- Implementation remark: If picks the next height to be verified. + We keep the precise choice of the next header under-specified. It is + subject to performance optimizations that do not influence the correctness +- Expected postcondition: **[LCV-SCHEDULE-POST.1]** + Return *H* s.t. + 1. if *lightStore.LatestVerified.Height = nextHeight* and + *lightStore.LatestVerified < targetHeight* then + *nextHeight < H <= targetHeight* + 2. if *lightStore.LatestVerified.Height < nextHeight* and + *lightStore.LatestVerified.Height < targetHeight* then + *lightStore.LatestVerified.Height < H < nextHeight* + 3. if *lightStore.LatestVerified.Height = targetHeight* then + *H = targetHeight* + +> Case i. captures the case where the light block at height *nextHeight* +> has been verified, and we can choose a height closer to the *targetHeight*. +> As we get the *lightStore* as parameter, the choice of the next height can +> depend on the *lightStore*, e.g., we can pick a height for which we have +> already downloaded a light block. +> In Case ii. the header of *nextHeight* could not be verified, and we need to pick a smaller height. +> In Case iii. is a special case when we have verified the *targetHeight*. + +### Solving the distributed specification + +*trustedStore* is implemented by the light blocks in lightStore that +have the state *StateVerified*. + +#### Argument for [**[LCV-DIST-SAFE.1]**](#lcv-dist-safe) + +- `ValidAndVerified` implements the soundness checks and the checks + [**[TMBC-VAL-CONTAINS-CORR.1]**][TMBC-VAL-CONTAINS-CORR-link] and + [**[TMBC-VAL-COMMIT.1]**][TMBC-VAL-COMMIT-link] under + the assumption [**[TMBC-FM-2THIRDS.1]**][TMBC-FM-2THIRDS-link] +- Only if `ValidAndVerified` returns with `SUCCESS`, the state of a light block is + set to *StateVerified*. + +#### Argument for [**[LCV-DIST-LIVE.1]**](#lcv-dist-life) + +- If *primary* is correct, + - `FetchLightBlock` will always return a light block consistent + with the blockchain + - `ValidAndVerified` either verifies the header using the trusting + period or falls back to sequential + verification + - If [**[LCV-INV-TP.1]**](#LCV-INV-TP.1) holds, eventually every + header will be verified and core verification **terminates successfully**. + - successful termination depends on the age of *lightStore.LatestVerified* + (for instance, initially on the age of *trustedHeader*) and the + changes of the validator sets on the blockchain. + We will give some examples [below](#liveness-scenarios). +- If *primary* is faulty, + - it either provides headers that pass all the tests, and we + return with the header + - it provides one header that fails a test, core verification + **terminates with failure**. + - it times out and core verification + **terminates with failure**. + +## Liveness Scenarios + +The liveness argument above assumes [**[LCV-INV-TP.1]**](#LCV-INV-TP.1) + +which requires that there is a header that does not expire before the +target height is reached. Here we discuss scenarios to ensure this. + +Let *startHeader* be *LightStore.LatestVerified* when core +verification is called (*trustedHeader*) and *startTime* be the time +core verification is invoked. + +In order to ensure liveness, *LightStore* always needs to contain a +verified (or initially trusted) header whose time is within the +trusting period. To ensure this, core verification needs to add new +headers to *LightStore* and verify them, before all headers in +*LightStore* expire. + +#### Many changes in validator set + + Let's consider `Schedule` implements + bisection, that is, it halves the distance. + Assume the case where the validator set changes completely in each +block. Then the + method in this specification needs to +sequentially verify all headers. That is, for + +- *W = log_2 (targetHeight - startHeader.Height)*, + +*W* headers need to be downloaded and checked before the +header of height *startHeader.Height + 1* is added to *LightStore*. + +- Let *Comp* + be the local computation time needed to check headers and signatures + for one header. +- Then we need in the worst case *Comp + 2 Delta* to download and + check one header. +- Then the first time a verified header could be added to *LightStore* is + startTime + W * (Comp + 2 Delta) +- [TP.1] However, it can only be added if we still have a header in + *LightStore*, + which is not + expired, that is only the case if + - startHeader.Time > startTime + WCG * (Comp + 2 Delta) - + trustingPeriod, + - that is, if core verification is started at + startTime < startHeader.Time + trustingPeriod - WCG * (Comp + 2 Delta) + +- one may then do an inductive argument from this point on, depending + on the implementation of `Schedule`. We may have to account for the + headers that are already + downloaded, but they are checked against the new *LightStore.LatestVerified*. + +> We observe that +> the worst case time it needs to verify the header of height +> *targetHeight* depends mainly on how frequent the validator set on the +> blockchain changes. That core verification terminates successfully +> crucially depends on the check [TP.1], that is, that the headers in +> *LightStore* do not expire in the time needed to download more +> headers, which depends on the creation time of the headers in +> *LightStore*. That is, termination of core verification is highly +> depending on the data stored in the blockchain. +> The current light client core verification protocol exploits that, in +> practice, changes in the validator set are rare. For instance, +> consider the following scenario. + +#### No change in validator set + +If on the blockchain the validator set of the block at height +*targetHeight* is equal to *startHeader.NextValidators*: + +- there is one round trip in `FetchLightBlock` to download the light + block + of height + *targetHeight*, and *Comp* to check it. +- as the validator sets are equal, `Verify` returns `SUCCESS`, if + *startHeader.Time > now - trustingPeriod*. +- that is, if *startTime < startHeader.Header.Time + trustingPeriod - + 2 Delta - Comp*, then core verification terminates successfully + +# Part V - Supporting the IBC Relayer + +The above specification focuses on the most common case, which also +constitutes the most challenging task: using the Tendermint [security +model][TMBC-FM-2THIRDS-link] to verify light blocks without +downloading all intermediate blocks. To focus on this challenge, above +we have restricted ourselves to the case where *targetHeight* is +greater than the height of any trusted header. This simplified +presentation of the algorithm as initially +`lightStore.LatestVerified()` is less than *targetHeight*, and in the +process of verification `lightStore.LatestVerified()` increases until +*targetHeight* is reached. + +For [IBC][ibc-rs] it might be that some "older" header is +needed, that is, *targetHeight < lightStore.LatestVerified()*. In this section we present a preliminary design, and we mark some +remaining open questions. +If *targetHeight < lightStore.LatestVerified()* our design separates +the following cases: + +- A previous instance of `VerifyToTarget` has already downloaded the + light block of *targetHeight*. There are two cases + - the light block has been verified + - the light block has not been verified yet +- No light block of *targetHeight* had been downloaded before. There + are two cases: + - there exists a verified light block of height less than *targetHeight* + - otherwise. In this case we need to do "backwards verification" + using the hash of the previous block in the `LastBlockID` field + of a header. + +**Open Question:** what are the security assumptions for backward +verification. Should we check that the light block we verify from +(and/or the checked light block) is within the trusting period? + +The design just presents the above case +distinction as a function, and defines some auxiliary functions in the +same way the protocol was presented in +[Part IV](#part-iv---light-client-verification-protocol). + +```go +func (ls LightStore) LatestPrevious(height Height) (LightBlock, bool) +``` + +- Expected postcondition + - returns a light block *lb* that satisfies: + - *lb* is in lightStore + - *lb* is verified and not expired + - *lb.Header.Height < height* + - for all *b* in lightStore s.t. *b* is verified and not expired it + holds *lb.Header.Height >= b.Header.Height* + - *false* in the second argument if + the LightStore does not contain such an *lb*. + +```go +func (ls LightStore) MinVerified() (LightBlock, bool) +``` + +- Expected postcondition + - returns a light block *lb* that satisfies: + - *lb* is in lightStore + - *lb* is verified **Open Question:** replace by trusted? + - *lb.Header.Height* is minimal in the lightStore + - **Open Question:** according to this, it might be expired (outside the + trusting period). This approach appears safe. Are there reasons we + should not do that? + - *false* in the second argument if + the LightStore does not contain such an *lb*. + +If a height that is smaller than the smallest height in the lightstore +is required, we check the hashes backwards. This is done with the +following function: + +#### **[LCV-FUNC-BACKWARDS.1]** + +```go +func Backwards (primary PeerID, lightStore LightStore, targetHeight Height) + (LightStore, Result) { + + lb,res = lightStore.MinVerified() + if res = false { + return (lightStore, ResultFailure) + } + + latest := lb.Header + for i := lb.Header.height - 1; i >= targetHeight; i-- { + // here we download height-by-height. We might first download all + // headers down to targetHeight and then check them. + current := FetchLightBlock(primary,i) + if (hash(current) != latest.Header.LastBlockId) { + return (lightStore, ResultFailure) + } + else { + lightStore.Update(current, StateVerified) + // **Open Question:** Do we need a new state type for + // backwards verified light blocks? + } + latest = current + } + return (lightStore, ResultSuccess) +} +``` + +The following function just decided based on the required height which +method should be used. + +#### **[LCV-FUNC-IBCMAIN.1]** + +```go +func Main (primary PeerID, lightStore LightStore, targetHeight Height) + (LightStore, Result) { + + b1, r1 = lightStore.Get(targetHeight) + if r1 = true and b1.State = StateVerified { + // block already there + return (lightStore, ResultSuccess) + } + + if targetHeight > lightStore.LatestVerified.height { + // case of Part IV + return VerifyToTarget(primary, lightStore, targetHeight) + } + else { + b2, r2 = lightStore.LatestPrevious(targetHeight); + if r2 = true { + // make auxiliary lightStore auxLS to call VerifyToTarget. + // VerifyToTarget uses LatestVerified of the given lightStore + // For that we need: + // auxLS.LatestVerified = lightStore.LatestPrevious(targetHeight) + auxLS.Init; + auxLS.Update(b2,StateVerified); + if r1 = true { + // we need to verify a previously downloaded light block. + // we add it to the auxiliary store so that VerifyToTarget + // does not download it again + auxLS.Update(b1,b1.State); + } + auxLS, res2 = VerifyToTarget(primary, auxLS, targetHeight) + // move all lightblocks from auxLS to lightStore, + // maintain state + // we do that whether VerifyToTarget was successful or not + for i, s range auxLS { + lighStore.Update(s,s.State) + } + return (lightStore, res2) + } + else { + return Backwards(primary, lightStore, targetHeight) + } + } +} +``` + + + + + + + + + + + + + + + + + + + +# References + +[[block]] Specification of the block data structure. + +[[RPC]] RPC client for Tendermint + +[[fork-detector]] The specification of the light client fork detector. + +[[fullnode]] Specification of the full node API + +[[ibc-rs]] Rust implementation of IBC modules and relayer. + +[[lightclient]] The light client ADR [77d2651 on Dec 27, 2019]. + +[RPC]: https://docs.tendermint.com/master/rpc/ + +[block]: https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md + +[TMBC-HEADER-link]: #tmbc-header1 +[TMBC-SEQ-link]: #tmbc-seq1 +[TMBC-CorrFull-link]: #tmbc-corr-full1 +[TMBC-Auth-Byz-link]: #tmbc-auth-byz1 +[TMBC-TIME_PARAMS-link]: #tmbc-time-params1 +[TMBC-FM-2THIRDS-link]: #tmbc-fm-2thirds1 +[TMBC-VAL-CONTAINS-CORR-link]: #tmbc-val-contains-corr1 +[TMBC-VAL-COMMIT-link]: #tmbc-val-commit1 +[TMBC-SOUND-DISTR-POSS-COMMIT-link]: #tmbc-sound-distr-poss-commit1 + +[lightclient]: https://github.com/interchainio/tendermint-rs/blob/e2cb9aca0b95430fca2eac154edddc9588038982/docs/architecture/adr-002-lite-client.md +[fork-detector]: https://github.com/informalsystems/tendermint-rs/blob/master/docs/spec/lightclient/detection.md +[fullnode]: https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/fullnode.md + +[ibc-rs]:https://github.com/informalsystems/ibc-rs + +[FN-LuckyCase-link]: https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/fullnode.md#fn-luckycase + +[blockchain-validator-set]: https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/blockchain.md#data-structures +[fullnode-data-structures]: https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/fullnode.md#data-structures + +[FN-ManifestFaulty-link]: https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/fullnode.md#fn-manifestfaulty + +[arXiv]: https://arxiv.org/abs/1807.04938 \ No newline at end of file diff --git a/spec/light-client/verification/verification_002_draft.md b/spec/light-client/verification/verification_002_draft.md new file mode 100644 index 000000000..23578ea34 --- /dev/null +++ b/spec/light-client/verification/verification_002_draft.md @@ -0,0 +1,1061 @@ +# Light Client Verification + +The light client implements a read operation of a +[header][TMBC-HEADER-link] from the [blockchain][TMBC-SEQ-link], by +communicating with full nodes. As some full nodes may be faulty, this +functionality must be implemented in a fault-tolerant way. + +In the Tendermint blockchain, the validator set may change with every +new block. The staking and unbonding mechanism induces a [security +model][TMBC-FM-2THIRDS-link]: starting at time *Time* of the +[header][TMBC-HEADER-link], +more than two-thirds of the next validators of a new block are correct +for the duration of *TrustedPeriod*. The fault-tolerant read +operation is designed for this security model. + +The challenge addressed here is that the light client might have a +block of height *h1* and needs to read the block of height *h2* +greater than *h1*. Checking all headers of heights from *h1* to *h2* +might be too costly (e.g., in terms of energy for mobile devices). +This specification tries to reduce the number of intermediate blocks +that need to be checked, by exploiting the guarantees provided by the +[security model][TMBC-FM-2THIRDS-link]. + +# Status + +## Previous Versions + +- [[001_published]](./verification_001_published.md) + is thoroughly reviewed, and the protocol has been +formalized in TLA+ and model checked. + +## Issues that are addressed in this revision + +As it is part of the larger light node, its data structures and +functions interact with the attack dectection functionality of the light +client. As a result of the work on + +- [attack detection](https://github.com/tendermint/spec/pull/164) for light nodes + +- attack detection for IBC and [relayer requirements](https://github.com/informalsystems/tendermint-rs/issues/497) + +- light client + [supervisor](https://github.com/tendermint/spec/pull/159) (also in + [Rust proposal](https://github.com/informalsystems/tendermint-rs/pull/509)) + +adaptations to the semantics and functions exposed by the LightStore +needed to be made. In contrast to [version +001](./verification_001_published.md) we specify the following: + +- `VerifyToTarget` and `Backwards` are called with a single lightblock + as root of trust in contrast to passing the complete lightstore. + +- During verification, we record for each lightblock which other + lightblock can be used to verify it in one step. This is needed to + generate verification traces that are needed for IBC. + +# Outline + +- [Part I](#part-i---tendermint-blockchain): Introduction of + relevant terms of the Tendermint +blockchain. + +- [Part II](#part-ii---sequential-definition-of-the-verification-problem): Introduction +of the problem addressed by the Lightclient Verification protocol. + - [Verification Informal Problem + statement](#Verification-Informal-Problem-statement): For the general + audience, that is, engineers who want to get an overview over what + the component is doing from a bird's eye view. + - [Sequential Problem statement](#Sequential-Problem-statement): + Provides a mathematical definition of the problem statement in + its sequential form, that is, ignoring the distributed aspect of + the implementation of the blockchain. + +- [Part III](#part-iii---light-client-as-distributed-system): Distributed + aspects of the light client, system assumptions and temporal + logic specifications. + + - [Incentives](#incentives): how faulty full nodes may benefit from + misbehaving and how correct full nodes benefit from cooperating. + + - [Computational Model](#Computational-Model): + timing and correctness assumptions. + + - [Distributed Problem Statement](#Distributed-Problem-Statement): + temporal properties that formalize safety and liveness + properties in the distributed setting. + +- [Part IV](#part-iv---light-client-verification-protocol): + Specification of the protocols. + + - [Definitions](#Definitions): Describes inputs, outputs, + variables used by the protocol, auxiliary functions + + - [Core Verification](#core-verification): gives an outline of the solution, + and details of the functions used (with preconditions, + postconditions, error conditions). + + - [Liveness Scenarios](#liveness-scenarios): when the light + client makes progress depends heavily on the changes in the + validator sets of the blockchain. We discuss some typical scenarios. + +- [Part V](#part-v---supporting-the-ibc-relayer): The above parts + focus on a common case where the last verified block has height *h1* + and the + requested height *h2* satisfies *h2 > h1*. For IBC, there are + scenarios where this might not be the case. In this part, we provide + some preliminaries for supporting this. As not all details of the + IBC requirements are clear by now, we do not provide a complete + specification at this point. We mark with "Open Question" points + that need to be addressed in order to finalize this specification. + It should be noted that the technically + most challenging case is the one specified in Part IV. + +In this document we quite extensively use tags in order to be able to +reference assumptions, invariants, etc. in future communication. In +these tags we frequently use the following short forms: + +- TMBC: Tendermint blockchain +- SEQ: for sequential specifications +- LCV: Lightclient Verification +- LIVE: liveness +- SAFE: safety +- FUNC: function +- INV: invariant +- A: assumption + +# Part I - Tendermint Blockchain + +## Header Fields necessary for the Light Client + +#### **[TMBC-HEADER.1]** + +A set of blockchain transactions is stored in a data structure called +*block*, which contains a field called *header*. (The data structure +*block* is defined [here][block]). As the header contains hashes to +the relevant fields of the block, for the purpose of this +specification, we will assume that the blockchain is a list of +headers, rather than a list of blocks. + +#### **[TMBC-HASH-UNIQUENESS.1]** + +We assume that every hash in the header identifies the data it hashes. +Therefore, in this specification, we do not distinguish between hashes and the +data they represent. + +#### **[TMBC-HEADER-FIELDS.2]** + +A header contains the following fields: + +- `Height`: non-negative integer +- `Time`: time (non-negative integer) +- `LastBlockID`: Hashvalue +- `LastCommit` DomainCommit +- `Validators`: DomainVal +- `NextValidators`: DomainVal +- `Data`: DomainTX +- `AppState`: DomainApp +- `LastResults`: DomainRes + +#### **[TMBC-SEQ.1]** + +The Tendermint blockchain is a list *chain* of headers. + +#### **[TMBC-VALIDATOR-PAIR.1]** + +Given a full node, a +*validator pair* is a pair *(peerID, voting_power)*, where + +- *peerID* is the PeerID (public key) of a full node, +- *voting_power* is an integer (representing the full node's + voting power in a certain consensus instance). + +> In the Golang implementation the data type for *validator +pair* is called `Validator` + +#### **[TMBC-VALIDATOR-SET.1]** + +A *validator set* is a set of validator pairs. For a validator set +*vs*, we write *TotalVotingPower(vs)* for the sum of the voting powers +of its validator pairs. + +#### **[TMBC-VOTE.1]** + +A *vote* contains a `prevote` or `precommit` message sent and signed by +a validator node during the execution of [consensus][arXiv]. Each +message contains the following fields + +- `Type`: prevote or precommit +- `Height`: positive integer +- `Round` a positive integer +- `BlockID` a Hashvalue of a block (not necessarily a block of the chain) + +#### **[TMBC-COMMIT.1]** + +A commit is a set of `precommit` message. + +## Tendermint Failure Model + +#### **[TMBC-AUTH-BYZ.1]** + +We assume the authenticated Byzantine fault model in which no node (faulty or +correct) may break digital signatures, but otherwise, no additional +assumption is made about the internal behavior of faulty +nodes. That is, faulty nodes are only limited in that they cannot forge +messages. + +#### **[TMBC-TIME-PARAMS.1]** + +A Tendermint blockchain has the following configuration parameters: + +- *unbondingPeriod*: a time duration. +- *trustingPeriod*: a time duration smaller than *unbondingPeriod*. + +#### **[TMBC-CORRECT.1]** + +We define a predicate *correctUntil(n, t)*, where *n* is a node and *t* is a +time point. +The predicate *correctUntil(n, t)* is true if and only if the node *n* +follows all the protocols (at least) until time *t*. + +#### **[TMBC-FM-2THIRDS.1]** + +If a block *h* is in the chain, +then there exists a subset *CorrV* +of *h.NextValidators*, such that: + +- *TotalVotingPower(CorrV) > 2/3 + TotalVotingPower(h.NextValidators)*; cf. [TMBC-VALIDATOR-SET.1] +- For every validator pair *(n,p)* in *CorrV*, it holds *correctUntil(n, + h.Time + trustingPeriod)*; cf. [TMBC-CORRECT.1] + +> The definition of correct +> [**[TMBC-CORRECT.1]**][TMBC-CORRECT-link] refers to realtime, while it +> is used here with *Time* and *trustingPeriod*, which are "hardware +> times". We do not make a distinction here. + +#### **[TMBC-CORR-FULL.1]** + +Every correct full node locally stores a prefix of the +current list of headers from [**[TMBC-SEQ.1]**][TMBC-SEQ-link]. + +## What the Light Client Checks + +> From [TMBC-FM-2THIRDS.1] we directly derive the following observation: + +#### **[TMBC-VAL-CONTAINS-CORR.1]** + +Given a (trusted) block *tb* of the blockchain, a given set of full nodes +*N* contains a correct node at a real-time *t*, if + +- *t - trustingPeriod < tb.Time < t* +- the voting power in tb.NextValidators of nodes in *N* is more + than 1/3 of *TotalVotingPower(tb.NextValidators)* + +> The following describes how a commit for a given block *b* must look +> like. + +#### **[TMBC-SOUND-DISTR-POSS-COMMIT.1]** + +For a block *b*, each element *pc* of *PossibleCommit(b)* satisfies: + +- *pc* contains only votes (cf. [TMBC-VOTE.1]) + by validators from *b.Validators* +- the sum of the voting powers in *pc* is greater than 2/3 + *TotalVotingPower(b.Validators)* +- and there is an *r* such that each vote *v* in *pc* satisfies + - v.Type = precommit + - v.Height = b.Height + - v.Round = r + - v.blockID = hash(b) + +> The following property comes from the validity of the [consensus][arXiv]: A +> correct validator node only sends `prevote` or `precommit`, if +> `BlockID` of the new (to-be-decided) block is equal to the hash of +> the last block. + +#### **[TMBC-VAL-COMMIT.1]** + +If for a block *b*, a commit *c* + +- contains at least one validator pair *(v,p)* such that *v* is a + **correct** validator node, and +- is contained in *PossibleCommit(b)* + +then the block *b* is on the blockchain. + +## Context of this document + +In this document we specify the light client verification component, +called *Core Verification*. The *Core Verification* communicates with +a full node. As full nodes may be faulty, it cannot trust the +received information, but the light client has to check whether the +header it receives coincides with the one generated by Tendermint +consensus. + +The two + properties [[TMBC-VAL-CONTAINS-CORR.1]][TMBC-VAL-CONTAINS-CORR-link] and +[[TMBC-VAL-COMMIT]][TMBC-VAL-COMMIT-link] formalize the checks done + by this specification: +Given a trusted block *tb* and an untrusted block *ub* with a commit *cub*, +one has to check that *cub* is in *PossibleCommit(ub)*, and that *cub* +contains a correct node using *tb*. + +# Part II - Sequential Definition of the Verification Problem + +## Verification Informal Problem statement + +Given a height *targetHeight* as an input, the *Verifier* eventually +stores a header *h* of height *targetHeight* locally. This header *h* +is generated by the Tendermint [blockchain][block]. In +particular, a header that was not generated by the blockchain should +never be stored. + +## Sequential Problem statement + +#### **[LCV-SEQ-LIVE.1]** + +The *Verifier* gets as input a height *targetHeight*, and eventually stores the +header of height *targetHeight* of the blockchain. + +#### **[LCV-SEQ-SAFE.1]** + +The *Verifier* never stores a header which is not in the blockchain. + +# Part III - Light Client as Distributed System + +## Incentives + +Faulty full nodes may benefit from lying to the light client, by making the +light client accept a block that deviates (e.g., contains additional +transactions) from the one generated by Tendermint consensus. +Users using the light client might be harmed by accepting a forged header. + +The [attack detector][attack-detector] of the light client may help the +correct full nodes to understand whether their header is a good one. +Hence, in combination with the light client detector, the correct full +nodes have the incentive to respond. We can thus base liveness +arguments on the assumption that correct full nodes reliably talk to +the light client. + +## Computational Model + +#### **[LCV-A-PEER.1]** + +The verifier communicates with a full node called *primary*. No assumption is made about the full node (it may be correct or faulty). + +#### **[LCV-A-COMM.1]** + +Communication between the light client and a correct full node is +reliable and bounded in time. Reliable communication means that +messages are not lost, not duplicated, and eventually delivered. There +is a (known) end-to-end delay *Delta*, such that if a message is sent +at time *t* then it is received and processes by time *t + Delta*. +This implies that we need a timeout of at least *2 Delta* for remote +procedure calls to ensure that the response of a correct peer arrives +before the timeout expires. + +#### **[LCV-A-TFM.1]** + +The Tendermint blockchain satisfies the Tendermint failure model [**[TMBC-FM-2THIRDS.1]**][TMBC-FM-2THIRDS-link]. + +#### **[LCV-A-VAL.1]** + +The system satisfies [**[TMBC-AUTH-BYZ.1]**][TMBC-Auth-Byz-link] and +[**[TMBC-FM-2THIRDS.1]**][TMBC-FM-2THIRDS-link]. Thus, there is a +blockchain that satisfies the soundness requirements (that is, the +validation rules in [[block]]). + +## Distributed Problem Statement + +### Two Kinds of Termination + +We do not assume that *primary* is correct. Under this assumption no +protocol can guarantee the combination of the sequential +properties. Thus, in the (unreliable) distributed setting, we consider +two kinds of termination (successful and failure) and we will specify +below under what (favorable) conditions *Core Verification* ensures to +terminate successfully, and satisfy the requirements of the sequential +problem statement: + +#### **[LCV-DIST-TERM.1]** + +*Core Verification* either *terminates +successfully* or it *terminates with failure*. + +### Design choices + +#### **[LCV-DIST-STORE.2]** + +*Core Verification* returns a data structure called *LightStore* that +contains light blocks (that contain a header). + +#### **[LCV-DIST-INIT.2]** + +*Core Verification* is called with + +- *primary*: the PeerID of a full node (with verification communicates) +- *root*: a light block (the root of trust) +- *targetHeight*: a height (the height of a header that should be obtained) + +### Temporal Properties + +#### **[LCV-DIST-SAFE.2]** + +It is always the case that every header in *LightStore* was +generated by an instance of Tendermint consensus. + +#### **[LCV-DIST-LIVE.2]** + +If a new instance of *Core Verification* is called with a +height *targetHeight* greater than root.Header.Height it must +must eventually terminate. + +- If + - the *primary* is correct (and locally has the block of + *targetHeight*), and + - the age of root is always less than the trusting period, + then *Core Verification* adds a verified header *hd* with height + *targetHeight* to *LightStore* and it **terminates successfully** + +> These definitions imply that if the primary is faulty, a header may or +> may not be added to *LightStore*. In any case, +> [**[LCV-DIST-SAFE.2]**](#lcv-dist-safe2) must hold. +> The invariant [**[LCV-DIST-SAFE.2]**](#lcv-dist-safe2) and the liveness +> requirement [**[LCV-DIST-LIVE.2]**](#lcv-dist-life) +> allow that verified headers are added to *LightStore* whose +> height was not passed +> to the verifier (e.g., intermediate headers used in bisection; see below). +> Note that for liveness, initially having a *root* within +> the *trustinPeriod* is not sufficient. However, as this +> specification will leave some freedom with respect to the strategy +> in which order to download intermediate headers, we do not give a +> more precise liveness specification here. After giving the +> specification of the protocol, we will discuss some liveness +> scenarios [below](#liveness-scenarios). + +### Solving the sequential specification + +This specification provides a partial solution to the sequential specification. +The *Verifier* solves the invariant of the sequential part + +[**[LCV-DIST-SAFE.2]**](#lcv-dist-safe2) => [**[LCV-SEQ-SAFE.1]**](#lcv-seq-safe1) + +In the case the primary is correct, and *root* is a recent header in *LightStore*, the verifier satisfies the liveness requirements. + +⋀ *primary is correct* +⋀ *root.header.Time* > *now* - *trustingPeriod* +⋀ [**[LCV-A-Comm.1]**](#lcv-a-comm) ⋀ ( + ( [**[TMBC-CorrFull.1]**][TMBC-CorrFull-link] ⋀ + [**[LCV-DIST-LIVE.2]**](#lcv-dist-live2) ) + ⟹ [**[LCV-SEQ-LIVE.1]**](#lcv-seq-live1) +) + +# Part IV - Light Client Verification Protocol + +We provide a specification for Light Client Verification. The local +code for verification is presented by a sequential function +`VerifyToTarget` to highlight the control flow of this functionality. +We note that if a different concurrency model is considered for +an implementation, the sequential flow of the function may be +implemented with mutexes, etc. However, the light client verification +is partitioned into three blocks that can be implemented and tested +independently: + +- `FetchLightBlock` is called to download a light block (header) of a + given height from a peer. +- `ValidAndVerified` is a local code that checks the header. +- `Schedule` decides which height to try to verify next. We keep this + underspecified as different implementations (currently in Goland and + Rust) may implement different optimizations here. We just provide + necessary conditions on how the height may evolve. + + + + +## Definitions + +### Data Types + +The core data structure of the protocol is the LightBlock. + +#### **[LCV-DATA-LIGHTBLOCK.1]** + +```go +type LightBlock struct { + Header Header + Commit Commit + Validators ValidatorSet +} +``` + +#### **[LCV-DATA-LIGHTSTORE.2]** + +LightBlocks are stored in a structure which stores all LightBlock from +initialization or received from peers. + +```go +type LightStore struct { + ... +} + +``` + +#### **[LCV-DATA-LS-ROOT.2]** + +For each lightblock in a lightstore we record in a field `verification-root` of +type Height. + +> `verification-root` records the height of a lightblock that can be used to verify +> the lightblock in one step + +#### **[LCV-INV-LS-ROOT.2]** + +At all times, if a lightblock *b* in a lightstore has *b.verification-root = h*, +then + +- the lightstore contains a lightblock with height *h*, or +- *b* has the minimal height of all lightblocks in lightstore, then + b.verification-root should be nil. + +The LightStore exposes the following functions to query stored LightBlocks. + +#### **[LCV-DATA-LS-STATE.1]** + +Each LightBlock is in one of the following states: + +```go +type VerifiedState int + +const ( + StateUnverified = iota + 1 + StateVerified + StateFailed + StateTrusted +) +``` + +#### **[LCV-FUNC-GET.1]** + +```go +func (ls LightStore) Get(height Height) (LightBlock, bool) +``` + +- Expected postcondition + - returns a LightBlock at a given height or false in the second argument if + the LightStore does not contain the specified LightBlock. + +#### **[LCV-FUNC-LATEST.1]** + +```go +func (ls LightStore) Latest() LightBlock +``` + +- Expected postcondition + - returns the highest light block + +#### **[LCV-FUNC-ADD.1]** + +```go +func (ls LightStore) Add(newBlock) +``` + +- Expected precondition + - the lightstore is empty +- Expected postcondition + - adds newBlock into light store + +#### **[LCV-FUNC-STORE.1]** + +```go +func (ls LightStore) store_chain(newLS LightStore) +``` + +- Expected postcondition + - adds `newLS` to the lightStore. + +#### **[LCV-FUNC-LATEST-VERIF.2]** + +```go +func (ls LightStore) LatestVerified() LightBlock +``` + +- Expected postcondition + - returns the highest light block whose state is `StateVerified` + +#### **[LCV-FUNC-FILTER.1]** + +```go +func (ls LightStore) FilterVerified() LightStore +``` + +- Expected postcondition + - returns all the lightblocks of the lightstore with state `StateVerified` + +#### **[LCV-FUNC-UPDATE.2]** + +```go +func (ls LightStore) Update(lightBlock LightBlock, verfiedState +VerifiedState, root-height Height) +``` + +- Expected postcondition + - the lightblock is part of the lightstore + - The state of the LightBlock is set to *verifiedState*. + - The verification-root of the LightBlock is set to *root-height* + +```go +func (ls LightStore) TraceTo(lightBlock LightBlock) (LightBlock, LightStore) +``` + +- Expected postcondition + - returns a **trusted** lightblock `root` from the lightstore with a height + less than `lightBlock` + - returns a lightstore that contains lightblocks that constitute a + [verification trace](TODOlinkToDetectorSpecOnceThere) from + `root` to `lightBlock` (including `lightBlock`) + +### Inputs + +- *root*: A light block that is trusted +- *primary*: peerID +- *targetHeight*: the height of the needed header + +### Configuration Parameters + +- *trustThreshold*: a float. Can be used if correctness should not be based on more voting power and 1/3. +- *trustingPeriod*: a time duration [**[TMBC-TIME_PARAMS.1]**][TMBC-TIME_PARAMS-link]. +- *clockDrift*: a time duration. Correction parameter dealing with only approximately synchronized clocks. + +### Variables + +- *nextHeight*: initially *targetHeight* + > *nextHeight* should be thought of the "height of the next header we need + > to download and verify" + +### Assumptions + +#### **[LCV-A-INIT.2]** + +- *root* is from the blockchain + +- *targetHeight > root.Header.Height* + +### Invariants + +#### **[LCV-INV-TP.1]** + +It is always the case that *LightStore.LatestTrusted.Header.Time > now - trustingPeriod*. + +> If the invariant is violated, the light client does not have a +> header it can trust. A trusted header must be obtained externally, +> its trust can only be based on social consensus. +> We use the convention that root is assumed to be verified. + +### Used Remote Functions + +We use the functions `commit` and `validators` that are provided +by the [RPC client for Tendermint][RPC]. + +```go +func Commit(height int64) (SignedHeader, error) +``` + +- Implementation remark + - RPC to full node *n* + - JSON sent: + +```javascript +// POST /commit +{ + "jsonrpc": "2.0", + "id": "ccc84631-dfdb-4adc-b88c-5291ea3c2cfb", // UUID v4, unique per request + "method": "commit", + "params": { + "height": 1234 + } +} +``` + +- Expected precondition + - header of `height` exists on blockchain +- Expected postcondition + - if *n* is correct: Returns the signed header of height `height` + from the blockchain if communication is timely (no timeout) + - if *n* is faulty: Returns a signed header with arbitrary content +- Error condition + - if *n* is correct: precondition violated or timeout + - if *n* is faulty: arbitrary error + +----; + +```go +func Validators(height int64) (ValidatorSet, error) +``` + +- Implementation remark + - RPC to full node *n* + - JSON sent: + +```javascript +// POST /validators +{ + "jsonrpc": "2.0", + "id": "ccc84631-dfdb-4adc-b88c-5291ea3c2cfb", // UUID v4, unique per request + "method": "validators", + "params": { + "height": 1234 + } +} +``` + +- Expected precondition + - header of `height` exists on blockchain +- Expected postcondition + - if *n* is correct: Returns the validator set of height `height` + from the blockchain if communication is timely (no timeout) + - if *n* is faulty: Returns arbitrary validator set +- Error condition + - if *n* is correct: precondition violated or timeout + - if *n* is faulty: arbitrary error + +----; + +### Communicating Function + +#### **[LCV-FUNC-FETCH.1]** + + ```go +func FetchLightBlock(peer PeerID, height Height) LightBlock +``` + +- Implementation remark + - RPC to peer at *PeerID* + - calls `Commit` for *height* and `Validators` for *height* and *height+1* +- Expected precondition + - `height` is less than or equal to height of the peer **[LCV-IO-PRE-HEIGHT.1]** +- Expected postcondition: + - if *node* is correct: + - Returns the LightBlock *lb* of height `height` + that is consistent with the blockchain + - *lb.provider = peer* **[LCV-IO-POST-PROVIDER.1]** + - *lb.Header* is a header consistent with the blockchain + - *lb.Validators* is the validator set of the blockchain at height *nextHeight* + - *lb.NextValidators* is the validator set of the blockchain at height *nextHeight + 1* + - if *node* is faulty: Returns a LightBlock with arbitrary content + [**[TMBC-AUTH-BYZ.1]**][TMBC-Auth-Byz-link] +- Error condition + - if *n* is correct: precondition violated + - if *n* is faulty: arbitrary error + - if *lb.provider != peer* + - times out after 2 Delta (by assumption *n* is faulty) + +----; + +## Core Verification + +### Outline + +The `VerifyToTarget` is the main function and uses the following functions. + +- `FetchLightBlock` is called to download the next light block. It is + the only function that communicates with other nodes +- `ValidAndVerified` checks whether header is valid and checks if a + new lightBlock should be trusted + based on a previously verified lightBlock. +- `Schedule` decides which height to try to verify next + +In the following description of `VerifyToTarget` we do not deal with error +handling. If any of the above function returns an error, VerifyToTarget just +passes the error on. + +#### **[LCV-FUNC-MAIN.2]** + +```go +func VerifyToTarget(primary PeerID, root LightBlock, + targetHeight Height) (LightStore, Result) { + + lightStore = new LightStore; + lightStore.Update(root, StateVerified, root.verifiedBy); + nextHeight := targetHeight; + + for lightStore.LatestVerified.height < targetHeight { + + // Get next LightBlock for verification + current, found := lightStore.Get(nextHeight) + if !found { + current = FetchLightBlock(primary, nextHeight) + lightStore.Update(current, StateUnverified, nil) + } + + // Verify + verdict = ValidAndVerified(lightStore.LatestVerified, current) + + // Decide whether/how to continue + if verdict == SUCCESS { + lightStore.Update(current, StateVerified, lightStore.LatestVerified.Height) + } + else if verdict == NOT_ENOUGH_TRUST { + // do nothing + // the light block current passed validation, but the validator + // set is too different to verify it. We keep the state of + // current at StateUnverified. For a later iteration, Schedule + // might decide to try verification of that light block again. + } + else { + // verdict is some error code + lightStore.Update(current, StateFailed, nil) + return (nil, ResultFailure) + } + nextHeight = Schedule(lightStore, nextHeight, targetHeight) + } + return (lightStore.FilterVerified, ResultSuccess) +} +``` + +- Expected precondition + - *root* is within the *trustingPeriod* **[LCV-PRE-TP.1]** + - *targetHeight* is greater than the height of *root* +- Expected postcondition: + - returns *lightStore* that contains a LightBlock that corresponds to a block + of the blockchain of height *targetHeight* + (that is, the LightBlock has been added to *lightStore*) **[LCV-POST-LS.1]** +- Error conditions + - if the precondition is violated + - if `ValidAndVerified` or `FetchLightBlock` report an error + - if [**[LCV-INV-TP.1]**](#LCV-INV-TP.1) is violated + +### Details of the Functions + +#### **[LCV-FUNC-VALID.2]** + +```go +func ValidAndVerified(trusted LightBlock, untrusted LightBlock) Result +``` + +- Expected precondition: + - *untrusted* is valid, that is, satisfies the soundness [checks][block] + - *untrusted* is **well-formed**, that is, + - *untrusted.Header.Time < now + clockDrift* + - *untrusted.Validators = hash(untrusted.Header.Validators)* + - *untrusted.NextValidators = hash(untrusted.Header.NextValidators)* + - *trusted.Header.Time > now - trustingPeriod* + - the `Height` and `Time` of `trusted` are smaller than the Height and + `Time` of `untrusted`, respectively + - the *untrusted.Header* is well-formed (passes the tests from + [[block]]), and in particular + - if the untrusted header `unstrusted.Header` is the immediate + successor of `trusted.Header`, then it holds that + - *trusted.Header.NextValidators = + untrusted.Header.Validators*, and + moreover, + - *untrusted.Header.Commit* + - contains signatures by more than two-thirds of the validators + - contains no signature from nodes that are not in *trusted.Header.NextValidators* +- Expected postcondition: + - Returns `SUCCESS`: + - if *untrusted* is the immediate successor of *trusted*, or otherwise, + - if the signatures of a set of validators that have more than + *max(1/3,trustThreshold)* of voting power in + *trusted.Nextvalidators* is contained in + *untrusted.Commit* (that is, header passes the tests + [**[TMBC-VAL-CONTAINS-CORR.1]**][TMBC-VAL-CONTAINS-CORR-link] + and [**[TMBC-VAL-COMMIT.1]**][TMBC-VAL-COMMIT-link]) + - Returns `NOT_ENOUGH_TRUST` if: + - *untrusted* is *not* the immediate successor of + *trusted* + and the *max(1/3,trustThreshold)* threshold is not reached + (that is, if + [**[TMBC-VAL-CONTAINS-CORR.1]**][TMBC-VAL-CONTAINS-CORR-link] + fails and header is does not violate the soundness + checks [[block]]). +- Error condition: + - if precondition violated + +----; + +#### **[LCV-FUNC-SCHEDULE.1]** + +```go +func Schedule(lightStore, nextHeight, targetHeight) Height +``` + +- Implementation remark: If picks the next height to be verified. + We keep the precise choice of the next header under-specified. It is + subject to performance optimizations that do not influence the correctness +- Expected postcondition: **[LCV-SCHEDULE-POST.1]** + Return *H* s.t. + 1. if *lightStore.LatestVerified.Height = nextHeight* and + *lightStore.LatestVerified < targetHeight* then + *nextHeight < H <= targetHeight* + 2. if *lightStore.LatestVerified.Height < nextHeight* and + *lightStore.LatestVerified.Height < targetHeight* then + *lightStore.LatestVerified.Height < H < nextHeight* + 3. if *lightStore.LatestVerified.Height = targetHeight* then + *H = targetHeight* + +> Case i. captures the case where the light block at height *nextHeight* +> has been verified, and we can choose a height closer to the *targetHeight*. +> As we get the *lightStore* as parameter, the choice of the next height can +> depend on the *lightStore*, e.g., we can pick a height for which we have +> already downloaded a light block. +> In Case ii. the header of *nextHeight* could not be verified, and we need to pick a smaller height. +> In Case iii. is a special case when we have verified the *targetHeight*. + +### Solving the distributed specification + +Analogous to [[001_published]](./verification_001_published.md#solving-the-distributed-specification) + +## Liveness Scenarios + +Analogous to [[001_published]](./verification_001_published.md#liveness-scenarios) + +# Part V - Supporting the IBC Relayer + +The above specification focuses on the most common case, which also +constitutes the most challenging task: using the Tendermint [security +model][TMBC-FM-2THIRDS-link] to verify light blocks without +downloading all intermediate blocks. To focus on this challenge, above +we have restricted ourselves to the case where *targetHeight* is +greater than the height of any trusted header. This simplified +presentation of the algorithm as initially +`lightStore.LatestVerified()` is less than *targetHeight*, and in the +process of verification `lightStore.LatestVerified()` increases until +*targetHeight* is reached. + +For [IBC][ibc-rs] there are two additional challenges: + +1. it might be that some "older" header is needed, that is, +*targetHeight < lightStore.LatestVerified()*. The +[supervisor](../supervisor/supervisor.md) checks whether it is in this +case by calling `LatestPrevious` and `MinVerified` and if so it calls +`Backwards`. All these functions are specified below. + +2. In order to submit proof of a light client attack, a relayer may + need to submit a verification trace. This it is important to + compute such a trace efficiently. That it can be done is based on + the invariant [[LCV-INV-LS-ROOT.2]](#LCV-INV-LS-ROOT2) that needs + to be maintained by the light client. In particular + `VerifyToTarget` and `Backwards` need to take care of setting + `verification-root`. + +#### **[LCV-FUNC-LATEST-PREV.2]** + +```go +func (ls LightStore) LatestPrevious(height Height) (LightBlock, bool) +``` + +- Expected postcondition + - returns a light block *lb* that satisfies: + - *lb* is in lightStore + - *lb* is in StateTrusted + - *lb* is not expired + - *lb.Header.Height < height* + - for all *b* in lightStore s.t. *b* is trusted and not expired it + holds *lb.Header.Height >= b.Header.Height* + - *false* in the second argument if + the LightStore does not contain such an *lb*. + +----; + +#### **[LCV-FUNC-LOWEST.2]** + +```go +func (ls LightStore) Lowest() (LightBlock) +``` + +- Expected postcondition + - returns the lowest trusted light block within trusting period + +----; + +#### **[LCV-FUNC-MIN.2]** + +```go +func (ls LightStore) MinVerified() (LightBlock, bool) +``` + +- Expected postcondition + - returns a light block *lb* that satisfies: + - *lb* is in lightStore + - *lb.Header.Height* is minimal in the lightStore + - *false* in the second argument if + the LightStore does not contain such an *lb*. + +If a height that is smaller than the smallest height in the lightstore +is required, we check the hashes backwards. This is done with the +following function: + +#### **[LCV-FUNC-BACKWARDS.2]** + +```go +func Backwards (primary PeerID, root LightBlock, targetHeight Height) + (LightStore, Result) { + + lb := root; + lightStore := new LightStore; + lightStore.Update(lb, StateTrusted, lb.verifiedBy) + + latest := lb.Header + for i := lb.Header.height - 1; i >= targetHeight; i-- { + // here we download height-by-height. We might first download all + // headers down to targetHeight and then check them. + current := FetchLightBlock(primary,i) + if (hash(current) != latest.Header.LastBlockId) { + return (nil, ResultFailure) + } + else { + // latest and current are linked together by LastBlockId + // therefore it is not relevant which we verified first + // for consistency, we store latest was veried using + // current so that the verifiedBy is always pointing down + // the chain + lightStore.Update(current, StateTrusted, nil) + lightStore.Update(latest, StateTrusted, current.Header.Height) + } + latest = current + } + return (lightStore, ResultSuccess) +} +``` + +# References + +[[block]] Specification of the block data structure. + +[[RPC]] RPC client for Tendermint + +[[attack-detector]] The specification of the light client attack detector. + +[[fullnode]] Specification of the full node API + +[[ibc-rs]] Rust implementation of IBC modules and relayer. + +[[lightclient]] The light client ADR [77d2651 on Dec 27, 2019]. + +[RPC]: https://docs.tendermint.com/master/rpc/ + +[block]: https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md + +[TMBC-HEADER-link]: #tmbc-header1 +[TMBC-SEQ-link]: #tmbc-seq1 +[TMBC-CorrFull-link]: #tmbc-corr-full1 +[TMBC-Auth-Byz-link]: #tmbc-auth-byz1 +[TMBC-TIME_PARAMS-link]: #tmbc-time-params1 +[TMBC-FM-2THIRDS-link]: #tmbc-fm-2thirds1 +[TMBC-VAL-CONTAINS-CORR-link]: #tmbc-val-contains-corr1 +[TMBC-VAL-COMMIT-link]: #tmbc-val-commit1 +[TMBC-SOUND-DISTR-POSS-COMMIT-link]: #tmbc-sound-distr-poss-commit1 + +[lightclient]: https://github.com/interchainio/tendermint-rs/blob/e2cb9aca0b95430fca2eac154edddc9588038982/docs/architecture/adr-002-lite-client.md +[attack-detector]: https://github.com/tendermint/tendermint/blob/v0.35.x/rust-spec/lightclient/detection/detection_001_reviewed.md +[fullnode]: https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/fullnode.md + +[ibc-rs]:https://github.com/informalsystems/ibc-rs + +[blockchain-validator-set]: https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/blockchain.md#data-structures +[fullnode-data-structures]: https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/fullnode.md#data-structures + +[FN-ManifestFaulty-link]: https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/fullnode.md#fn-manifestfaulty + +[arXiv]: https://arxiv.org/abs/1807.04938 diff --git a/spec/light-client/verification/verification_003_draft.md b/spec/light-client/verification/verification_003_draft.md new file mode 100644 index 000000000..cd38e7e96 --- /dev/null +++ b/spec/light-client/verification/verification_003_draft.md @@ -0,0 +1,76 @@ +# Light Client Verificaiton + +#### **[LCV-FUNC-VERIFYCOMMITLIGHT.1]** + +VerifyCommitLight verifies that 2/3+ of the signatures for a validator set were for +a given blockID. The function will finish early and thus may not check all signatures. + +```go +func VerifyCommitLight(chainID string, vals *ValidatorSet, blockID BlockID, +height int64, commit *Commit) error { + // run a basic validation of the arguments + if err := verifyBasicValsAndCommit(vals, commit, height, blockID); err != nil { + return err + } + + // calculate voting power needed + votingPowerNeeded := vals.TotalVotingPower() * 2 / 3 + + var ( + val *Validator + valIdx int32 + seenVals = make(map[int32]int, len(commit.Signatures)) + talliedVotingPower int64 = 0 + voteSignBytes []byte + ) + for idx, commitSig := range commit.Signatures { + // ignore all commit signatures that are not for the block + if !commitSig.ForBlock() { + continue + } + + // If the vals and commit have a 1-to-1 correspondance we can retrieve + // them by index else we need to retrieve them by address + if lookUpByIndex { + val = vals.Validators[idx] + } else { + valIdx, val = vals.GetByAddress(commitSig.ValidatorAddress) + + // if the signature doesn't belong to anyone in the validator set + // then we just skip over it + if val == nil { + continue + } + + // because we are getting validators by address we need to make sure + // that the same validator doesn't commit twice + if firstIndex, ok := seenVals[valIdx]; ok { + secondIndex := idx + return fmt.Errorf("double vote from %v (%d and %d)", val, firstIndex, secondIndex) + } + seenVals[valIdx] = idx + } + + voteSignBytes = commit.VoteSignBytes(chainID, int32(idx)) + + if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) { + return fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature) + } + + // Add the voting power of the validator + // to the tally + talliedVotingPower += val.VotingPower + + // check if we have enough signatures and can thus exit early + if talliedVotingPower > votingPowerNeeded { + return nil + } + } + + if got, needed := talliedVotingPower, votingPowerNeeded; got <= needed { + return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed} + } + + return nil +} +``` \ No newline at end of file diff --git a/spec/p2p/config.md b/spec/p2p/config.md new file mode 100644 index 000000000..b63c04f28 --- /dev/null +++ b/spec/p2p/config.md @@ -0,0 +1,49 @@ +# P2P Config + +Here we describe configuration options around the Peer Exchange. +These can be set using flags or via the `$TMHOME/config/config.toml` file. + +## Seed Mode + +`--p2p.seed_mode` + +The node operates in seed mode. In seed mode, a node continuously crawls the network for peers, +and upon incoming connection shares some peers and disconnects. + +## Seeds + +`--p2p.seeds “id100000000000000000000000000000000@1.2.3.4:26656,id200000000000000000000000000000000@2.3.4.5:4444”` + +Dials these seeds when we need more peers. They should return a list of peers and then disconnect. +If we already have enough peers in the address book, we may never need to dial them. + +## Persistent Peers + +`--p2p.persistent_peers “id100000000000000000000000000000000@1.2.3.4:26656,id200000000000000000000000000000000@2.3.4.5:26656”` + +Dial these peers and auto-redial them if the connection fails. +These are intended to be trusted persistent peers that can help +anchor us in the p2p network. The auto-redial uses exponential +backoff and will give up after a day of trying to connect. + +But If `persistent_peers_max_dial_period` is set greater than zero, +pause between each dial to each persistent peer will not exceed `persistent_peers_max_dial_period` +during exponential backoff and we keep trying again without giving up + +**Note:** If `seeds` and `persistent_peers` intersect, +the user will be warned that seeds may auto-close connections +and that the node may not be able to keep the connection persistent. + +## Private Peers + +`--p2p.private_peer_ids “id100000000000000000000000000000000,id200000000000000000000000000000000”` + +These are IDs of the peers that we do not add to the address book or gossip to +other peers. They stay private to us. + +## Unconditional Peers + +`--p2p.unconditional_peer_ids “id100000000000000000000000000000000,id200000000000000000000000000000000”` + +These are IDs of the peers which are allowed to be connected by both inbound or outbound regardless of +`max_num_inbound_peers` or `max_num_outbound_peers` of user's node reached or not. diff --git a/spec/p2p/connection.md b/spec/p2p/connection.md new file mode 100644 index 000000000..33178f479 --- /dev/null +++ b/spec/p2p/connection.md @@ -0,0 +1,111 @@ +# P2P Multiplex Connection + +## MConnection + +`MConnection` is a multiplex connection that supports multiple independent streams +with distinct quality of service guarantees atop a single TCP connection. +Each stream is known as a `Channel` and each `Channel` has a globally unique _byte id_. +Each `Channel` also has a relative priority that determines the quality of service +of the `Channel` compared to other `Channel`s. +The _byte id_ and the relative priorities of each `Channel` are configured upon +initialization of the connection. + +The `MConnection` supports three packet types: + +- Ping +- Pong +- Msg + +### Ping and Pong + +The ping and pong messages consist of writing a single byte to the connection; 0x1 and 0x2, respectively. + +When we haven't received any messages on an `MConnection` in time `pingTimeout`, we send a ping message. +When a ping is received on the `MConnection`, a pong is sent in response only if there are no other messages +to send and the peer has not sent us too many pings (TODO). + +If a pong or message is not received in sufficient time after a ping, the peer is disconnected from. + +### Msg + +Messages in channels are chopped into smaller `msgPacket`s for multiplexing. + +```go +type msgPacket struct { + ChannelID byte + EOF byte // 1 means message ends here. + Bytes []byte +} +``` + +The `msgPacket` is serialized using [Proto3](https://developers.google.com/protocol-buffers/docs/proto3). +The received `Bytes` of a sequential set of packets are appended together +until a packet with `EOF=1` is received, then the complete serialized message +is returned for processing by the `onReceive` function of the corresponding channel. + +### Multiplexing + +Messages are sent from a single `sendRoutine`, which loops over a select statement and results in the sending +of a ping, a pong, or a batch of data messages. The batch of data messages may include messages from multiple channels. +Message bytes are queued for sending in their respective channel, with each channel holding one unsent message at a time. +Messages are chosen for a batch one at a time from the channel with the lowest ratio of recently sent bytes to channel priority. + +## Sending Messages + +There are two methods for sending messages: + +```go +func (m MConnection) Send(chID byte, msg interface{}) bool {} +func (m MConnection) TrySend(chID byte, msg interface{}) bool {} +``` + +`Send(chID, msg)` is a blocking call that waits until `msg` is successfully queued +for the channel with the given id byte `chID`. The message `msg` is serialized +using the `tendermint/go-amino` submodule's `WriteBinary()` reflection routine. + +`TrySend(chID, msg)` is a nonblocking call that queues the message msg in the channel +with the given id byte chID if the queue is not full; otherwise it returns false immediately. + +`Send()` and `TrySend()` are also exposed for each `Peer`. + +## Peer + +Each peer has one `MConnection` instance, and includes other information such as whether the connection +was outbound, whether the connection should be recreated if it closes, various identity information about the node, +and other higher level thread-safe data used by the reactors. + +## Switch/Reactor + +The `Switch` handles peer connections and exposes an API to receive incoming messages +on `Reactors`. Each `Reactor` is responsible for handling incoming messages of one +or more `Channels`. So while sending outgoing messages is typically performed on the peer, +incoming messages are received on the reactor. + +```go +// Declare a MyReactor reactor that handles messages on MyChannelID. +type MyReactor struct{} + +func (reactor MyReactor) GetChannels() []*ChannelDescriptor { + return []*ChannelDescriptor{ChannelDescriptor{ID:MyChannelID, Priority: 1}} +} + +func (reactor MyReactor) Receive(chID byte, peer *Peer, msgBytes []byte) { + r, n, err := bytes.NewBuffer(msgBytes), new(int64), new(error) + msgString := ReadString(r, n, err) + fmt.Println(msgString) +} + +// Other Reactor methods omitted for brevity +... + +switch := NewSwitch([]Reactor{MyReactor{}}) + +... + +// Send a random message to all outbound connections +for _, peer := range switch.Peers().List() { + if peer.IsOutbound() { + peer.Send(MyChannelID, "Here's a random message") + } +} +``` diff --git a/spec/p2p/messages/README.md b/spec/p2p/messages/README.md new file mode 100644 index 000000000..1b5f5c60d --- /dev/null +++ b/spec/p2p/messages/README.md @@ -0,0 +1,19 @@ +--- +order: 1 +parent: + title: Messages + order: 1 +--- + +# Messages + +An implementation of the spec consists of many components. While many parts of these components are implementation specific, the p2p messages are not. In this section we will be covering all the p2p messages of components. + +There are two parts to the P2P messages, the message and the channel. The channel is message specific and messages are specific to components of Tendermint. When a node connect to a peer it will tell the other node which channels are available. This notifies the peer what services the connecting node offers. You can read more on channels in [connection.md](../connection.md#mconnection) + +- [Block Sync](./block-sync.md) +- [Mempool](./mempool.md) +- [Evidence](./evidence.md) +- [State Sync](./state-sync.md) +- [Pex](./pex.md) +- [Consensus](./consensus.md) diff --git a/spec/p2p/messages/block-sync.md b/spec/p2p/messages/block-sync.md new file mode 100644 index 000000000..48aa6155f --- /dev/null +++ b/spec/p2p/messages/block-sync.md @@ -0,0 +1,68 @@ +--- +order: 2 +--- + +# Block Sync + +## Channel + +Block sync has one channel. + +| Name | Number | +|-------------------|--------| +| BlockchainChannel | 64 | + +## Message Types + +There are multiple message types for Block Sync + +### BlockRequest + +BlockRequest asks a peer for a block at the height specified. + +| Name | Type | Description | Field Number | +|--------|-------|---------------------------|--------------| +| Height | int64 | Height of requested block | 1 | + +### NoBlockResponse + +NoBlockResponse notifies the peer requesting a block that the node does not contain it. + +| Name | Type | Description | Field Number | +|--------|-------|---------------------------|--------------| +| Height | int64 | Height of requested block | 1 | + +### BlockResponse + +BlockResponse contains the block requested. + +| Name | Type | Description | Field Number | +|-------|----------------------------------------------|-----------------|--------------| +| Block | [Block](../../core/data_structures.md#block) | Requested Block | 1 | + +### StatusRequest + +StatusRequest is an empty message that notifies the peer to respond with the highest and lowest blocks it has stored. + +> Empty message. + +### StatusResponse + +StatusResponse responds to a peer with the highest and lowest block stored. + +| Name | Type | Description | Field Number | +|--------|-------|-------------------------------------------------------------------|--------------| +| Height | int64 | Current Height of a node | 1 | +| base | int64 | First known block, if pruning is enabled it will be higher than 1 | 1 | + +### Message + +Message is a [`oneof` protobuf type](https://developers.google.com/protocol-buffers/docs/proto#oneof). The `oneof` consists of five messages. + +| Name | Type | Description | Field Number | +|-------------------|----------------------------------|--------------------------------------------------------------|--------------| +| block_request | [BlockRequest](#blockrequest) | Request a block from a peer | 1 | +| no_block_response | [NoBlockResponse](#noblockresponse) | Response saying it doe snot have the requested block | 2 | +| block_response | [BlockResponse](#blockresponse) | Response with requested block | 3 | +| status_request | [StatusRequest](#statusrequest) | Request the highest and lowest block numbers from a peer | 4 | +| status_response | [StatusResponse](#statusresponse) | Response with the highest and lowest block numbers the store | 5 | diff --git a/spec/p2p/messages/consensus.md b/spec/p2p/messages/consensus.md new file mode 100644 index 000000000..01053a902 --- /dev/null +++ b/spec/p2p/messages/consensus.md @@ -0,0 +1,149 @@ +--- +order: 7 +--- + +# Consensus + +## Channel + +Consensus has four separate channels. The channel identifiers are listed below. + +| Name | Number | +|--------------------|--------| +| StateChannel | 32 | +| DataChannel | 33 | +| VoteChannel | 34 | +| VoteSetBitsChannel | 35 | + +## Message Types + +### Proposal + +Proposal is sent when a new block is proposed. It is a suggestion of what the +next block in the blockchain should be. + +| Name | Type | Description | Field Number | +|----------|----------------------------------------------------|----------------------------------------|--------------| +| proposal | [Proposal](../../core/data_structures.md#proposal) | Proposed Block to come to consensus on | 1 | + +### Vote + +Vote is sent to vote for some block (or to inform others that a process does not vote in the +current round). Vote is defined in the +[Blockchain](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/core/data_structures.md#blockidd) +section and contains validator's +information (validator address and index), height and round for which the vote is sent, vote type, +blockID if process vote for some block (`nil` otherwise) and a timestamp when the vote is sent. The +message is signed by the validator private key. + +| Name | Type | Description | Field Number | +|------|--------------------------------------------|---------------------------|--------------| +| vote | [Vote](../../core/data_structures.md#vote) | Vote for a proposed Block | 1 | + +### BlockPart + +BlockPart is sent when gossiping a piece of the proposed block. It contains height, round +and the block part. + +| Name | Type | Description | Field Number | +|--------|--------------------------------------------|----------------------------------------|--------------| +| height | int64 | Height of corresponding block. | 1 | +| round | int32 | Round of voting to finalize the block. | 2 | +| part | [Part](../../core/data_structures.md#part) | A part of the block. | 3 | + +### NewRoundStep + +NewRoundStep is sent for every step transition during the core consensus algorithm execution. +It is used in the gossip part of the Tendermint protocol to inform peers about a current +height/round/step a process is in. + +| Name | Type | Description | Field Number | +|--------------------------|--------|----------------------------------------|--------------| +| height | int64 | Height of corresponding block | 1 | +| round | int32 | Round of voting to finalize the block. | 2 | +| step | uint32 | | 3 | +| seconds_since_start_time | int64 | | 4 | +| last_commit_round | int32 | | 5 | + +### NewValidBlock + +NewValidBlock is sent when a validator observes a valid block B in some round r, +i.e., there is a Proposal for block B and 2/3+ prevotes for the block B in the round r. +It contains height and round in which valid block is observed, block parts header that describes +the valid block and is used to obtain all +block parts, and a bit array of the block parts a process currently has, so its peers can know what +parts it is missing so they can send them. +In case the block is also committed, then IsCommit flag is set to true. + +| Name | Type | Description | Field Number | +|-----------------------|--------------------------------------------------------------|----------------------------------------|--------------| +| height | int64 | Height of corresponding block | 1 | +| round | int32 | Round of voting to finalize the block. | 2 | +| block_part_set_header | [PartSetHeader](../../core/data_structures.md#partsetheader) | | 3 | +| block_parts | int32 | | 4 | +| is_commit | bool | | 5 | + +### ProposalPOL + +ProposalPOL is sent when a previous block is re-proposed. +It is used to inform peers in what round the process learned for this block (ProposalPOLRound), +and what prevotes for the re-proposed block the process has. + +| Name | Type | Description | Field Number | +|--------------------|----------|-------------------------------|--------------| +| height | int64 | Height of corresponding block | 1 | +| proposal_pol_round | int32 | | 2 | +| proposal_pol | bitarray | | 3 | + +### ReceivedVote + +ReceivedVote is sent to indicate that a particular vote has been received. It contains height, +round, vote type and the index of the validator that is the originator of the corresponding vote. + +| Name | Type | Description | Field Number | +|--------|------------------------------------------------------------------|----------------------------------------|--------------| +| height | int64 | Height of corresponding block | 1 | +| round | int32 | Round of voting to finalize the block. | 2 | +| type | [SignedMessageType](../../core/data_structures.md#signedmsgtype) | | 3 | +| index | int32 | | 4 | + +### VoteSetMaj23 + +VoteSetMaj23 is sent to indicate that a process has seen +2/3 votes for some BlockID. +It contains height, round, vote type and the BlockID. + +| Name | Type | Description | Field Number | +|--------|------------------------------------------------------------------|----------------------------------------|--------------| +| height | int64 | Height of corresponding block | 1 | +| round | int32 | Round of voting to finalize the block. | 2 | +| type | [SignedMessageType](../../core/data_structures.md#signedmsgtype) | | 3 | + +### VoteSetBits + +VoteSetBits is sent to communicate the bit-array of votes a process has seen for a given +BlockID. It contains height, round, vote type, BlockID and a bit array of +the votes a process has. + +| Name | Type | Description | Field Number | +|----------|------------------------------------------------------------------|----------------------------------------|--------------| +| height | int64 | Height of corresponding block | 1 | +| round | int32 | Round of voting to finalize the block. | 2 | +| type | [SignedMessageType](../../core/data_structures.md#signedmsgtype) | | 3 | +| block_id | [BlockID](../../core/data_structures.md#blockid) | | 4 | +| votes | BitArray | Round of voting to finalize the block. | 5 | + +### Message + +Message is a [`oneof` protobuf type](https://developers.google.com/protocol-buffers/docs/proto#oneof). + +| Name | Type | Description | Field Number | +|-----------------|---------------------------------|----------------------------------------|--------------| +| new_round_step | [NewRoundStep](#newroundstep) | Height of corresponding block | 1 | +| new_valid_block | [NewValidBlock](#newvalidblock) | Round of voting to finalize the block. | 2 | +| proposal | [Proposal](#proposal) | | 3 | +| proposal_pol | [ProposalPOL](#proposalpol) | | 4 | +| block_part | [BlockPart](#blockpart) | | 5 | +| vote | [Vote](#vote) | | 6 | +| received_vote | [ReceivedVote](#ReceivedVote) | | 7 | +| vote_set_maj23 | [VoteSetMaj23](#votesetmaj23) | | 8 | +| vote_set_bits | [VoteSetBits](#votesetbits) | | 9 | diff --git a/spec/p2p/messages/evidence.md b/spec/p2p/messages/evidence.md new file mode 100644 index 000000000..34fc40a91 --- /dev/null +++ b/spec/p2p/messages/evidence.md @@ -0,0 +1,23 @@ +--- +order: 3 +--- + +# Evidence + +## Channel + +Evidence has one channel. The channel identifier is listed below. + +| Name | Number | +|-----------------|--------| +| EvidenceChannel | 56 | + +## Message Types + +### EvidenceList + +EvidenceList consists of a list of verified evidence. This evidence will already have been propagated throughout the network. EvidenceList is used in two places, as a p2p message and within the block [block](../../core/data_structures.md#block) as well. + +| Name | Type | Description | Field Number | +|----------|-------------------------------------------------------------|------------------------|--------------| +| evidence | repeated [Evidence](../../core/data_structures.md#evidence) | List of valid evidence | 1 | diff --git a/spec/p2p/messages/mempool.md b/spec/p2p/messages/mempool.md new file mode 100644 index 000000000..8f3925cad --- /dev/null +++ b/spec/p2p/messages/mempool.md @@ -0,0 +1,33 @@ +--- +order: 4 +--- +# Mempool + +## Channel + +Mempool has one channel. The channel identifier is listed below. + +| Name | Number | +|----------------|--------| +| MempoolChannel | 48 | + +## Message Types + +There is currently only one message that Mempool broadcasts and receives over +the p2p gossip network (via the reactor): `TxsMessage` + +### Txs + +A list of transactions. These transactions have been checked against the application for validity. This does not mean that the transactions are valid, it is up to the application to check this. + +| Name | Type | Description | Field Number | +|------|----------------|----------------------|--------------| +| txs | repeated bytes | List of transactions | 1 | + +### Message + +Message is a [`oneof` protobuf type](https://developers.google.com/protocol-buffers/docs/proto#oneof). The one of consists of one message [`Txs`](#txs). + +| Name | Type | Description | Field Number | +|------|-------------|-----------------------|--------------| +| txs | [Txs](#txs) | List of transactions | 1 | diff --git a/spec/p2p/messages/pex.md b/spec/p2p/messages/pex.md new file mode 100644 index 000000000..e12a076e5 --- /dev/null +++ b/spec/p2p/messages/pex.md @@ -0,0 +1,76 @@ +--- +order: 6 +--- + +# Peer Exchange + +## Channels + +Pex has one channel. The channel identifier is listed below. + +| Name | Number | +|------------|--------| +| PexChannel | 0 | + +## Message Types + +The current PEX service has two versions. The first uses IP/port pair but since the p2p stack is moving towards a transport agnostic approach, +node endpoints require a `Protocol` and `Path` hence the V2 version uses a [url](https://golang.org/pkg/net/url/#URL) instead. + +### PexRequest + +PexRequest is an empty message requesting a list of peers. + +> EmptyRequest + +### PexResponse + +PexResponse is an list of net addresses provided to a peer to dial. + +| Name | Type | Description | Field Number | +|-------|------------------------------------|------------------------------------------|--------------| +| addresses | repeated [PexAddress](#PexAddress) | List of peer addresses available to dial | 1 | + +### PexAddress + +PexAddress provides needed information for a node to dial a peer. + +| Name | Type | Description | Field Number | +|------|--------|------------------|--------------| +| id | string | NodeID of a peer | 1 | +| ip | string | The IP of a node | 2 | +| port | port | Port of a peer | 3 | + + +### PexRequestV2 + +PexRequest is an empty message requesting a list of peers. + +> EmptyRequest + +### PexResponseV2 + +PexResponse is an list of net addresses provided to a peer to dial. + +| Name | Type | Description | Field Number | +|-------|------------------------------------|------------------------------------------|--------------| +| addresses | repeated [PexAddressV2](#PexAddressV2) | List of peer addresses available to dial | 1 | + +### PexAddressV2 + +PexAddress provides needed information for a node to dial a peer. + +| Name | Type | Description | Field Number | +|------|--------|------------------|--------------| +| url | string | See [golang url](https://golang.org/pkg/net/url/#URL) | 1 | + +### Message + +Message is a [`oneof` protobuf type](https://developers.google.com/protocol-buffers/docs/proto#oneof). The one of consists of two messages. + +| Name | Type | Description | Field Number | +|--------------|---------------------------|------------------------------------------------------|--------------| +| pex_request | [PexRequest](#PexRequest) | Empty request asking for a list of addresses to dial | 1 | +| pex_response | [PexResponse](#PexResponse) | List of addresses to dial | 2 | +| pex_request_v2 | [PexRequestV2](#PexRequestV2) | Empty request asking for a list of addresses to dial | 3 | +| pex_response_v2 | [PexRespinseV2](#PexResponseV2) | List of addresses to dial | 4 | diff --git a/spec/p2p/messages/state-sync.md b/spec/p2p/messages/state-sync.md new file mode 100644 index 000000000..71e3ae71b --- /dev/null +++ b/spec/p2p/messages/state-sync.md @@ -0,0 +1,133 @@ +--- +order: 5 +--- + +# State Sync + +## Channels + +State sync has four distinct channels. The channel identifiers are listed below. + +| Name | Number | +|-------------------|--------| +| SnapshotChannel | 96 | +| ChunkChannel | 97 | +| LightBlockChannel | 98 | +| ParamsChannel | 99 | + +## Message Types + +### SnapshotRequest + +When a new node begin state syncing, it will ask all peers it encounters if it has any +available snapshots: + +| Name | Type | Description | Field Number | +|----------|--------|-------------|--------------| + +### SnapShotResponse + +The receiver will query the local ABCI application via `ListSnapshots`, and send a message +containing snapshot metadata (limited to 4 MB) for each of the 10 most recent snapshots: and stored at the application layer. When a peer is starting it will request snapshots. + +| Name | Type | Description | Field Number | +|----------|--------|-----------------------------------------------------------|--------------| +| height | uint64 | Height at which the snapshot was taken | 1 | +| format | uint32 | Format of the snapshot. | 2 | +| chunks | uint32 | How many chunks make up the snapshot | 3 | +| hash | bytes | Arbitrary snapshot hash | 4 | +| metadata | bytes | Arbitrary application data. **May be non-deterministic.** | 5 | + +### ChunkRequest + +The node running state sync will offer these snapshots to the local ABCI application via +`OfferSnapshot` ABCI calls, and keep track of which peers contain which snapshots. Once a snapshot +is accepted, the state syncer will request snapshot chunks from appropriate peers: + +| Name | Type | Description | Field Number | +|--------|--------|-------------------------------------------------------------|--------------| +| height | uint64 | Height at which the chunk was created | 1 | +| format | uint32 | Format chosen for the chunk. **May be non-deterministic.** | 2 | +| index | uint32 | Index of the chunk within the snapshot. | 3 | + +### ChunkResponse + +The receiver will load the requested chunk from its local application via `LoadSnapshotChunk`, +and respond with it (limited to 16 MB): + +| Name | Type | Description | Field Number | +|---------|--------|-------------------------------------------------------------|--------------| +| height | uint64 | Height at which the chunk was created | 1 | +| format | uint32 | Format chosen for the chunk. **May be non-deterministic.** | 2 | +| index | uint32 | Index of the chunk within the snapshot. | 3 | +| hash | bytes | Arbitrary snapshot hash | 4 | +| missing | bool | Arbitrary application data. **May be non-deterministic.** | 5 | + +Here, `Missing` is used to signify that the chunk was not found on the peer, since an empty +chunk is a valid (although unlikely) response. + +The returned chunk is given to the ABCI application via `ApplySnapshotChunk` until the snapshot +is restored. If a chunk response is not returned within some time, it will be re-requested, +possibly from a different peer. + +The ABCI application is able to request peer bans and chunk refetching as part of the ABCI protocol. + +### LightBlockRequest + +To verify state and to provide state relevant information for consensus, the node will ask peers for +light blocks at specified heights. + +| Name | Type | Description | Field Number | +|----------|--------|----------------------------|--------------| +| height | uint64 | Height of the light block | 1 | + +### LightBlockResponse + +The receiver will retrieve and construct the light block from both the block and state stores. The +receiver will verify the data by comparing the hashes and store the header, commit and validator set +if necessary. The light block at the height of the snapshot will be used to verify the `AppHash`. + +| Name | Type | Description | Field Number | +|---------------|---------------------------------------------------------|--------------------------------------|--------------| +| light_block | [LightBlock](../../core/data_structures.md#lightblock) | Light block at the height requested | 1 | + +State sync will use [light client verification](../../light-client/verification.README.md) to verify +the light blocks. + + +If no state sync is in progress (i.e. during normal operation), any unsolicited response messages +are discarded. + +### ParamsRequest + +In order to build tendermint state, the state provider will request the params at the height of the snapshot and use the header to verify it. + +| Name | Type | Description | Field Number | +|----------|--------|----------------------------|--------------| +| height | uint64 | Height of the consensus params | 1 | + + +### ParamsResponse + +A reciever to the request will use the state store to fetch the consensus params at that height and return it to the sender. + +| Name | Type | Description | Field Number | +|----------|--------|---------------------------------|--------------| +| height | uint64 | Height of the consensus params | 1 | +| consensus_params | [ConsensusParams](../../core/data_structures.md#ConsensusParams) | Consensus params at the height requested | 2 | + + +### Message + +Message is a [`oneof` protobuf type](https://developers.google.com/protocol-buffers/docs/proto#oneof). The `oneof` consists of eight messages. + +| Name | Type | Description | Field Number | +|----------------------|--------------------------------------------|----------------------------------------------|--------------| +| snapshots_request | [SnapshotRequest](#snapshotrequest) | Request a recent snapshot from a peer | 1 | +| snapshots_response | [SnapshotResponse](#snapshotresponse) | Respond with the most recent snapshot stored | 2 | +| chunk_request | [ChunkRequest](#chunkrequest) | Request chunks of the snapshot. | 3 | +| chunk_response | [ChunkRequest](#chunkresponse) | Response of chunks used to recreate state. | 4 | +| light_block_request | [LightBlockRequest](#lightblockrequest) | Request a light block. | 5 | +| light_block_response | [LightBlockResponse](#lightblockresponse) | Respond with a light block | 6 | +| params_request | [ParamsRequest](#paramsrequest) | Request the consensus params at a height. | 7 | +| params_response | [ParamsResponse](#paramsresponse) | Respond with the consensus params | 8 | diff --git a/spec/p2p/node.md b/spec/p2p/node.md new file mode 100644 index 000000000..6a834c7d6 --- /dev/null +++ b/spec/p2p/node.md @@ -0,0 +1,67 @@ +# Peer Discovery + +A Tendermint P2P network has different kinds of nodes with different requirements for connectivity to one another. +This document describes what kind of nodes Tendermint should enable and how they should work. + +## Seeds + +Seeds are the first point of contact for a new node. +They return a list of known active peers and then disconnect. + +Seeds should operate full nodes with the PEX reactor in a "crawler" mode +that continuously explores to validate the availability of peers. + +Seeds should only respond with some top percentile of the best peers it knows about. +See [the peer-exchange docs](https://github.com/tendermint/tendermint/blob/v0.35/spec/reactors/pex/pex.md) for + details on peer quality. + +## New Full Node + +A new node needs a few things to connect to the network: + +- a list of seeds, which can be provided to Tendermint via config file or flags, + or hardcoded into the software by in-process apps +- a `ChainID`, also called `Network` at the p2p layer +- a recent block height, H, and hash, HASH for the blockchain. + +The values `H` and `HASH` must be received and corroborated by means external to Tendermint, and specific to the user - ie. via the user's trusted social consensus. +This requirement to validate `H` and `HASH` out-of-band and via social consensus +is the essential difference in security models between Proof-of-Work and Proof-of-Stake blockchains. + +With the above, the node then queries some seeds for peers for its chain, +dials those peers, and runs the Tendermint protocols with those it successfully connects to. + +When the peer catches up to height H, it ensures the block hash matches HASH. +If not, Tendermint will exit, and the user must try again - either they are connected +to bad peers or their social consensus is invalid. + +## Restarted Full Node + +A node checks its address book on startup and attempts to connect to peers from there. +If it can't connect to any peers after some time, it falls back to the seeds to find more. + +Restarted full nodes can run the `blockchain` or `consensus` reactor protocols to sync up +to the latest state of the blockchain from wherever they were last. +In a Proof-of-Stake context, if they are sufficiently far behind (greater than the length +of the unbonding period), they will need to validate a recent `H` and `HASH` out-of-band again +so they know they have synced the correct chain. + +## Validator Node + +A validator node is a node that interfaces with a validator signing key. +These nodes require the highest security, and should not accept incoming connections. +They should maintain outgoing connections to a controlled set of "Sentry Nodes" that serve +as their proxy shield to the rest of the network. + +Validators that know and trust each other can accept incoming connections from one another and maintain direct private connectivity via VPN. + +## Sentry Node + +Sentry nodes are guardians of a validator node and provide it access to the rest of the network. +They should be well connected to other full nodes on the network. +Sentry nodes may be dynamic, but should maintain persistent connections to some evolving random subset of each other. +They should always expect to have direct incoming connections from the validator node and its backup(s). +They do not report the validator node's address in the PEX and +they may be more strict about the quality of peers they keep. + +Sentry nodes belonging to validators that trust each other may wish to maintain persistent connections via VPN with one another, but only report each other sparingly in the PEX. diff --git a/spec/p2p/peer.md b/spec/p2p/peer.md new file mode 100644 index 000000000..c0d68cf49 --- /dev/null +++ b/spec/p2p/peer.md @@ -0,0 +1,130 @@ +# Peers + +This document explains how Tendermint Peers are identified and how they connect to one another. + +For details on peer discovery, see the [peer exchange (PEX) reactor doc](https://github.com/tendermint/tendermint/blob/v0.35.x/spec/reactors/pex/pex.md). + +## Peer Identity + +Tendermint peers are expected to maintain long-term persistent identities in the form of a public key. +Each peer has an ID defined as `peer.ID == peer.PubKey.Address()`, where `Address` uses the scheme defined in `crypto` package. + +A single peer ID can have multiple IP addresses associated with it, but a node +will only ever connect to one at a time. + +When attempting to connect to a peer, we use the PeerURL: `@:`. +We will attempt to connect to the peer at IP:PORT, and verify, +via authenticated encryption, that it is in possession of the private key +corresponding to ``. This prevents man-in-the-middle attacks on the peer layer. + +## Connections + +All p2p connections use TCP. +Upon establishing a successful TCP connection with a peer, +two handshakes are performed: one for authenticated encryption, and one for Tendermint versioning. +Both handshakes have configurable timeouts (they should complete quickly). + +### Authenticated Encryption Handshake + +Tendermint implements the Station-to-Station protocol +using X25519 keys for Diffie-Helman key-exchange and chacha20poly1305 for encryption. + +Previous versions of this protocol (0.32 and below) suffered from malleability attacks whereas an active man +in the middle attacker could compromise confidentiality as described in [Prime, Order Please! +Revisiting Small Subgroup and Invalid Curve Attacks on +Protocols using Diffie-Hellman](https://eprint.iacr.org/2019/526.pdf). + +We have added dependency on the Merlin a keccak based transcript hashing protocol to ensure non-malleability. + +It goes as follows: + +- generate an ephemeral X25519 keypair +- send the ephemeral public key to the peer +- wait to receive the peer's ephemeral public key +- create a new Merlin Transcript with the string "TENDERMINT_SECRET_CONNECTION_TRANSCRIPT_HASH" +- Sort the ephemeral keys and add the high labeled "EPHEMERAL_UPPER_PUBLIC_KEY" and the low keys labeled "EPHEMERAL_LOWER_PUBLIC_KEY" to the Merlin transcript. +- compute the Diffie-Hellman shared secret using the peers ephemeral public key and our ephemeral private key +- add the DH secret to the transcript labeled DH_SECRET. +- generate two keys to use for encryption (sending and receiving) and a challenge for authentication as follows: + - create a hkdf-sha256 instance with the key being the diffie hellman shared secret, and info parameter as + `TENDERMINT_SECRET_CONNECTION_KEY_AND_CHALLENGE_GEN` + - get 64 bytes of output from hkdf-sha256 + - if we had the smaller ephemeral pubkey, use the first 32 bytes for the key for receiving, the second 32 bytes for sending; else the opposite. +- use a separate nonce for receiving and sending. Both nonces start at 0, and should support the full 96 bit nonce range +- all communications from now on are encrypted in 1400 byte frames (plus encoding overhead), + using the respective secret and nonce. Each nonce is incremented by one after each use. +- we now have an encrypted channel, but still need to authenticate +- extract a 32 bytes challenge from merlin transcript with the label "SECRET_CONNECTION_MAC" +- sign the common challenge obtained from the hkdf with our persistent private key +- send the amino encoded persistent pubkey and signature to the peer +- wait to receive the persistent public key and signature from the peer +- verify the signature on the challenge using the peer's persistent public key + +If this is an outgoing connection (we dialed the peer) and we used a peer ID, +then finally verify that the peer's persistent public key corresponds to the peer ID we dialed, +ie. `peer.PubKey.Address() == `. + +The connection has now been authenticated. All traffic is encrypted. + +Note: only the dialer can authenticate the identity of the peer, +but this is what we care about since when we join the network we wish to +ensure we have reached the intended peer (and are not being MITMd). + +### Peer Filter + +Before continuing, we check if the new peer has the same ID as ourselves or +an existing peer. If so, we disconnect. + +We also check the peer's address and public key against +an optional whitelist which can be managed through the ABCI app - +if the whitelist is enabled and the peer does not qualify, the connection is +terminated. + +### Tendermint Version Handshake + +The Tendermint Version Handshake allows the peers to exchange their NodeInfo: + +```golang +type NodeInfo struct { + Version p2p.Version + ID p2p.ID + ListenAddr string + + Network string + SoftwareVersion string + Channels []int8 + + Moniker string + Other NodeInfoOther +} + +type Version struct { + P2P uint64 + Block uint64 + App uint64 +} + +type NodeInfoOther struct { + TxIndex string + RPCAddress string +} +``` + +The connection is disconnected if: + +- `peer.NodeInfo.ID` is not equal `peerConn.ID` +- `peer.NodeInfo.Version.Block` does not match ours +- `peer.NodeInfo.Network` is not the same as ours +- `peer.Channels` does not intersect with our known Channels. +- `peer.NodeInfo.ListenAddr` is malformed or is a DNS host that cannot be + resolved + +At this point, if we have not disconnected, the peer is valid. +It is added to the switch and hence all reactors via the `AddPeer` method. +Note that each reactor may handle multiple channels. + +## Connection Activity + +Once a peer is added, incoming messages for a given reactor are handled through +that reactor's `Receive` method, and output messages are sent directly by the Reactors +on each peer. A typical reactor maintains per-peer go-routine(s) that handle this. diff --git a/spec/p2p/readme.md b/spec/p2p/readme.md new file mode 100644 index 000000000..96867aad0 --- /dev/null +++ b/spec/p2p/readme.md @@ -0,0 +1,6 @@ +--- +order: 1 +parent: + title: P2P + order: 6 +--- diff --git a/spec/rpc/README.md b/spec/rpc/README.md new file mode 100644 index 000000000..7cdf417dc --- /dev/null +++ b/spec/rpc/README.md @@ -0,0 +1,1264 @@ +--- +order: 1 +parent: + title: RPC + order: 6 +--- + +# RPC spec + +This file defines the JSON-RPC spec of Tendermint. This is meant to be implemented by all clients. + +## Support + + | | [Tendermint-Go](https://github.com/tendermint/tendermint/) | [endermint-Rs](https://github.com/informalsystems/tendermint-rs) | + |--------------|:----------------------------------------------------------:|:----------------------------------------------------------------:| + | JSON-RPC 2.0 | ✅ | ✅ | + | HTTP | ✅ | ✅ | + | HTTPS | ✅ | ❌ | + | WS | ✅ | ✅ | + + | Routes | [Tendermint-Go](https://github.com/tendermint/tendermint/) | [Tendermint-Rs](https://github.com/informalsystems/tendermint-rs) | + |-----------------------------------------|:----------------------------------------------------------:|:-----------------------------------------------------------------:| + | [Health](#health) | ✅ | ✅ | + | [Status](#status) | ✅ | ✅ | + | [NetInfo](#netinfo) | ✅ | ✅ | + | [Blockchain](#blockchain) | ✅ | ✅ | + | [Block](#block) | ✅ | ✅ | + | [BlockByHash](#blockbyhash) | ✅ | ❌ | + | [BlockResults](#blockresults) | ✅ | ✅ | + | [Commit](#commit) | ✅ | ✅ | + | [Validators](#validators) | ✅ | ✅ | + | [Genesis](#genesis) | ✅ | ✅ | + | [GenesisChunked](#genesischunked) | ✅ | ❌ | + | [ConsensusParams](#consensusparams) | ✅ | ❌ | + | [UnconfirmedTxs](#unconfirmedtxs) | ✅ | ❌ | + | [NumUnconfirmedTxs](#numunconfirmedtxs) | ✅ | ❌ | + | [Tx](#tx) | ✅ | ❌ | + | [BroadCastTxSync](#broadcasttxsync) | ✅ | ✅ | + | [BroadCastTxAsync](#broadcasttxasync) | ✅ | ✅ | + | [ABCIInfo](#abciinfo) | ✅ | ✅ | + | [ABCIQuery](#abciquery) | ✅ | ✅ | + | [BroadcastTxAsync](#broadcasttxasync) | ✅ | ✅ | + | [BroadcastEvidence](#broadcastevidence) | ✅ | ✅ | + +## Timestamps + +Timestamps in the RPC layer of Tendermint follows RFC3339Nano. The RFC3339Nano format removes trailing zeros from the seconds field. + +This means if a block has a timestamp like: `1985-04-12T23:20:50.5200000Z`, the value returned in the RPC will be `1985-04-12T23:20:50.52Z`. + + + +## Info Routes + +### Health + +Node heartbeat + +#### Parameters + +None + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/health +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"health\"}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": -1, + "result": {} +} +``` + +### Status + +Get Tendermint status including node info, pubkey, latest block hash, app hash, block height and time. + +#### Parameters + +None + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/status +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"status\"}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": -1, + "result": { + "node_info": { + "protocol_version": { + "p2p": "8", + "block": "11", + "app": "0" + }, + "id": "b93270b358a72a2db30089f3856475bb1f918d6d", + "listen_addr": "tcp://0.0.0.0:26656", + "network": "cosmoshub-4", + "version": "v0.34.8", + "channels": "40202122233038606100", + "moniker": "aib-hub-node", + "other": { + "tx_index": "on", + "rpc_address": "tcp://0.0.0.0:26657" + } + }, + "sync_info": { + "latest_block_hash": "50F03C0EAACA8BCA7F9C14189ACE9C05A9A1BBB5268DB63DC6A3C848D1ECFD27", + "latest_app_hash": "2316CFF7644219F4F15BEE456435F280E2B38955EEA6D4617CCB6D7ABF781C22", + "latest_block_height": "5622165", + "latest_block_time": "2021-03-25T14:00:43.356134226Z", + "earliest_block_hash": "1455A0C15AC49BB506992EC85A3CD4D32367E53A087689815E01A524231C3ADF", + "earliest_app_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + "earliest_block_height": "5200791", + "earliest_block_time": "2019-12-11T16:11:34Z", + "catching_up": false + }, + "validator_info": { + "address": "38FB765D0092470989360ECA1C89CD06C2C1583C", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "Z+8kntVegi1sQiWLYwFSVLNWqdAUGEy7lskL78gxLZI=" + }, + "voting_power": "0" + } + } +} +``` + +### NetInfo + +Network information + +#### Parameters + +None + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/net_info +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"net_info\"}" +``` + +#### Response + +```json +{ + "id": 0, + "jsonrpc": "2.0", + "result": { + "listening": true, + "listeners": [ + "Listener(@)" + ], + "n_peers": "1", + "peers": [ + { + "node_id": "5576458aef205977e18fd50b274e9b5d9014525a", + "url": "tcp://5576458aef205977e18fd50b274e9b5d9014525a@95.179.155.35:26656" + } + ] + } +} +``` + +### Blockchain + +Get block headers. Returned in descending order. May be limited in quantity. + +#### Parameters + +- `minHeight (integer)`: The lowest block to be returned in the response +- `maxHeight (integer)`: The highest block to be returned in the response + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/blockchain + +curl http://127.0.0.1:26657/blockchain?minHeight=1&maxHeight=2 +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"blockchain\",\"params\":{\"minHeight\":\"1\", \"maxHeight\":\"2\"}}" +``` + +#### Response + +```json +{ + "id": 0, + "jsonrpc": "2.0", + "result": { + "last_height": "1276718", + "block_metas": [ + { + "block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "block_size": 1000000, + "header": { + "version": { + "block": "10", + "app": "0" + }, + "chain_id": "cosmoshub-2", + "height": "12", + "time": "2019-04-22T17:01:51.701356223Z", + "last_block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "last_commit_hash": "21B9BC845AD2CB2C4193CDD17BFC506F1EBE5A7402E84AD96E64171287A34812", + "data_hash": "970886F99E77ED0D60DA8FCE0447C2676E59F2F77302B0C4AA10E1D02F18EF73", + "validators_hash": "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0", + "next_validators_hash": "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0", + "consensus_hash": "0F2908883A105C793B74495EB7D6DF2EEA479ED7FC9349206A65CB0F9987A0B8", + "app_hash": "223BF64D4A01074DC523A80E76B9BBC786C791FB0A1893AC5B14866356FCFD6C", + "last_results_hash": "", + "evidence_hash": "", + "proposer_address": "D540AB022088612AC74B287D076DBFBC4A377A2E" + }, + "num_txs": "54" + } + ] + } +} +``` + +### Block + +Get block at a specified height. + +#### Parameters + +- `height (integer)`: height of the requested block. If no height is specified the latest block will be used. + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/block + +curl http://127.0.0.1:26657/block?height=1 +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"block\",\"params\":{\"height\":\"1\"}}" +``` + +#### Response + +```json +{ + "id": 0, + "jsonrpc": "2.0", + "result": { + "block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "block": { + "header": { + "version": { + "block": "10", + "app": "0" + }, + "chain_id": "cosmoshub-2", + "height": "12", + "time": "2019-04-22T17:01:51.701356223Z", + "last_block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "last_commit_hash": "21B9BC845AD2CB2C4193CDD17BFC506F1EBE5A7402E84AD96E64171287A34812", + "data_hash": "970886F99E77ED0D60DA8FCE0447C2676E59F2F77302B0C4AA10E1D02F18EF73", + "validators_hash": "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0", + "next_validators_hash": "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0", + "consensus_hash": "0F2908883A105C793B74495EB7D6DF2EEA479ED7FC9349206A65CB0F9987A0B8", + "app_hash": "223BF64D4A01074DC523A80E76B9BBC786C791FB0A1893AC5B14866356FCFD6C", + "last_results_hash": "", + "evidence_hash": "", + "proposer_address": "D540AB022088612AC74B287D076DBFBC4A377A2E" + }, + "data": [ + "yQHwYl3uCkKoo2GaChRnd+THLQ2RM87nEZrE19910Z28ABIUWW/t8AtIMwcyU0sT32RcMDI9GF0aEAoFdWF0b20SBzEwMDAwMDASEwoNCgV1YXRvbRIEMzEwMRCd8gEaagom61rphyEDoJPxlcjRoNDtZ9xMdvs+lRzFaHe2dl2P5R2yVCWrsHISQKkqX5H1zXAIJuC57yw0Yb03Fwy75VRip0ZBtLiYsUqkOsPUoQZAhDNP+6LY+RUwz/nVzedkF0S29NZ32QXdGv0=" + ], + "evidence": [ + { + "type": "string", + "height": 0, + "time": 0, + "total_voting_power": 0, + "validator": { + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "A6DoBUypNtUAyEHWtQ9bFjfNg8Bo9CrnkUGl6k6OHN4=" + }, + "voting_power": 0, + "address": "string" + } + } + ], + "last_commit": { + "height": 0, + "round": 0, + "block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "signatures": [ + { + "type": 2, + "height": "1262085", + "round": 0, + "block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "timestamp": "2019-08-01T11:39:38.867269833Z", + "validator_address": "000001E443FD237E4B616E2FA69DF4EE3D49A94F", + "validator_index": 0, + "signature": "DBchvucTzAUEJnGYpNvMdqLhBAHG4Px8BsOBB3J3mAFCLGeuG7uJqy+nVngKzZdPhPi8RhmE/xcw/M9DOJjEDg==" + } + ] + } + } + } +} +``` + +### BlockByHash + +#### Parameters + +- `hash (string)`: Hash of the block to query for. + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/block_by_hash?hash=0xD70952032620CC4E2737EB8AC379806359D8E0B17B0488F627997A0B043ABDED +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"block_by_hash\",\"params\":{\"hash\":\"0xD70952032620CC4E2737EB8AC379806359D8E0B17B0488F627997A0B043ABDED\"}}" +``` + +#### Response + +```json +{ + "id": 0, + "jsonrpc": "2.0", + "result": { + "block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "block": { + "header": { + "version": { + "block": "10", + "app": "0" + }, + "chain_id": "cosmoshub-2", + "height": "12", + "time": "2019-04-22T17:01:51.701356223Z", + "last_block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "last_commit_hash": "21B9BC845AD2CB2C4193CDD17BFC506F1EBE5A7402E84AD96E64171287A34812", + "data_hash": "970886F99E77ED0D60DA8FCE0447C2676E59F2F77302B0C4AA10E1D02F18EF73", + "validators_hash": "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0", + "next_validators_hash": "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0", + "consensus_hash": "0F2908883A105C793B74495EB7D6DF2EEA479ED7FC9349206A65CB0F9987A0B8", + "app_hash": "223BF64D4A01074DC523A80E76B9BBC786C791FB0A1893AC5B14866356FCFD6C", + "last_results_hash": "", + "evidence_hash": "", + "proposer_address": "D540AB022088612AC74B287D076DBFBC4A377A2E" + }, + "data": [ + "yQHwYl3uCkKoo2GaChRnd+THLQ2RM87nEZrE19910Z28ABIUWW/t8AtIMwcyU0sT32RcMDI9GF0aEAoFdWF0b20SBzEwMDAwMDASEwoNCgV1YXRvbRIEMzEwMRCd8gEaagom61rphyEDoJPxlcjRoNDtZ9xMdvs+lRzFaHe2dl2P5R2yVCWrsHISQKkqX5H1zXAIJuC57yw0Yb03Fwy75VRip0ZBtLiYsUqkOsPUoQZAhDNP+6LY+RUwz/nVzedkF0S29NZ32QXdGv0=" + ], + "evidence": [ + { + "type": "string", + "height": 0, + "time": 0, + "total_voting_power": 0, + "validator": { + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "A6DoBUypNtUAyEHWtQ9bFjfNg8Bo9CrnkUGl6k6OHN4=" + }, + "voting_power": 0, + "address": "string" + } + } + ], + "last_commit": { + "height": 0, + "round": 0, + "block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "signatures": [ + { + "type": 2, + "height": "1262085", + "round": 0, + "block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "timestamp": "2019-08-01T11:39:38.867269833Z", + "validator_address": "000001E443FD237E4B616E2FA69DF4EE3D49A94F", + "validator_index": 0, + "signature": "DBchvucTzAUEJnGYpNvMdqLhBAHG4Px8BsOBB3J3mAFCLGeuG7uJqy+nVngKzZdPhPi8RhmE/xcw/M9DOJjEDg==" + } + ] + } + } + } +} +``` + +### BlockResults + +### Parameters + +- `height (integer)`: Height of the block which contains the results. If no height is specified, the latest block height will be used + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/block_results + + +curl http://127.0.0.1:26657/block_results?height=1 +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"block_results\",\"params\":{\"height\":\"1\"}}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "height": "12", + "total_gas_used": "100", + "txs_results": [ + { + "code": "0", + "data": "", + "log": "not enough gas", + "info": "", + "gas_wanted": "100", + "gas_used": "100", + "events": [ + { + "type": "app", + "attributes": [ + { + "key": "YWN0aW9u", + "value": "c2VuZA==", + "index": false + } + ] + } + ], + "codespace": "ibc" + } + ], + "begin_block_events": [ + { + "type": "app", + "attributes": [ + { + "key": "YWN0aW9u", + "value": "c2VuZA==", + "index": false + } + ] + } + ], + "end_block": [ + { + "type": "app", + "attributes": [ + { + "key": "YWN0aW9u", + "value": "c2VuZA==", + "index": false + } + ] + } + ], + "validator_updates": [ + { + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "9tK9IT+FPdf2qm+5c2qaxi10sWP+3erWTKgftn2PaQM=" + }, + "power": "300" + } + ], + "consensus_params_updates": { + "block": { + "max_bytes": "22020096", + "max_gas": "1000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "100000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + } + } +} +``` + +### Commit + +#### Parameters + +- `height (integer)`: Height of the block the requested commit pertains to. If no height is set the latest commit will be returned. + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/commit + + +curl http://127.0.0.1:26657/commit?height=1 +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"commit\",\"params\":{\"height\":\"1\"}}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "signed_header": { + "header": { + "version": { + "block": "10", + "app": "0" + }, + "chain_id": "cosmoshub-2", + "height": "12", + "time": "2019-04-22T17:01:51.701356223Z", + "last_block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "last_commit_hash": "21B9BC845AD2CB2C4193CDD17BFC506F1EBE5A7402E84AD96E64171287A34812", + "data_hash": "970886F99E77ED0D60DA8FCE0447C2676E59F2F77302B0C4AA10E1D02F18EF73", + "validators_hash": "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0", + "next_validators_hash": "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0", + "consensus_hash": "0F2908883A105C793B74495EB7D6DF2EEA479ED7FC9349206A65CB0F9987A0B8", + "app_hash": "223BF64D4A01074DC523A80E76B9BBC786C791FB0A1893AC5B14866356FCFD6C", + "last_results_hash": "", + "evidence_hash": "", + "proposer_address": "D540AB022088612AC74B287D076DBFBC4A377A2E" + }, + "commit": { + "height": "1311801", + "round": 0, + "block_id": { + "hash": "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7", + "parts": { + "total": 1, + "hash": "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + } + }, + "signatures": [ + { + "block_id_flag": 2, + "validator_address": "000001E443FD237E4B616E2FA69DF4EE3D49A94F", + "timestamp": "2019-04-22T17:01:58.376629719Z", + "signature": "14jaTQXYRt8kbLKEhdHq7AXycrFImiLuZx50uOjs2+Zv+2i7RTG/jnObD07Jo2ubZ8xd7bNBJMqkgtkd0oQHAw==" + } + ] + } + }, + "canonical": true + } +} +``` + +### Validators + +#### Parameters + +- `height (integer)`: Block height at which the validators were present on. If no height is set the latest commit will be returned. +- `page (integer)`: +- `per_page (integer)`: + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/validators +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"validators\",\"params\":{\"height\":\"1\", \"page\":\"1\", \"per_page\":\"20\"}}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "block_height": "55", + "validators": [ + { + "address": "000001E443FD237E4B616E2FA69DF4EE3D49A94F", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "9tK9IT+FPdf2qm+5c2qaxi10sWP+3erWTKgftn2PaQM=" + }, + "voting_power": "239727", + "proposer_priority": "-11896414" + } + ], + "count": "1", + "total": "25" + } +} +``` + +### Genesis + +Get Genesis of the chain. If the response is large, this operation +will return an error: use `genesis_chunked` instead. + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/genesis +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"genesis\"}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "genesis": { + "genesis_time": "2019-04-22T17:00:00Z", + "chain_id": "cosmoshub-2", + "initial_height": "2", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "1000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "100000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + }, + "validators": [ + { + "address": "B00A6323737F321EB0B8D59C6FD497A14B60938A", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "cOQZvh/h9ZioSeUMZB/1Vy1Xo5x2sjrVjlE/qHnYifM=" + }, + "power": "9328525", + "name": "Certus One" + } + ], + "app_hash": "", + "app_state": {} + } + } +} +``` + +### GenesisChunked + +Get the genesis document in a chunks to support easily transfering larger documents. + +#### Parameters + +- `chunk` (integer): the index number of the chunk that you wish to + fetch. These IDs are 0 indexed. + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/genesis_chunked?chunk=0 +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"genesis_chunked\",\"params\":{\"chunk\":0}}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "chunk": 0, + "total": 10, + "data": "dGVuZGVybWludAo=" + } +} +``` + +### ConsensusParams + +Get the consensus parameters. + +#### Parameters + +- `height (integer)`: Block height at which the consensus params would like to be fetched for. + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/consensus_params +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"consensus_params\"}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "block_height": "1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "1000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "100000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + } + } +} +``` + +### UnconfirmedTxs + +Get a list of unconfirmed transactions. + +#### Parameters + +- `limit (integer)` The amount of txs to respond with. + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/unconfirmed_txs +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"unconfirmed_txs\, \"params\":{\"limit\":\"20\"}}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "n_txs": "82", + "total": "82", + "total_bytes": "19974", + "txs": [ + "gAPwYl3uCjCMTXENChSMnIkb5ZpYHBKIZqecFEV2tuZr7xIUA75/FmYq9WymsOBJ0XSJ8yV8zmQKMIxNcQ0KFIyciRvlmlgcEohmp5wURXa25mvvEhQbrvwbvlNiT+Yjr86G+YQNx7kRVgowjE1xDQoUjJyJG+WaWBwSiGannBRFdrbma+8SFK2m+1oxgILuQLO55n8mWfnbIzyPCjCMTXENChSMnIkb5ZpYHBKIZqecFEV2tuZr7xIUQNGfkmhTNMis4j+dyMDIWXdIPiYKMIxNcQ0KFIyciRvlmlgcEohmp5wURXa25mvvEhS8sL0D0wwgGCItQwVowak5YB38KRIUCg4KBXVhdG9tEgUxMDA1NBDoxRgaagom61rphyECn8x7emhhKdRCB2io7aS/6Cpuq5NbVqbODmqOT3jWw6kSQKUresk+d+Gw0BhjiggTsu8+1voW+VlDCQ1GRYnMaFOHXhyFv7BCLhFWxLxHSAYT8a5XqoMayosZf9mANKdXArA=" + ] + } +} +``` + +### NumUnconfirmedTxs + +Get data about unconfirmed transactions. + +#### Parameters + +None + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/num_unconfirmed_txs +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"num_unconfirmed_txs\"}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "n_txs": "31", + "total": "82", + "total_bytes": "19974" + } +} +``` + +### Tx + +#### Parameters + +- `hash (string)`: The hash of the transaction +- `prove (bool)`: If the response should include proof the transaction was included in a block. + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/num_unconfirmed_txs +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"num_unconfirmed_txs\"}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "hash": "D70952032620CC4E2737EB8AC379806359D8E0B17B0488F627997A0B043ABDED", + "height": "1000", + "index": 0, + "tx_result": { + "log": "[{\"msg_index\":\"0\",\"success\":true,\"log\":\"\"}]", + "gas_wanted": "200000", + "gas_used": "28596", + "tags": [ + { + "key": "YWN0aW9u", + "value": "c2VuZA==", + "index": false + } + ] + }, + "tx": "5wHwYl3uCkaoo2GaChQmSIu8hxpJxLcCuIi8fiHN4TMwrRIU/Af1cEG7Rcs/6LjTl7YjRSymJfYaFAoFdWF0b20SCzE0OTk5OTk1MDAwEhMKDQoFdWF0b20SBDUwMDAQwJoMGmoKJuta6YchAwswBShaB1wkZBctLIhYqBC3JrAI28XGzxP+rVEticGEEkAc+khTkKL9CDE47aDvjEHvUNt+izJfT4KVF2v2JkC+bmlH9K08q3PqHeMI9Z5up+XMusnTqlP985KF+SI5J3ZOIhhNYWRlIGJ5IENpcmNsZSB3aXRoIGxvdmU=" + } +} +``` + +## Transaction Routes + +### BroadCastTxSync + +Returns with the response from CheckTx. Does not wait for DeliverTx result. + +#### Parameters + +- `tx (string)`: The transaction encoded + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/broadcast_tx_sync?tx=encoded_tx +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"broadcast_tx_sync\",\"params\":{\"tx\":\"a/encoded_tx/c\"}}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "code": "0", + "data": "", + "log": "", + "codespace": "ibc", + "hash": "0D33F2F03A5234F38706E43004489E061AC40A2E" + }, + "error": "" +} +``` + +### BroadCastTxAsync + +Returns right away, with no response. Does not wait for CheckTx nor DeliverTx results. + +#### Parameters + +- `tx (string)`: The transaction encoded + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/broadcast_tx_async?tx=encoded_tx +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"broadcast_tx_async\",\"params\":{\"tx\":\"a/encoded_tx/c\"}}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "code": "0", + "data": "", + "log": "", + "codespace": "ibc", + "hash": "0D33F2F03A5234F38706E43004489E061AC40A2E" + }, + "error": "" +} +``` + +### CheckTx + +Checks the transaction without executing it. + +#### Parameters + +- `tx (string)`: String of the encoded transaction + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/check_tx?tx=encoded_tx +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"check_tx\",\"params\":{\"tx\":\"a/encoded_tx/c\"}}" +``` + +#### Response + +```json +{ + "id": 0, + "jsonrpc": "2.0", + "error": "", + "result": { + "code": "0", + "data": "", + "log": "", + "info": "", + "gas_wanted": "1", + "gas_used": "0", + "events": [ + { + "type": "app", + "attributes": [ + { + "key": "YWN0aW9u", + "value": "c2VuZA==", + "index": false + } + ] + } + ], + "codespace": "bank" + } +} +``` + +## ABCI Routes + +### ABCIInfo + +Get some info about the application. + +#### Parameters + +None + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/abci_info +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"abci_info\"}" +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "response": { + "data": "{\"size\":0}", + "version": "0.16.1", + "app_version": "1314126" + } + } +} +``` + +### ABCIQuery + +Query the application for some information. + +#### Parameters + +- `path (string)`: Path to the data. This is defined by the application. +- `data (string)`: The data requested +- `height (integer)`: Height at which the data is being requested for. +- `prove (bool)`: Include proofs of the transactions inclusion in the block + +#### Request + +##### HTTP + +```sh +curl http://127.0.0.1:26657/abci_query?path="a/b/c"=IHAVENOIDEA&height=1&prove=true +``` + +##### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"abci_query\",\"params\":{\"path\":\"a/b/c\", \"height\":\"1\", \"bool\":\"true\"}}" +``` + +#### Response + +```json +{ + "error": "", + "result": { + "response": { + "log": "exists", + "height": "0", + "proof": "010114FED0DAD959F36091AD761C922ABA3CBF1D8349990101020103011406AA2262E2F448242DF2C2607C3CDC705313EE3B0001149D16177BC71E445476174622EA559715C293740C", + "value": "61626364", + "key": "61626364", + "index": "-1", + "code": "0" + } + }, + "id": 0, + "jsonrpc": "2.0" +} +``` + +## Evidence Routes + +### BroadcastEvidence + +Broadcast evidence of the misbehavior. + +#### Parameters + +- `evidence (string)`: + +#### Request + +##### HTTP + +```sh +curl http://localhost:26657/broadcast_evidence?evidence=JSON_EVIDENCE_encoded +``` + +#### JSONRPC + +```sh +curl -X POST https://localhost:26657 -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"broadcast_evidence\",\"params\":{\"evidence\":\"JSON_EVIDENCE_encoded\"}}" +``` + +#### Response + +```json +{ + "error": "", + "result": "", + "id": 0, + "jsonrpc": "2.0" +} +``` diff --git a/third_party/proto/gogoproto/gogo.proto b/third_party/proto/gogoproto/gogo.proto index 27960ecfb..31c516cd0 100644 --- a/third_party/proto/gogoproto/gogo.proto +++ b/third_party/proto/gogoproto/gogo.proto @@ -144,4 +144,4 @@ extend google.protobuf.FieldOptions { optional bool wktpointer = 65012; optional string castrepeated = 65013; -} \ No newline at end of file +} diff --git a/tools/tools.go b/tools/tools.go index 9fc291d99..52a676b00 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -8,6 +8,7 @@ package tools import ( + _ "github.com/bufbuild/buf/cmd/buf" _ "github.com/golangci/golangci-lint/cmd/golangci-lint" _ "github.com/vektra/mockery/v2" ) diff --git a/types/block.go b/types/block.go index 2f444be74..b60eb2973 100644 --- a/types/block.go +++ b/types/block.go @@ -329,7 +329,7 @@ func MakeBlock(height int64, txs []Tx, lastCommit *Commit, evidence []Evidence) // NOTE: changes to the Header should be duplicated in: // - header.Hash() // - abci.Header -// - https://github.com/tendermint/spec/blob/master/spec/blockchain/blockchain.md +// - https://github.com/tendermint/tendermint/blob/v0.35.x/spec/blockchain/blockchain.md type Header struct { // basic block info Version version.Consensus `json:"version"` From f6bbd8302cb0d6c9f625efd5065a5c42c4a858a1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Jul 2022 14:24:53 +0200 Subject: [PATCH 50/58] migration: scope key migration to stores (#9005) (#9027) --- cmd/tendermint/commands/key_migrate.go | 3 +- config/db.go | 1 + scripts/keymigrate/migrate.go | 635 ++++++++++++++++--------- scripts/keymigrate/migrate_test.go | 208 ++++---- 4 files changed, 508 insertions(+), 339 deletions(-) diff --git a/cmd/tendermint/commands/key_migrate.go b/cmd/tendermint/commands/key_migrate.go index db06fa7d1..614127e3f 100644 --- a/cmd/tendermint/commands/key_migrate.go +++ b/cmd/tendermint/commands/key_migrate.go @@ -34,7 +34,6 @@ func RunDatabaseMigration(ctx context.Context, logger log.Logger, conf *cfg.Conf // reduce the possibility of the // ephemeral data overwriting later data "tx_index", - "peerstore", "light", "blockstore", "state", @@ -57,7 +56,7 @@ func RunDatabaseMigration(ctx context.Context, logger log.Logger, conf *cfg.Conf return fmt.Errorf("constructing database handle: %w", err) } - if err = keymigrate.Migrate(ctx, db); err != nil { + if err = keymigrate.Migrate(ctx, dbctx, db); err != nil { return fmt.Errorf("running migration for context %q: %w", dbctx, err) } diff --git a/config/db.go b/config/db.go index 8f489a87a..4f0048bf7 100644 --- a/config/db.go +++ b/config/db.go @@ -23,5 +23,6 @@ type DBProvider func(*DBContext) (dbm.DB, error) // specified in the Config. func DefaultDBProvider(ctx *DBContext) (dbm.DB, error) { dbType := dbm.BackendType(ctx.Config.DBBackend) + return dbm.NewDB(ctx.ID, dbType, ctx.Config.DBDir()) } diff --git a/scripts/keymigrate/migrate.go b/scripts/keymigrate/migrate.go index a0b43aef6..2c5873427 100644 --- a/scripts/keymigrate/migrate.go +++ b/scripts/keymigrate/migrate.go @@ -26,7 +26,7 @@ type ( migrateFunc func(keyID) (keyID, error) ) -func getAllLegacyKeys(db dbm.DB) ([]keyID, error) { +func getAllLegacyKeys(db dbm.DB, storeName string) ([]keyID, error) { var out []keyID iter, err := db.Iterator(nil, nil) @@ -37,9 +37,16 @@ func getAllLegacyKeys(db dbm.DB) ([]keyID, error) { for ; iter.Valid(); iter.Next() { k := iter.Key() - // make sure it's a key with a legacy format, and skip - // all other keys, to make it safe to resume the migration. - if !checkKeyType(k).isLegacy() { + // make sure it's a key that we'd expect to see in + // this database, with a legacy format, and skip all + // other keys, to make it safe to resume the + // migration. + kt, err := checkKeyType(k, storeName) + if err != nil { + return nil, err + } + + if !kt.isLegacy() { continue } @@ -88,73 +95,377 @@ var prefixes = []struct { ktype keyType check func(keyID) bool }{ - {[]byte("consensusParamsKey:"), consensusParamsKey, nil}, - {[]byte("abciResponsesKey:"), abciResponsesKey, nil}, - {[]byte("validatorsKey:"), validatorsKey, nil}, - {[]byte("stateKey"), stateStoreKey, nil}, - {[]byte("H:"), blockMetaKey, nil}, - {[]byte("P:"), blockPartKey, nil}, - {[]byte("C:"), commitKey, nil}, - {[]byte("SC:"), seenCommitKey, nil}, - {[]byte("BH:"), blockHashKey, nil}, - {[]byte("size"), lightSizeKey, nil}, - {[]byte("lb/"), lightBlockKey, nil}, - {[]byte("\x00"), evidenceCommittedKey, checkEvidenceKey}, - {[]byte("\x01"), evidencePendingKey, checkEvidenceKey}, + {[]byte(legacyConsensusParamsPrefix), consensusParamsKey, nil}, + {[]byte(legacyAbciResponsePrefix), abciResponsesKey, nil}, + {[]byte(legacyValidatorPrefix), validatorsKey, nil}, + {[]byte(legacyStateKeyPrefix), stateStoreKey, nil}, + {[]byte(legacyBlockMetaPrefix), blockMetaKey, nil}, + {[]byte(legacyBlockPartPrefix), blockPartKey, nil}, + {[]byte(legacyCommitPrefix), commitKey, nil}, + {[]byte(legacySeenCommitPrefix), seenCommitKey, nil}, + {[]byte(legacyBlockHashPrefix), blockHashKey, nil}, + {[]byte(legacyLightSizePrefix), lightSizeKey, nil}, + {[]byte(legacyLightBlockPrefix), lightBlockKey, nil}, + {[]byte(legacyEvidenceComittedPrefix), evidenceCommittedKey, checkEvidenceKey}, + {[]byte(legacyEvidencePendingPrefix), evidencePendingKey, checkEvidenceKey}, +} + +const ( + legacyConsensusParamsPrefix = "consensusParamsKey:" + legacyAbciResponsePrefix = "abciResponsesKey:" + legacyValidatorPrefix = "validatorsKey:" + legacyStateKeyPrefix = "stateKey" + legacyBlockMetaPrefix = "H:" + legacyBlockPartPrefix = "P:" + legacyCommitPrefix = "C:" + legacySeenCommitPrefix = "SC:" + legacyBlockHashPrefix = "BH:" + legacyLightSizePrefix = "size" + legacyLightBlockPrefix = "lb/" + legacyEvidenceComittedPrefix = "\x00" + legacyEvidencePendingPrefix = "\x01" +) + +type migrationDefinition struct { + name string + storeName string + prefix []byte + ktype keyType + check func(keyID) bool + transform migrateFunc +} + +var migrations = []migrationDefinition{ + { + name: "consensus-params", + storeName: "state", + prefix: []byte(legacyConsensusParamsPrefix), + ktype: consensusParamsKey, + transform: func(key keyID) (keyID, error) { + val, err := strconv.Atoi(string(key[19:])) + if err != nil { + return nil, err + } + + return orderedcode.Append(nil, int64(6), int64(val)) + }, + }, + { + name: "abci-responses", + storeName: "state", + prefix: []byte(legacyAbciResponsePrefix), + ktype: abciResponsesKey, + transform: func(key keyID) (keyID, error) { + val, err := strconv.Atoi(string(key[17:])) + if err != nil { + return nil, err + } + + return orderedcode.Append(nil, int64(7), int64(val)) + }, + }, + { + name: "validators", + storeName: "state", + prefix: []byte(legacyValidatorPrefix), + ktype: validatorsKey, + transform: func(key keyID) (keyID, error) { + val, err := strconv.Atoi(string(key[14:])) + if err != nil { + return nil, err + } + + return orderedcode.Append(nil, int64(5), int64(val)) + }, + }, + { + name: "tendermint-state", + storeName: "state", + prefix: []byte(legacyStateKeyPrefix), + ktype: stateStoreKey, + transform: func(key keyID) (keyID, error) { + return orderedcode.Append(nil, int64(8)) + }, + }, + { + name: "block-meta", + storeName: "blockstore", + prefix: []byte(legacyBlockMetaPrefix), + ktype: blockMetaKey, + transform: func(key keyID) (keyID, error) { + val, err := strconv.Atoi(string(key[2:])) + if err != nil { + return nil, err + } + + return orderedcode.Append(nil, int64(0), int64(val)) + }, + }, + { + name: "block-part", + storeName: "blockstore", + prefix: []byte(legacyBlockPartPrefix), + ktype: blockPartKey, + transform: func(key keyID) (keyID, error) { + parts := bytes.Split(key[2:], []byte(":")) + if len(parts) != 2 { + return nil, fmt.Errorf("block parts key has %d rather than 2 components", + len(parts)) + } + valOne, err := strconv.Atoi(string(parts[0])) + if err != nil { + return nil, err + } + + valTwo, err := strconv.Atoi(string(parts[1])) + if err != nil { + return nil, err + } + + return orderedcode.Append(nil, int64(1), int64(valOne), int64(valTwo)) + }, + }, + { + name: "commit", + storeName: "blockstore", + prefix: []byte(legacyCommitPrefix), + ktype: commitKey, + transform: func(key keyID) (keyID, error) { + val, err := strconv.Atoi(string(key[2:])) + if err != nil { + return nil, err + } + + return orderedcode.Append(nil, int64(2), int64(val)) + }, + }, + { + name: "seen-commit", + storeName: "blockstore", + prefix: []byte(legacySeenCommitPrefix), + ktype: seenCommitKey, + transform: func(key keyID) (keyID, error) { + val, err := strconv.Atoi(string(key[3:])) + if err != nil { + return nil, err + } + + return orderedcode.Append(nil, int64(3), int64(val)) + }, + }, + { + name: "block-hash", + storeName: "blockstore", + prefix: []byte(legacyBlockHashPrefix), + ktype: blockHashKey, + transform: func(key keyID) (keyID, error) { + hash := string(key[3:]) + if len(hash)%2 == 1 { + hash = "0" + hash + } + val, err := hex.DecodeString(hash) + if err != nil { + return nil, err + } + + return orderedcode.Append(nil, int64(4), string(val)) + }, + }, + { + name: "light-size", + storeName: "light", + prefix: []byte(legacyLightSizePrefix), + ktype: lightSizeKey, + transform: func(key keyID) (keyID, error) { + return orderedcode.Append(nil, int64(12)) + }, + }, + { + name: "light-block", + storeName: "light", + prefix: []byte(legacyLightBlockPrefix), + ktype: lightBlockKey, + transform: func(key keyID) (keyID, error) { + if len(key) < 24 { + return nil, fmt.Errorf("light block evidence %q in invalid format", string(key)) + } + + val, err := strconv.Atoi(string(key[len(key)-20:])) + if err != nil { + return nil, err + } + + return orderedcode.Append(nil, int64(11), int64(val)) + }, + }, + { + name: "evidence-pending", + storeName: "evidence", + prefix: []byte(legacyEvidencePendingPrefix), + ktype: evidencePendingKey, + transform: func(key keyID) (keyID, error) { + return convertEvidence(key, 10) + }, + }, + { + name: "evidence-committed", + storeName: "evidence", + prefix: []byte(legacyEvidenceComittedPrefix), + ktype: evidenceCommittedKey, + transform: func(key keyID) (keyID, error) { + return convertEvidence(key, 9) + }, + }, + { + name: "event-tx", + storeName: "tx_index", + prefix: nil, + ktype: txHeightKey, + transform: func(key keyID) (keyID, error) { + parts := bytes.Split(key, []byte("/")) + if len(parts) != 4 { + return nil, fmt.Errorf("key has %d parts rather than 4", len(parts)) + } + parts = parts[1:] // drop prefix + + elems := make([]interface{}, 0, len(parts)+1) + elems = append(elems, "tx.height") + + for idx, pt := range parts { + val, err := strconv.Atoi(string(pt)) + if err != nil { + return nil, err + } + if idx == 0 { + elems = append(elems, fmt.Sprintf("%d", val)) + } else { + elems = append(elems, int64(val)) + } + } + + return orderedcode.Append(nil, elems...) + }, + }, + { + name: "event-abci", + storeName: "tx_index", + prefix: nil, + ktype: abciEventKey, + transform: func(key keyID) (keyID, error) { + parts := bytes.Split(key, []byte("/")) + + elems := make([]interface{}, 0, 4) + if len(parts) == 4 { + elems = append(elems, string(parts[0]), string(parts[1])) + + val, err := strconv.Atoi(string(parts[2])) + if err != nil { + return nil, err + } + elems = append(elems, int64(val)) + + val2, err := strconv.Atoi(string(parts[3])) + if err != nil { + return nil, err + } + elems = append(elems, int64(val2)) + } else { + elems = append(elems, string(parts[0])) + parts = parts[1:] + + val, err := strconv.Atoi(string(parts[len(parts)-1])) + if err != nil { + return nil, err + } + + val2, err := strconv.Atoi(string(parts[len(parts)-2])) + if err != nil { + return nil, err + } + + appKey := bytes.Join(parts[:len(parts)-3], []byte("/")) + elems = append(elems, string(appKey), int64(val), int64(val2)) + } + + return orderedcode.Append(nil, elems...) + }, + }, + { + name: "event-tx-hash", + storeName: "tx_index", + prefix: nil, + ktype: txHashKey, + transform: func(key keyID) (keyID, error) { + return orderedcode.Append(nil, "tx.hash", string(key)) + }, + }, } // checkKeyType classifies a candidate key based on its structure. -func checkKeyType(key keyID) keyType { - for _, p := range prefixes { - if bytes.HasPrefix(key, p.prefix) { - if p.check == nil || p.check(key) { - return p.ktype +func checkKeyType(key keyID, storeName string) (keyType, error) { + var migrations []migrationDefinition + for _, m := range migrations { + if m.storeName != storeName { + continue + } + if m.prefix == nil && storeName == "tx_index" { + // A legacy event key has the form: + // + // / / / + // + // Transaction hashes are stored as a raw binary hash with no prefix. + // + // Note, though, that nothing prevents event names or values from containing + // additional "/" separators, so the parse has to be forgiving. + parts := bytes.Split(key, []byte("/")) + if len(parts) >= 4 { + // Special case for tx.height. + if len(parts) == 4 && bytes.Equal(parts[0], []byte("tx.height")) { + return txHeightKey, nil + } + + // The name cannot be empty, but we don't know where the name ends and + // the value begins, so insist that there be something. + var n int + for _, part := range parts[:len(parts)-2] { + n += len(part) + } + // Check whether the last two fields could be .../height/index. + if n > 0 && isDecimal(parts[len(parts)-1]) && isDecimal(parts[len(parts)-2]) { + return abciEventKey, nil + } } - } - } - // A legacy event key has the form: - // - // / / / - // - // Transaction hashes are stored as a raw binary hash with no prefix. - // - // Because a hash can contain any byte, it is possible (though unlikely) - // that a hash could have the correct form for an event key, in which case - // we would translate it incorrectly. To reduce the likelihood of an - // incorrect interpretation, we parse candidate event keys and check for - // some structural properties before making a decision. - // - // Note, though, that nothing prevents event names or values from containing - // additional "/" separators, so the parse has to be forgiving. - parts := bytes.Split(key, []byte("/")) - if len(parts) >= 4 { - // Special case for tx.height. - if len(parts) == 4 && bytes.Equal(parts[0], []byte("tx.height")) { - return txHeightKey - } - - // The name cannot be empty, but we don't know where the name ends and - // the value begins, so insist that there be something. - var n int - for _, part := range parts[:len(parts)-2] { - n += len(part) - } - // Check whether the last two fields could be .../height/index. - if n > 0 && isDecimal(parts[len(parts)-1]) && isDecimal(parts[len(parts)-2]) { - return abciEventKey + // If we get here, it's not an event key. Treat it as a hash if it is the + // right length. Note that it IS possible this could collide with the + // translation of some other key (though not a hash, since encoded hashes + // will be longer). The chance of that is small, but there is nothing we can + // do to detect it. + if len(key) == 32 { + return txHashKey, nil + } + } else if bytes.HasPrefix(key, m.prefix) { + if m.check == nil || m.check(key) { + return m.ktype, nil + } + // we have an expected legacy prefix but that + // didn't pass the check. This probably means + // the evidence data is currupt (based on the + // defined migrations) best to error here. + return -1, fmt.Errorf("in store %q, key %q exists but is not a valid key of type %q", storeName, key, m.ktype) } + // if we get here, the key in question is either + // migrated or of a different type. We can't break + // here because there are more than one key type in a + // specific database, so we have to keep iterating. } + // if we've looked at every migration and not identified a key + // type, then the key has been migrated *or* we (possibly, but + // very unlikely have data that is in the wrong place or the + // sign of corruption.) In either case we should not attempt + // more migrations at this point - // If we get here, it's not an event key. Treat it as a hash if it is the - // right length. Note that it IS possible this could collide with the - // translation of some other key (though not a hash, since encoded hashes - // will be longer). The chance of that is small, but there is nothing we can - // do to detect it. - if len(key) == 32 { - return txHashKey - } - return nonLegacyKey + return nonLegacyKey, nil } // isDecimal reports whether buf is a non-empty sequence of Unicode decimal @@ -168,161 +479,21 @@ func isDecimal(buf []byte) bool { return len(buf) != 0 } -func migrateKey(key keyID) (keyID, error) { - switch checkKeyType(key) { - case blockMetaKey: - val, err := strconv.Atoi(string(key[2:])) - if err != nil { - return nil, err - } - - return orderedcode.Append(nil, int64(0), int64(val)) - case blockPartKey: - parts := bytes.Split(key[2:], []byte(":")) - if len(parts) != 2 { - return nil, fmt.Errorf("block parts key has %d rather than 2 components", - len(parts)) - } - valOne, err := strconv.Atoi(string(parts[0])) - if err != nil { - return nil, err - } - - valTwo, err := strconv.Atoi(string(parts[1])) - if err != nil { - return nil, err - } - - return orderedcode.Append(nil, int64(1), int64(valOne), int64(valTwo)) - case commitKey: - val, err := strconv.Atoi(string(key[2:])) - if err != nil { - return nil, err - } - - return orderedcode.Append(nil, int64(2), int64(val)) - case seenCommitKey: - val, err := strconv.Atoi(string(key[3:])) - if err != nil { - return nil, err - } - - return orderedcode.Append(nil, int64(3), int64(val)) - case blockHashKey: - hash := string(key[3:]) - if len(hash)%2 == 1 { - hash = "0" + hash - } - val, err := hex.DecodeString(hash) - if err != nil { - return nil, err - } - - return orderedcode.Append(nil, int64(4), string(val)) - case validatorsKey: - val, err := strconv.Atoi(string(key[14:])) - if err != nil { - return nil, err - } - - return orderedcode.Append(nil, int64(5), int64(val)) - case consensusParamsKey: - val, err := strconv.Atoi(string(key[19:])) - if err != nil { - return nil, err - } - - return orderedcode.Append(nil, int64(6), int64(val)) - case abciResponsesKey: - val, err := strconv.Atoi(string(key[17:])) - if err != nil { - return nil, err - } - - return orderedcode.Append(nil, int64(7), int64(val)) - case stateStoreKey: - return orderedcode.Append(nil, int64(8)) - case evidenceCommittedKey: - return convertEvidence(key, 9) - case evidencePendingKey: - return convertEvidence(key, 10) - case lightBlockKey: - if len(key) < 24 { - return nil, fmt.Errorf("light block evidence %q in invalid format", string(key)) - } - - val, err := strconv.Atoi(string(key[len(key)-20:])) - if err != nil { - return nil, err - } - - return orderedcode.Append(nil, int64(11), int64(val)) - case lightSizeKey: - return orderedcode.Append(nil, int64(12)) - case txHeightKey: - parts := bytes.Split(key, []byte("/")) - if len(parts) != 4 { - return nil, fmt.Errorf("key has %d parts rather than 4", len(parts)) - } - parts = parts[1:] // drop prefix - - elems := make([]interface{}, 0, len(parts)+1) - elems = append(elems, "tx.height") - - for idx, pt := range parts { - val, err := strconv.Atoi(string(pt)) - if err != nil { - return nil, err - } - if idx == 0 { - elems = append(elems, fmt.Sprintf("%d", val)) - } else { - elems = append(elems, int64(val)) - } - } - - return orderedcode.Append(nil, elems...) - case abciEventKey: - parts := bytes.Split(key, []byte("/")) - - elems := make([]interface{}, 0, 4) - if len(parts) == 4 { - elems = append(elems, string(parts[0]), string(parts[1])) - - val, err := strconv.Atoi(string(parts[2])) - if err != nil { - return nil, err - } - elems = append(elems, int64(val)) - - val2, err := strconv.Atoi(string(parts[3])) - if err != nil { - return nil, err - } - elems = append(elems, int64(val2)) - } else { - elems = append(elems, string(parts[0])) - parts = parts[1:] - - val, err := strconv.Atoi(string(parts[len(parts)-1])) - if err != nil { - return nil, err - } - - val2, err := strconv.Atoi(string(parts[len(parts)-2])) - if err != nil { - return nil, err - } - - appKey := bytes.Join(parts[:len(parts)-3], []byte("/")) - elems = append(elems, string(appKey), int64(val), int64(val2)) - } - return orderedcode.Append(nil, elems...) - case txHashKey: - return orderedcode.Append(nil, "tx.hash", string(key)) - default: - return nil, fmt.Errorf("key %q is in the wrong format", string(key)) +func migrateKey(key keyID, storeName string) (keyID, error) { + kt, err := checkKeyType(key, storeName) + if err != nil { + return nil, err } + for _, migration := range migrations { + if migration.storeName != storeName { + continue + } + if kt == migration.ktype { + return migration.transform(key) + } + } + + return nil, fmt.Errorf("key %q is in the wrong format", string(key)) } func convertEvidence(key keyID, newPrefix int64) ([]byte, error) { @@ -374,7 +545,29 @@ func isHex(data []byte) bool { return len(data) != 0 } -func replaceKey(db dbm.DB, key keyID, gooseFn migrateFunc) error { +func getMigrationFunc(storeName string, key keyID) (*migrationDefinition, error) { + for idx := range migrations { + migration := migrations[idx] + + if migration.storeName == storeName { + if migration.prefix == nil { + return &migration, nil + } + if bytes.HasPrefix(migration.prefix, key) { + return &migration, nil + } + } + } + return nil, fmt.Errorf("no migration defined for data store %q and key %q", storeName, key) +} + +func replaceKey(db dbm.DB, storeName string, key keyID) error { + migration, err := getMigrationFunc(storeName, key) + if err != nil { + return err + } + gooseFn := migration.transform + exists, err := db.Has(key) if err != nil { return err @@ -433,8 +626,8 @@ func replaceKey(db dbm.DB, key keyID, gooseFn migrateFunc) error { // The context allows for a safe termination of the operation // (e.g connected to a singal handler,) to abort the operation // in-between migration operations. -func Migrate(ctx context.Context, db dbm.DB) error { - keys, err := getAllLegacyKeys(db) +func Migrate(ctx context.Context, storeName string, db dbm.DB) error { + keys, err := getAllLegacyKeys(db, storeName) if err != nil { return err } @@ -451,7 +644,7 @@ func Migrate(ctx context.Context, db dbm.DB) error { if err := ctx.Err(); err != nil { return err } - return replaceKey(db, key, migrateKey) + return replaceKey(db, storeName, key) }) } if g.Wait() != nil { diff --git a/scripts/keymigrate/migrate_test.go b/scripts/keymigrate/migrate_test.go index f7322b352..b2e7a4ab8 100644 --- a/scripts/keymigrate/migrate_test.go +++ b/scripts/keymigrate/migrate_test.go @@ -1,16 +1,12 @@ package keymigrate import ( - "context" - "errors" "fmt" - "math" "strings" "testing" "github.com/google/orderedcode" "github.com/stretchr/testify/require" - dbm "github.com/tendermint/tm-db" ) func makeKey(t *testing.T, elems ...interface{}) []byte { @@ -78,30 +74,6 @@ func getNewPrefixKeys(t *testing.T, val int) map[string][]byte { } } -func getLegacyDatabase(t *testing.T) (int, dbm.DB) { - db := dbm.NewMemDB() - batch := db.NewBatch() - ct := 0 - - generated := []map[string][]byte{ - getLegacyPrefixKeys(8), - getLegacyPrefixKeys(9001), - getLegacyPrefixKeys(math.MaxInt32 << 1), - getLegacyPrefixKeys(math.MaxInt64 - 8), - } - - // populate database - for _, km := range generated { - for _, key := range km { - ct++ - require.NoError(t, batch.Set(key, []byte(fmt.Sprintf(`{"value": %d}`, ct)))) - } - } - require.NoError(t, batch.WriteSync()) - require.NoError(t, batch.Close()) - return ct - (2 * len(generated)) + 2, db -} - func TestMigration(t *testing.T) { t.Run("Idempotency", func(t *testing.T) { // we want to make sure that the key space for new and @@ -113,37 +85,12 @@ func TestMigration(t *testing.T) { require.Equal(t, len(legacyPrefixes), len(newPrefixes)) - t.Run("Legacy", func(t *testing.T) { - for kind, le := range legacyPrefixes { - require.True(t, checkKeyType(le).isLegacy(), kind) - } - }) - t.Run("New", func(t *testing.T) { - for kind, ne := range newPrefixes { - require.False(t, checkKeyType(ne).isLegacy(), kind) - } - }) - t.Run("Conversion", func(t *testing.T) { - for kind, le := range legacyPrefixes { - nk, err := migrateKey(le) - require.NoError(t, err, kind) - require.False(t, checkKeyType(nk).isLegacy(), kind) - } - }) t.Run("Hashes", func(t *testing.T) { t.Run("NewKeysAreNotHashes", func(t *testing.T) { for _, key := range getNewPrefixKeys(t, 9001) { require.True(t, len(key) != 32) } }) - t.Run("ContrivedLegacyKeyDetection", func(t *testing.T) { - // length 32: should appear to be a hash - require.Equal(t, txHashKey, checkKeyType([]byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"))) - - // length ≠ 32: should not appear to be a hash - require.Equal(t, nonLegacyKey, checkKeyType([]byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--"))) - require.Equal(t, nonLegacyKey, checkKeyType([]byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"))) - }) }) }) t.Run("Migrations", func(t *testing.T) { @@ -171,72 +118,101 @@ func TestMigration(t *testing.T) { "UserKey3": []byte("foo/bar/baz/1.2/4"), } for kind, key := range table { - out, err := migrateKey(key) + out, err := migrateKey(key, "") + // TODO probably these error at the + // moment because of store missmatches require.Error(t, err, kind) require.Nil(t, out, kind) } }) - t.Run("Replacement", func(t *testing.T) { - t.Run("MissingKey", func(t *testing.T) { - db := dbm.NewMemDB() - require.NoError(t, replaceKey(db, keyID("hi"), nil)) - }) - t.Run("ReplacementFails", func(t *testing.T) { - db := dbm.NewMemDB() - key := keyID("hi") - require.NoError(t, db.Set(key, []byte("world"))) - require.Error(t, replaceKey(db, key, func(k keyID) (keyID, error) { - return nil, errors.New("hi") - })) - }) - t.Run("KeyDisappears", func(t *testing.T) { - db := dbm.NewMemDB() - key := keyID("hi") - require.NoError(t, db.Set(key, []byte("world"))) - require.Error(t, replaceKey(db, key, func(k keyID) (keyID, error) { - require.NoError(t, db.Delete(key)) - return keyID("wat"), nil - })) - - exists, err := db.Has(key) - require.NoError(t, err) - require.False(t, exists) - - exists, err = db.Has(keyID("wat")) - require.NoError(t, err) - require.False(t, exists) - }) - }) - }) - t.Run("Integration", func(t *testing.T) { - t.Run("KeyDiscovery", func(t *testing.T) { - size, db := getLegacyDatabase(t) - keys, err := getAllLegacyKeys(db) - require.NoError(t, err) - require.Equal(t, size, len(keys)) - legacyKeys := 0 - for _, k := range keys { - if checkKeyType(k).isLegacy() { - legacyKeys++ - } - } - require.Equal(t, size, legacyKeys) - }) - t.Run("KeyIdempotency", func(t *testing.T) { - for _, key := range getNewPrefixKeys(t, 84) { - require.False(t, checkKeyType(key).isLegacy()) - } - }) - t.Run("Migrate", func(t *testing.T) { - _, db := getLegacyDatabase(t) - - ctx := context.Background() - err := Migrate(ctx, db) - require.NoError(t, err) - keys, err := getAllLegacyKeys(db) - require.NoError(t, err) - require.Equal(t, 0, len(keys)) - - }) }) } + +func TestGlobalDataStructuresForRefactor(t *testing.T) { + defer func() { + if t.Failed() { + t.Log("number of migrations:", len(migrations)) + } + }() + + const unPrefixedLegacyKeys = 3 + + t.Run("MigrationsAreDefined", func(t *testing.T) { + if len(prefixes)+unPrefixedLegacyKeys != len(migrations) { + t.Fatal("migrationse are not correctly defined", + "prefixes", len(prefixes), + "migrations", len(migrations)) + } + }) + t.Run("AllMigrationsHavePrefixDefined", func(t *testing.T) { + for _, m := range migrations { + if m.prefix == nil && m.storeName != "tx_index" { + t.Errorf("migration named %q for store %q does not have a prefix defined", m.name, m.storeName) + } + } + }) + t.Run("Deduplication", func(t *testing.T) { + t.Run("Prefixes", func(t *testing.T) { + set := map[string]struct{}{} + for _, prefix := range prefixes { + set[string(prefix.prefix)] = struct{}{} + } + if len(set) != len(prefixes) { + t.Fatal("duplicate prefix definition", + "set", len(set), + "values", set) + } + }) + t.Run("MigrationName", func(t *testing.T) { + set := map[string]struct{}{} + for _, migration := range migrations { + set[migration.name] = struct{}{} + } + if len(set) != len(migrations) { + t.Fatal("duplicate migration name defined", + "set", len(set), + "values", set) + } + }) + t.Run("MigrationPrefix", func(t *testing.T) { + set := map[string]struct{}{} + for _, migration := range migrations { + set[string(migration.prefix)] = struct{}{} + } + // three keys don't have prefixes in the + // legacy system; this is fine but it means + // the set will have 1 less than expected + // (well 2 less, but the empty key takes one + // of the slots): + expectedDupl := unPrefixedLegacyKeys - 1 + + if len(set) != len(migrations)-expectedDupl { + t.Fatal("duplicate migration prefix defined", + "set", len(set), + "expected", len(migrations)-expectedDupl, + "values", set) + } + }) + t.Run("MigrationStoreName", func(t *testing.T) { + set := map[string]struct{}{} + for _, migration := range migrations { + set[migration.storeName] = struct{}{} + } + if len(set) != 5 { + t.Fatal("duplicate migration store name defined", + "set", len(set), + "values", set) + } + if _, ok := set[""]; ok { + t.Fatal("empty store name defined") + } + }) + }) + t.Run("NilPrefix", func(t *testing.T) { + _, err := getMigrationFunc("tx_index", []byte("fooo")) + if err != nil { + t.Fatal("should find an index for tx", err) + } + }) + +} From 9d1dd560e6a5423f07fdde2426c1d47ac4a22849 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Wed, 20 Jul 2022 15:28:54 -0700 Subject: [PATCH 51/58] Prepare changelog for Release v0.35.9. (#9057) --- CHANGELOG.md | 6 ++++-- CHANGELOG_PENDING.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 906321aa8..c012d3d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,16 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/cosmos). -## v0.35.9-rc0 +## v0.35.9 + +July 20, 2022 This release fixes a deadlock that could occur in some cases when using the priority mempool with the ABCI socket client. ### BUG FIXES -- [mempool] \#9030 rework lock discipline to mitigate callback deadlocks (@creachadair) +- [mempool] [\#9030](https://github.com/tendermint/tendermint/pull/9030) rework lock discipline to mitigate callback deadlocks (@creachadair) ## v0.35.8 diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 183026e46..06eed7004 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -2,7 +2,7 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/cosmos). -## v0.35.9 +## v0.35.10 Month DD, YYYY From 65feb7097b9026a33af459bdd19443b103e38ae1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Jul 2022 17:22:47 -0700 Subject: [PATCH 52/58] build(deps): Bump github.com/golangci/golangci-lint (#9045) --- go.mod | 2 +- go.sum | 31 +++++++++++++------------------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 23f180216..6e8a420c8 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/go-kit/kit v0.12.0 github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.2 - github.com/golangci/golangci-lint v1.47.0 + github.com/golangci/golangci-lint v1.47.1 github.com/google/go-cmp v0.5.8 github.com/google/orderedcode v0.0.1 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index affbe939c..52fdf979c 100644 --- a/go.sum +++ b/go.sum @@ -146,7 +146,6 @@ github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -253,8 +252,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/daixiang0/gci v0.4.2 h1:PyT/Y4a265wDhPCZo2ip/YH33M4zEuFA3nDMdAvcKSA= -github.com/daixiang0/gci v0.4.2/go.mod h1:d0f+IJhr9loBtIq+ebwhRoTt1LGbPH96ih8bKlsRT9E= +github.com/daixiang0/gci v0.4.3 h1:wf7x0xRjQqTlA2dzHTI0A/xPyp7VcBatBG9nwGatwbQ= +github.com/daixiang0/gci v0.4.3/go.mod h1:EpVfrztufwVgQRXjnX4zuNinEpLj5OmMjtu/+MB0V0c= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -451,8 +450,8 @@ github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6 github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks= github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.47.0 h1:h2s+ZGGF63fdzUtac+VYUHPsEO0ADTqHouI7Vase+FY= -github.com/golangci/golangci-lint v1.47.0/go.mod h1:3TZhfF5KolbIkXYjUFvER6G9CoxzLEaafr/u/QI1S5A= +github.com/golangci/golangci-lint v1.47.1 h1:hbubHskV2Ppwz4ZZE2lc0/Pw9ZhqLuzm2dT7ZVpLA6Y= +github.com/golangci/golangci-lint v1.47.1/go.mod h1:lpS2pjBZtRyXewUcOY7yUL3K4KfpoWz072yRN8AuhHg= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= @@ -1021,8 +1020,8 @@ github.com/sivchari/containedctx v1.0.2 h1:0hLQKpgC53OVF1VT7CeoFHk9YKstur1XOgfYI github.com/sivchari/containedctx v1.0.2/go.mod h1:PwZOeqm4/DLoJOqMSIJs3aKqXRX4YO+uXww087KZ7Bw= github.com/sivchari/nosnakecase v1.5.0 h1:ZBvAu1H3uteN0KQ0IsLpIFOwYgPEhKLyv2ahrVkub6M= github.com/sivchari/nosnakecase v1.5.0/go.mod h1:CwDzrzPea40/GB6uynrNLiorAlgFRvRbFSgJx2Gs+QY= -github.com/sivchari/tenv v1.6.0 h1:FyE4WysxLwYljKqWhTfOMjgKjBSnmzzg7lWOmpDiAcc= -github.com/sivchari/tenv v1.6.0/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= +github.com/sivchari/tenv v1.7.0 h1:d4laZMBK6jpe5PWepxlV9S+LC0yXqvYHiq8E6ceoVVE= +github.com/sivchari/tenv v1.7.0/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa h1:YJfZp12Z3AFhSBeXOlv4BO55RMwPn2NoQeDsrdWnBtY= @@ -1110,8 +1109,6 @@ github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaE github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 h1:kl4KhGNsJIbDHS9/4U9yQo1UcPQM0kOMJHn29EoH/Ro= github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= -github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= -github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -1201,7 +1198,6 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= @@ -1209,7 +1205,6 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= @@ -1217,7 +1212,6 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= @@ -1264,8 +1258,9 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 h1:FR+oGxGfbQu1d+jglI3rCkjAjUnhRSZcUxr+DqlDLNo= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d h1:+W8Qf4iJtMGKkyAygcKohjxTk4JPsL9DpzApJ22m5Ic= +golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1499,7 +1494,6 @@ golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1515,6 +1509,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220702020025-31831981b65f h1:xdsejrW/0Wf2diT5CPp3XmKUNbr7Xvw8kYilQ+6qjRY= golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -1641,8 +1636,9 @@ golang.org/x/tools v0.1.9-0.20211228192929-ee1ca4ffc4da/go.mod h1:nABZi5QlRsZVlz golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= -golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/tools v0.1.12-0.20220628192153-7743d1d949f1 h1:NHLFZ56qCjD+0hYY3kE5Wl40Z7q4Gn9Ln/7YU0lsGko= +golang.org/x/tools v0.1.12-0.20220628192153-7743d1d949f1/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1876,7 +1872,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -1900,8 +1895,8 @@ mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wp mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= -mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 h1:Jh3LAeMt1eGpxomyu3jVkmVZWW2MxZ1qIIV2TZ/nRio= -mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5/go.mod h1:b8RRCBm0eeiWR8cfN88xeq2G5SG3VKGO+5UPWi5FSOY= +mvdan.cc/unparam v0.0.0-20220706161116-678bad134442 h1:seuXWbRB1qPrS3NQnHmFKLJLtskWyueeIzmLXghMGgk= +mvdan.cc/unparam v0.0.0-20220706161116-678bad134442/go.mod h1:F/Cxw/6mVrNKqrR2YjFf5CaW0Bw4RL8RfbEf4GRggJk= pgregory.net/rapid v0.4.8 h1:d+5SGZWUbJPbl3ss6tmPFqnNeQR6VDOFly+eTjwPiEw= pgregory.net/rapid v0.4.8/go.mod h1:Z5PbWqjvWR1I3UGjvboUuan4fe4ZYEYNLNQLExzCoUs= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= From ba671c1acfa4d5b32d12d2b643939a6819d1e0d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Jul 2022 09:19:33 -0400 Subject: [PATCH 53/58] build(deps): Bump github.com/BurntSushi/toml from 1.1.0 to 1.2.0 (#9063) Bumps [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) from 1.1.0 to 1.2.0. - [Release notes](https://github.com/BurntSushi/toml/releases) - [Commits](https://github.com/BurntSushi/toml/compare/v1.1.0...v1.2.0) --- updated-dependencies: - dependency-name: github.com/BurntSushi/toml dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 6e8a420c8..5fb0981fd 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/tendermint/tendermint go 1.16 require ( - github.com/BurntSushi/toml v1.1.0 + github.com/BurntSushi/toml v1.2.0 github.com/Workiva/go-datastructures v1.0.53 github.com/adlio/schema v1.3.3 github.com/btcsuite/btcd v0.22.1 diff --git a/go.sum b/go.sum index 52fdf979c..0d70e481f 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,9 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= +github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= From d4495b862660fae752a3578c89185e56a81b9dbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Jul 2022 18:53:12 +0200 Subject: [PATCH 54/58] build(deps): Bump google.golang.org/grpc from 1.47.0 to 1.48.0 (#9060) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5fb0981fd..4a9f36711 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f - google.golang.org/grpc v1.47.0 + google.golang.org/grpc v1.48.0 gotest.tools v2.2.0+incompatible // indirect pgregory.net/rapid v0.4.8 ) diff --git a/go.sum b/go.sum index 0d70e481f..aa89ac13f 100644 --- a/go.sum +++ b/go.sum @@ -1822,8 +1822,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From 0d2bf39c23c8c12fe28f7cb7553933edf253aea1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Jul 2022 19:33:08 +0200 Subject: [PATCH 55/58] indexer: work around indexing problem for duplicate transactions (forward port: #8625) (#8950) --- CHANGELOG_PENDING.md | 2 + internal/state/indexer/indexer_service.go | 53 +++++- .../state/indexer/indexer_service_test.go | 159 ++++++++++++++++++ 3 files changed, 213 insertions(+), 1 deletion(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 06eed7004..b02ab5343 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -24,4 +24,6 @@ Special thanks to external contributors on this release: ### IMPROVEMENTS +- (indexer) \#8625 Fix overriding tx index of duplicated txs. + ### BUG FIXES diff --git a/internal/state/indexer/indexer_service.go b/internal/state/indexer/indexer_service.go index c00b1e54b..9715be876 100644 --- a/internal/state/indexer/indexer_service.go +++ b/internal/state/indexer/indexer_service.go @@ -4,6 +4,7 @@ import ( "context" "time" + abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/libs/service" "github.com/tendermint/tendermint/types" @@ -108,7 +109,15 @@ func (is *Service) OnStart() error { if len(batch.Ops) > 0 { start := time.Now() - err := sink.IndexTxEvents(batch.Ops) + + var err error + batch.Ops, err = DeduplicateBatch(batch.Ops, sink) + if err != nil { + is.Logger.Error("failed to deduplicate batch", "height", height, "error", err, "sink", sink.Type()) + continue + } + + err = sink.IndexTxEvents(batch.Ops) if err != nil { is.Logger.Error("failed to index block txs", "height", height, "err", err) } else { @@ -167,3 +176,45 @@ func IndexingEnabled(sinks []EventSink) bool { return false } + +// DeduplicateBatch consider the case of duplicate txs. +// if the current one under investigation is NOT OK, then we need to check +// whether there's a previously indexed tx. +// SKIP the current tx if the previously indexed record is found and successful. +func DeduplicateBatch(ops []*abci.TxResult, sink EventSink) ([]*abci.TxResult, error) { + result := make([]*abci.TxResult, 0, len(ops)) + + // keep track of successful txs in this block in order to suppress latter ones being indexed. + var successfulTxsInThisBlock = make(map[string]struct{}) + + for _, txResult := range ops { + hash := types.Tx(txResult.Tx).Hash() + + if txResult.Result.IsOK() { + successfulTxsInThisBlock[string(hash)] = struct{}{} + } else { + // if it already appeared in current block and was successful, skip. + if _, found := successfulTxsInThisBlock[string(hash)]; found { + continue + } + + // check if this tx hash is already indexed + old, err := sink.GetTxByHash(hash) + + // if db op errored + // Not found is not an error + if err != nil { + return nil, err + } + + // if it's already indexed in an older block and was successful, skip. + if old != nil && old.Result.Code == abci.CodeTypeOK { + continue + } + } + + result = append(result, txResult) + } + + return result, nil +} diff --git a/internal/state/indexer/indexer_service_test.go b/internal/state/indexer/indexer_service_test.go index 2ca6d168b..20fbedd8f 100644 --- a/internal/state/indexer/indexer_service_test.go +++ b/internal/state/indexer/indexer_service_test.go @@ -112,6 +112,165 @@ func TestIndexerServiceIndexesBlocks(t *testing.T) { assert.Nil(t, teardown(t, pool)) } +func TestTxIndexDuplicatedTx(t *testing.T) { + var mockTx = types.Tx("MOCK_TX_HASH") + + testCases := []struct { + name string + tx1 abci.TxResult + tx2 abci.TxResult + expSkip bool // do we expect the second tx to be skipped by tx indexer + }{ + {"skip, previously successful", + abci.TxResult{ + Height: 1, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK, + }, + }, + abci.TxResult{ + Height: 2, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK + 1, + }, + }, + true, + }, + {"not skip, previously unsuccessful", + abci.TxResult{ + Height: 1, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK + 1, + }, + }, + abci.TxResult{ + Height: 2, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK + 1, + }, + }, + false, + }, + {"not skip, both successful", + abci.TxResult{ + Height: 1, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK, + }, + }, + abci.TxResult{ + Height: 2, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK, + }, + }, + false, + }, + {"not skip, both unsuccessful", + abci.TxResult{ + Height: 1, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK + 1, + }, + }, + abci.TxResult{ + Height: 2, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK + 1, + }, + }, + false, + }, + {"skip, same block, previously successful", + abci.TxResult{ + Height: 1, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK, + }, + }, + abci.TxResult{ + Height: 1, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK + 1, + }, + }, + true, + }, + {"not skip, same block, previously unsuccessful", + abci.TxResult{ + Height: 1, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK + 1, + }, + }, + abci.TxResult{ + Height: 1, + Index: 0, + Tx: mockTx, + Result: abci.ResponseDeliverTx{ + Code: abci.CodeTypeOK, + }, + }, + false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sink := kv.NewEventSink(dbm.NewMemDB()) + + if tc.tx1.Height != tc.tx2.Height { + // index the first tx + err := sink.IndexTxEvents([]*abci.TxResult{&tc.tx1}) + require.NoError(t, err) + + // check if the second one should be skipped. + ops, err := indexer.DeduplicateBatch([]*abci.TxResult{&tc.tx2}, sink) + require.NoError(t, err) + + if tc.expSkip { + require.Empty(t, ops) + } else { + require.Equal(t, []*abci.TxResult{&tc.tx2}, ops) + } + } else { + // same block + ops := []*abci.TxResult{&tc.tx1, &tc.tx2} + ops, err := indexer.DeduplicateBatch(ops, sink) + require.NoError(t, err) + if tc.expSkip { + // the second one is skipped + require.Equal(t, []*abci.TxResult{&tc.tx1}, ops) + } else { + require.Equal(t, []*abci.TxResult{&tc.tx1, &tc.tx2}, ops) + } + } + }) + } +} + func readSchema() ([]*schema.Migration, error) { filename := "./sink/psql/schema.sql" contents, err := ioutil.ReadFile(filename) From a4ce134c9379e6f1e5e36ddb469a633984ccc6fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Jul 2022 15:30:09 +0200 Subject: [PATCH 56/58] build(deps): Bump github.com/bufbuild/buf from 1.3.1 to 1.6.0 (#9064) --- go.mod | 2 +- go.sum | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 4a9f36711..632b8728e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/adlio/schema v1.3.3 github.com/btcsuite/btcd v0.22.1 github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce - github.com/bufbuild/buf v1.3.1 + github.com/bufbuild/buf v1.6.0 github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/creachadair/atomicfile v0.2.6 github.com/creachadair/taskgroup v0.3.2 diff --git a/go.sum b/go.sum index aa89ac13f..6686452cb 100644 --- a/go.sum +++ b/go.sum @@ -181,8 +181,10 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/bufbuild/buf v1.3.1 h1:AelWcENnbNEjwxmQXIZaU51GHgnWQ8Mc94kZdDUKgRs= -github.com/bufbuild/buf v1.3.1/go.mod h1:CTRUb23N+zlm1U8ZIBKz0Sqluk++qQloB2i/MZNZHIs= +github.com/bufbuild/buf v1.6.0 h1:VXHJ+n3NYzpDH4Ysuj9ivVf23Xk/NAxcZPbAAFv4EDs= +github.com/bufbuild/buf v1.6.0/go.mod h1:23u11F6tmCF9sSB/dz7ybxxJ5q6qmWY0Lb4E4TR1rqw= +github.com/bufbuild/connect-go v0.1.1 h1:EANBP3Vrk+MH08bfOJQ07FAtA5p4wQB5ouCXYm3LxGE= +github.com/bufbuild/connect-go v0.1.1/go.mod h1:4efZ2eXFENwd4p7tuLaL9m0qtTsCOzuBvrohvRGevDM= github.com/butuzov/ireturn v0.1.1 h1:QvrO2QF2+/Cx1WA/vETCIYBKtRjc30vesdoPUNo1EbY= github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= @@ -341,6 +343,8 @@ github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3n github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-critic/go-critic v0.6.3 h1:abibh5XYBTASawfTQ0rA7dVtQT+6KzpGqb/J+DxRDaw= github.com/go-critic/go-critic v0.6.3/go.mod h1:c6b3ZP1MQ7o6lPR7Rv3lEf7pYQUmAcx8ABHgdZCQt/k= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -632,11 +636,15 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jgautheron/goconst v1.5.1 h1:HxVbL1MhydKs8R8n/HE5NPvzfaYmQJA3o879lE4+WcM= github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= github.com/jhump/protocompile v0.0.0-20220216033700-d705409f108f h1:BNuUg9k2EiJmlMwjoef3e8vZLHplbVw6DrjGFjLL+Yo= github.com/jhump/protocompile v0.0.0-20220216033700-d705409f108f/go.mod h1:qr2b5kx4HbFS7/g4uYO5qv9ei8303JMsC7ESbYiqr2Q= github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= -github.com/jhump/protoreflect v1.11.1-0.20220213155251-0c2aedc66cf4 h1:E2CdxLXYSn6Zrj2+u8DWrwMJW3YZLSWtM/7kIL8OL18= -github.com/jhump/protoreflect v1.11.1-0.20220213155251-0c2aedc66cf4/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= +github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= +github.com/jhump/protoreflect v1.12.1-0.20220417024638-438db461d753 h1:uFlcJKZPLQd7rmOY/RrvBuUaYmAFnlFHKLivhO6cOy8= +github.com/jhump/protoreflect v1.12.1-0.20220417024638-438db461d753/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= @@ -679,8 +687,8 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= -github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY= +github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -1083,6 +1091,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -1785,8 +1794,9 @@ google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad h1:kqrS+lhvaMHCxul6sKQvKJ8nAAhlVItmZV822hYFH/U= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1822,6 +1832,7 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= From ed4d0de5597350edc74f365e5ab0790653647862 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 10:34:58 +0200 Subject: [PATCH 57/58] build(deps): Bump github.com/golangci/golangci-lint (#9069) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 632b8728e..b9b0bef5f 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/go-kit/kit v0.12.0 github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.2 - github.com/golangci/golangci-lint v1.47.1 + github.com/golangci/golangci-lint v1.47.2 github.com/google/go-cmp v0.5.8 github.com/google/orderedcode v0.0.1 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index 6686452cb..532b342d1 100644 --- a/go.sum +++ b/go.sum @@ -455,8 +455,8 @@ github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6 github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks= github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.47.1 h1:hbubHskV2Ppwz4ZZE2lc0/Pw9ZhqLuzm2dT7ZVpLA6Y= -github.com/golangci/golangci-lint v1.47.1/go.mod h1:lpS2pjBZtRyXewUcOY7yUL3K4KfpoWz072yRN8AuhHg= +github.com/golangci/golangci-lint v1.47.2 h1:qvMDVv49Hrx3PSEXZ0bD/yhwSbhsOihQjFYCKieegIw= +github.com/golangci/golangci-lint v1.47.2/go.mod h1:lpS2pjBZtRyXewUcOY7yUL3K4KfpoWz072yRN8AuhHg= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= From b4eaccd242b875511dc758751fe375348300ebbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 07:03:29 -0700 Subject: [PATCH 58/58] build(deps): Bump github.com/creachadair/tomledit from 0.0.22 to 0.0.23 (#9085) Bumps [github.com/creachadair/tomledit](https://github.com/creachadair/tomledit) from 0.0.22 to 0.0.23. - [Release notes](https://github.com/creachadair/tomledit/releases) - [Commits](https://github.com/creachadair/tomledit/compare/v0.0.22...v0.0.23) --- updated-dependencies: - dependency-name: github.com/creachadair/tomledit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b9b0bef5f..b55afc5ed 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/creachadair/atomicfile v0.2.6 github.com/creachadair/taskgroup v0.3.2 - github.com/creachadair/tomledit v0.0.22 + github.com/creachadair/tomledit v0.0.23 github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect diff --git a/go.sum b/go.sum index 532b342d1..7cc093a14 100644 --- a/go.sum +++ b/go.sum @@ -249,8 +249,8 @@ github.com/creachadair/atomicfile v0.2.6/go.mod h1:BRq8Une6ckFneYXZQ+kO7p1ZZP3I2 github.com/creachadair/command v0.0.0-20220426235536-a748effdf6a1/go.mod h1:bAM+qFQb/KwWyCc9MLC4U1jvn3XyakqP5QRkds5T6cY= github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6VqkYlkM= github.com/creachadair/taskgroup v0.3.2/go.mod h1:wieWwecHVzsidg2CsUnFinW1faVN4+kq+TDlRJQ0Wbk= -github.com/creachadair/tomledit v0.0.22 h1:lRtepmrwhzDq+g1gv5ftVn5itgo7CjYbm6abKTToqJ4= -github.com/creachadair/tomledit v0.0.22/go.mod h1:cIu/4x5L855oSRejIqr+WRFh+mv9g4fWLiUFaApYn/Y= +github.com/creachadair/tomledit v0.0.23 h1:ohYJjMsxwzj4dDzKaBWFbWH5J+3LO/8CYnlVY+baBWA= +github.com/creachadair/tomledit v0.0.23/go.mod h1:cIu/4x5L855oSRejIqr+WRFh+mv9g4fWLiUFaApYn/Y= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=

#_$y479`m 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 yG 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