github.com/MagHErmit/tendermint@v0.282.1/PHILOSOPHY.md (about) 1 # Design goals 2 3 The design goals for Tendermint (and the SDK and related libraries) are: 4 5 * Simplicity and Legibility 6 * Parallel performance, namely ability to utilize multicore architecture 7 * Ability to evolve the codebase bug-free 8 * Debuggability 9 * Complete correctness that considers all edge cases, esp in concurrency 10 * Future-proof modular architecture, message protocol, APIs, and encapsulation 11 12 13 ## Justification 14 15 Legibility is key to maintaining bug-free software as it evolves toward more 16 optimizations, more ease of debugging, and additional features. 17 18 It is too easy to introduce bugs over time by replacing lines of code with 19 those that may panic, which means ideally locks are unlocked by defer 20 statements. 21 22 For example, 23 24 ```go 25 func (obj *MyObj) something() { 26 mtx.Lock() 27 obj.something = other 28 mtx.Unlock() 29 } 30 ``` 31 32 It is too easy to refactor the codebase in the future to replace `other` with 33 `other.String()` for example, and this may introduce a bug that causes a 34 deadlock. So as much as reasonably possible, we need to be using defer 35 statements, even though it introduces additional overhead. 36 37 If it is necessary to optimize the unlocking of mutex locks, the solution is 38 more modularity via smaller functions, so that defer'd unlocks are scoped 39 within a smaller function. 40 41 Similarly, idiomatic for-loops should always be preferred over those that use 42 custom counters, because it is too easy to evolve the body of a for-loop to 43 become more complicated over time, and it becomes more and more difficult to 44 assess the correctness of such a for-loop by visual inspection. 45 46 47 ## On performance 48 49 It doesn't matter whether there are alternative implementations that are 2x or 50 3x more performant, when the software doesn't work, deadlocks, or if bugs 51 cannot be debugged. By taking advantage of multicore concurrency, the 52 Tendermint implementation will at least be an order of magnitude within the 53 range of what is theoretically possible. The design philosophy of Tendermint, 54 and the choice of Go as implementation language, is designed to make Tendermint 55 implementation the standard specification for concurrent BFT software. 56 57 By focusing on the message protocols (e.g. ABCI, p2p messages), and 58 encapsulation e.g. IAVL module, (relatively) independent reactors, we are both 59 implementing a standard implementation to be used as the specification for 60 future implementations in more optimizable languages like Rust, Java, and C++; 61 as well as creating sufficiently performant software. Tendermint Core will 62 never be as fast as future implementations of the Tendermint Spec, because Go 63 isn't designed to be as fast as possible. The advantage of using Go is that we 64 can develop the whole stack of modular components **faster** than in other 65 languages. 66 67 Furthermore, the real bottleneck is in the application layer, and it isn't 68 necessary to support more than a sufficiently decentralized set of validators 69 (e.g. 100 ~ 300 validators is sufficient, with delegated bonded PoS). 70 71 Instead of optimizing Tendermint performance down to the metal, lets focus on 72 optimizing on other matters, namely ability to push feature complete software 73 that works well enough, can be debugged and maintained, and can serve as a spec 74 for future implementations. 75 76 77 ## On encapsulation 78 79 In order to create maintainable, forward-optimizable software, it is critical 80 to develop well-encapsulated objects that have well understood properties, and 81 to re-use these easy-to-use-correctly components as building blocks for further 82 encapsulated meta-objects. 83 84 For example, mutexes are cheap enough for Tendermint's design goals when there 85 isn't goroutine contention, so it is encouraged to create concurrency safe 86 structures with struct-level mutexes. If they are used in the context of 87 non-concurrent logic, then the performance is good enough. If they are used in 88 the context of concurrent logic, then it will still perform correctly. 89 90 Examples of this design principle can be seen in the types.ValidatorSet struct, 91 and the rand.Rand struct. It's one single struct declaration that can be used 92 in both concurrent and non-concurrent logic, and due to its well encapsulation, 93 it's easy to get the usage of the mutex right. 94 95 ### example: rand.Rand 96 97 `The default Source is safe for concurrent use by multiple goroutines, but 98 Sources created by NewSource are not`. The reason why the default 99 package-level source is safe for concurrent use is because it is protected (see 100 `lockedSource` in <https://golang.org/src/math/rand/rand.go>). 101 102 But we shouldn't rely on the global source, we should be creating our own 103 Rand/Source instances and using them, especially for determinism in testing. 104 So it is reasonable to have rand.Rand be protected by a mutex. Whether we want 105 our own implementation of Rand is another question, but the answer there is 106 also in the affirmative. Sometimes you want to know where Rand is being used 107 in your code, so it becomes a simple matter of dropping in a log statement to 108 inject inspectability into Rand usage. Also, it is nice to be able to extend 109 the functionality of Rand with custom methods. For these reasons, and for the 110 reasons which is outlined in this design philosophy document, we should 111 continue to use the rand.Rand object, with mutex protection. 112 113 Another key aspect of good encapsulation is the choice of exposed vs unexposed 114 methods. It should be clear to the reader of the code, which methods are 115 intended to be used in what context, and what safe usage is. Part of this is 116 solved by hiding methods via unexported methods. Another part of this is 117 naming conventions on the methods (e.g. underscores) with good documentation, 118 and code organization. If there are too many exposed methods and it isn't 119 clear what methods have what side effects, then there is something wrong about 120 the design of abstractions that should be revisited. 121 122 123 ## On concurrency 124 125 In order for Tendermint to remain relevant in the years to come, it is vital 126 for Tendermint to take advantage of multicore architectures. Due to the nature 127 of the problem, namely consensus across a concurrent p2p gossip network, and to 128 handle RPC requests for a large number of consuming subscribers, it is 129 unavoidable for Tendermint development to require expertise in concurrency 130 design, especially when it comes to the reactor design, and also for RPC 131 request handling. 132 133 134 # Guidelines 135 136 Here are some guidelines for designing for (sufficient) performance and concurrency: 137 138 * Mutex locks are cheap enough when there isn't contention. 139 * Do not optimize code without analytical or observed proof that it is in a hot path. 140 * Don't over-use channels when mutex locks w/ encapsulation are sufficient. 141 * The need to drain channels are often a hint of unconsidered edge cases. 142 * The creation of O(N) one-off goroutines is generally technical debt that 143 needs to get addressed sooner than later. Avoid creating too many 144 goroutines as a patch around incomplete concurrency design, or at least be 145 aware of the debt and do not invest in the debt. On the other hand, Tendermint 146 is designed to have a limited number of peers (e.g. 10 or 20), so the creation 147 of O(C) goroutines per O(P) peers is still O(C\*P=constant). 148 * Use defer statements to unlock as much as possible. If you want to unlock sooner, 149 try to create more modular functions that do make use of defer statements. 150 151 # Mantras 152 153 * Premature optimization kills 154 * Readability is paramount 155 * Beautiful is better than fast. 156 * In the face of ambiguity, refuse the temptation to guess. 157 * In the face of bugs, refuse the temptation to cover the bug. 158 * There should be one-- and preferably only one --obvious way to do it.