A very experimental PLC implementation which uses BFT consensus for decentralization
19
fork

Configure Feed

Select the types of activity you want to include in your feed.

Apply logistic curve to validator voting powers and eliminate potentially non-deterministic floating point math

gbl08ma 723c38e7 70da204d

+148 -25
+145 -25
abciapp/tx_epoch.go
··· 12 12 "github.com/cometbft/cometbft/crypto/ed25519" 13 13 protocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" 14 14 "github.com/gbl08ma/stacktrace" 15 + "github.com/govalues/decimal" 15 16 cbornode "github.com/ipfs/go-ipld-cbor" 17 + "github.com/samber/lo" 16 18 "tangled.org/gbl08ma.com/didplcbft/store" 17 19 ) 18 20 19 21 const UpdateValidatorsBlockInterval = 10000 20 22 const MaxActiveValidators = 50 21 - const MinReputationForBecomingValidator = 20000 23 + const MinReputationForBecomingValidator = (ReputationGainPerProvenBlock - ReputationEntropyLossPerBlock) * 60 * 60 * 24 // must solve challenges for about one day, assuming one block per second 22 24 23 25 func init() { 24 26 store.Consensus.ConfigureEpochSize(UpdateValidatorsBlockInterval) ··· 41 43 cbornode.RegisterCborType(Transaction[UpdateValidatorsArguments]{}) 42 44 } 43 45 44 - func computeVotingPowerFromReputation(reputation uint64) uint64 { 46 + var LogisticL = decimal.MustNew(10000000000000, 0) 47 + var LogisticNegK = lo.Must(decimal.NewFromInt64(0, 2, 8)).Neg() // -0.00000002 (already negated!) 48 + var LogisticX0 = lo.Must(decimal.New(60*60*24*365*8, 0)) // LogisticNegK and LogisticX0 were obtained based on vibes; for a target block interval of 1s, validator voting power is expected to plateau after about 1.5 years 49 + var LogisticOne = decimal.MustNew(1, 0) 50 + var LogisticZeroAdjust = decimal.MustNew(63954022804, 0) 51 + 52 + func computeVotingPowerFromReputation(reputation uint64) (uint64, error) { 45 53 if reputation < MinReputationForBecomingValidator { 46 - return 0 54 + return 0, nil 55 + } 56 + 57 + reputation = reputation - MinReputationForBecomingValidator 58 + 59 + rep, err := decimal.New(int64(reputation), 0) 60 + if err != nil { 61 + return 0, stacktrace.Propagate(err) 62 + } 63 + paren, err := rep.Sub(LogisticX0) 64 + if err != nil { 65 + return 0, stacktrace.Propagate(err) 66 + } 67 + exponent, err := paren.Mul(LogisticNegK) 68 + if err != nil { 69 + return 0, stacktrace.Propagate(err) 70 + } 71 + exponent, err = exponent.Exp() 72 + if err != nil { 73 + return 0, stacktrace.Propagate(err) 74 + } 75 + 76 + denominator, err := exponent.Add(LogisticOne) 77 + if err != nil { 78 + return 0, stacktrace.Propagate(err) 79 + } 80 + 81 + votingPower, err := LogisticL.Quo(denominator) 82 + if err != nil { 83 + return 0, stacktrace.Propagate(err) 84 + } 85 + 86 + votingPower, err = votingPower.Sub(LogisticZeroAdjust) 87 + if err != nil { 88 + return 0, stacktrace.Propagate(err) 89 + } 90 + 91 + // just for the voting power values to continue to see some insignificant movement after the top of the logistical curve is reached 92 + votingPower, err = votingPower.Add(rep) 93 + if err != nil { 94 + return 0, stacktrace.Propagate(err) 95 + } 96 + 97 + whole, _, ok := votingPower.Int64(0) 98 + if !ok { 99 + return 0, stacktrace.NewError("voting power can't be represented as a pair of int64") 47 100 } 48 - return reputation - MinReputationForBecomingValidator // TODO design and apply S-curve 101 + 102 + return uint64(whole), nil 49 103 } 104 + 105 + var updateValidatorsBlockIntervalDecimal = decimal.MustNew(UpdateValidatorsBlockInterval, 0) 50 106 51 107 func processUpdateValidatorsTx(ctx context.Context, deps TransactionProcessorDependencies, txBytes []byte) (*processResult, error) { 52 108 _, err := UnmarshalTransaction[UpdateValidatorsArguments](txBytes) ··· 96 152 } 97 153 98 154 voteCount, hasVotingPower := oldActiveValidatorSet[[store.PublicKeyLength]byte(validatorPubKey)] 99 - votesIncludedInFraction := float64(voteCount) / float64(UpdateValidatorsBlockInterval) 100 155 101 - decrease := computeReputationDecrease(uint64(deps.workingHeight), reputation, rangeChallengeCompletion, votesIncludedInFraction, hasVotingPower) 156 + decrease, err := computeReputationDecrease(uint64(deps.workingHeight), reputation, rangeChallengeCompletion, voteCount, hasVotingPower) 157 + if err != nil { 158 + return 0, stacktrace.Propagate(err) 159 + } 160 + 102 161 if decrease > reputation { 103 162 reputation = 0 104 163 } else { 105 164 reputation -= decrease 106 165 } 107 166 108 - votingPower := computeVotingPowerFromReputation(reputation) 167 + votingPower, err := computeVotingPowerFromReputation(reputation) 168 + if err != nil { 169 + return 0, stacktrace.Propagate(err) 170 + } 171 + 109 172 if votingPower > 0 { 110 173 vwvp := validatorWithVotingPower{ 111 174 validatorPubKey: validatorPubKey, ··· 114 177 115 178 if valHeap.Len() < MaxActiveValidators { 116 179 heap.Push(&valHeap, vwvp) 117 - } else if votingPower > valHeap[0].votingPower { 180 + } else if valHeap[0].less(vwvp) { 118 181 heap.Pop(&valHeap) 119 182 heap.Push(&valHeap, vwvp) 120 183 } ··· 194 257 votingPower uint64 195 258 } 196 259 260 + func (v1 validatorWithVotingPower) less(v2 validatorWithVotingPower) bool { 261 + if v1.votingPower == v2.votingPower { 262 + return bytes.Compare(v1.validatorPubKey, v2.validatorPubKey) < 0 263 + } 264 + return v1.votingPower < v2.votingPower 265 + } 266 + 197 267 type validatorHeap []validatorWithVotingPower 198 268 199 - func (h validatorHeap) Len() int { return len(h) } 200 - func (h validatorHeap) Less(i, j int) bool { return h[i].votingPower < h[j].votingPower } 201 - func (h validatorHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 269 + func (h validatorHeap) Len() int { return len(h) } 270 + func (h validatorHeap) Less(i, j int) bool { 271 + return h[i].less(h[j]) 272 + } 273 + func (h validatorHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 202 274 203 275 func (h *validatorHeap) Push(x any) { 204 276 *h = append(*h, x.(validatorWithVotingPower)) ··· 229 301 return out, nil 230 302 } 231 303 232 - func computeReputationDecrease(workingHeight uint64, reputation uint64, rangeChallengeCompletion uint64, voteInclusionFrequency float64, validatorHasVotingPower bool) uint64 { 304 + var pointZero3 = decimal.MustNew(3, 2) 305 + var maxMissedRangeChallengePenaltyMul = decimal.MustNew(15, 2) 306 + 307 + var pointOne = decimal.MustNew(1, 1) 308 + var pointSeven = decimal.MustNew(7, 1) 309 + 310 + func computeReputationDecrease(workingHeight uint64, reputation uint64, rangeChallengeCompletion uint64, voteCount uint64, validatorHasVotingPower bool) (uint64, error) { 233 311 const expectedGainForCompletelyActiveValidator = ReputationGainPerProvenBlock * UpdateValidatorsBlockInterval 234 312 entropyLoss := ReputationEntropyLossPerBlock * UpdateValidatorsBlockInterval 235 313 ··· 243 321 244 322 rangeChallengeMissedEpochs := (workingHeight - rangeChallengeCompletion) / UpdateValidatorsBlockInterval 245 323 // allow for missing one epoch without penalty 246 - missedRangeChallengePenalty := float64(max(0, int64(rangeChallengeMissedEpochs)-1)) * 0.03 247 - if missedRangeChallengePenalty > 0.15 { 248 - // avoid a too sharp drop off 249 - missedRangeChallengePenalty = 0.15 324 + missedRangeChallengePenalty, err := decimal.MustNew(max(0, int64(rangeChallengeMissedEpochs)-1), 0).Mul(pointZero3) 325 + if err != nil { 326 + return 0, stacktrace.Propagate(err) 250 327 } 251 - if missedRangeChallengePenalty > 0 { 252 - penaltyInt := uint64(float64(reputation) * missedRangeChallengePenalty) 253 - if reputation < penaltyInt { 254 - return 0 328 + // avoid a too sharp drop off 329 + missedRangeChallengePenalty = missedRangeChallengePenalty.Min(maxMissedRangeChallengePenaltyMul) 330 + 331 + if missedRangeChallengePenalty.IsPos() { 332 + rep, err := decimal.New(int64(reputation), 0) 333 + if err != nil { 334 + return 0, stacktrace.Propagate(err) 335 + } 336 + 337 + penalty, err := rep.Mul(missedRangeChallengePenalty) 338 + if err != nil { 339 + return 0, stacktrace.Propagate(err) 340 + } 341 + 342 + penaltyInt, _, ok := penalty.Int64(0) 343 + if !ok { 344 + return 0, stacktrace.NewError("penalty not representable as an int64") 345 + } 346 + if reputation < uint64(penaltyInt) { 347 + return 0, nil 255 348 } 256 - decrease += penaltyInt 349 + decrease += uint64(penaltyInt) 257 350 } 258 351 259 352 // penalize active validators that haven't been voting ··· 263 356 // 2. MarkValidatorVote only runs on FinalizeBlock, which is called after the epoch transaction has been processed 264 357 // 3. Validator updates take a few blocks to fully take effect, meaning validators might only become (in)active after the epoch transaction has been processed 265 358 if validatorHasVotingPower { 359 + voteCountDecimal := decimal.MustNew(int64(voteCount), 0) 360 + voteInclusionFrequency, err := voteCountDecimal.Quo(updateValidatorsBlockIntervalDecimal) 361 + if err != nil { 362 + return 0, stacktrace.Propagate(err) 363 + } 364 + 266 365 switch { 267 - case voteInclusionFrequency < 0.1: 366 + case voteInclusionFrequency.Less(pointOne): 268 367 decrease += 5 * expectedGainForCompletelyActiveValidator 269 - case voteInclusionFrequency < 0.7: 270 - decrease += uint64(5 * float64(expectedGainForCompletelyActiveValidator) * (0.7 - voteInclusionFrequency) / 0.7) 368 + case voteInclusionFrequency.Less(pointSeven): 369 + dec, err := decimal.New(5*expectedGainForCompletelyActiveValidator, 0) 370 + if err != nil { 371 + return 0, stacktrace.Propagate(err) 372 + } 373 + 374 + penaltyFrac, err := pointSeven.Sub(voteInclusionFrequency) 375 + if err != nil { 376 + return 0, stacktrace.Propagate(err) 377 + } 378 + penaltyFrac, err = penaltyFrac.Quo(pointSeven) 379 + if err != nil { 380 + return 0, stacktrace.Propagate(err) 381 + } 382 + 383 + dec, err = dec.Mul(penaltyFrac) 384 + if err != nil { 385 + return 0, stacktrace.Propagate(err) 386 + } 387 + 388 + decInt, _, _ := dec.Int64(0) 389 + 390 + decrease += uint64(decInt) 271 391 } 272 392 } 273 393 274 - return decrease 394 + return decrease, nil 275 395 }
+1
go.mod
··· 69 69 github.com/google/orderedcode v0.0.1 // indirect 70 70 github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect 71 71 github.com/gorilla/websocket v1.5.3 // indirect 72 + github.com/govalues/decimal v0.1.36 // indirect 72 73 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 73 74 github.com/hashicorp/hcl v1.0.0 // indirect 74 75 github.com/ingonyama-zk/icicle-gnark/v3 v3.2.2 // indirect
+2
go.sum
··· 151 151 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 152 152 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 153 153 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 154 + github.com/govalues/decimal v0.1.36 h1:dojDpsSvrk0ndAx8+saW5h9WDIHdWpIwrH/yhl9olyU= 155 + github.com/govalues/decimal v0.1.36/go.mod h1:Ee7eI3Llf7hfqDZtpj8Q6NCIgJy1iY3kH1pSwDrNqlM= 154 156 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 155 157 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 156 158 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=