A very experimental PLC implementation which uses BFT consensus for decentralization
at main 336 lines 11 kB view raw
1package abciapp 2 3import ( 4 "bytes" 5 "context" 6 "embed" 7 "errors" 8 "math/big" 9 "time" 10 11 "github.com/Yiling-J/theine-go" 12 "github.com/cometbft/cometbft/crypto" 13 cmtlog "github.com/cometbft/cometbft/libs/log" 14 "github.com/consensys/gnark-crypto/ecc" 15 "github.com/consensys/gnark-crypto/ecc/bn254" 16 "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" 17 "github.com/consensys/gnark/backend/groth16" 18 "github.com/consensys/gnark/backend/witness" 19 "github.com/consensys/gnark/constraint" 20 "github.com/consensys/gnark/frontend" 21 gnarklogger "github.com/consensys/gnark/logger" 22 "github.com/gbl08ma/stacktrace" 23 "github.com/rs/zerolog" 24 "github.com/samber/lo" 25 "resenje.org/singleflight" 26 "tangled.org/gbl08ma.com/didplcbft/proof" 27 "tangled.org/gbl08ma.com/didplcbft/store" 28 "tangled.org/gbl08ma.com/didplcbft/transaction" 29) 30 31//go:embed proofcircuit/BlockChallenge_* 32var blockChallengeCircuitFS embed.FS 33 34var blockChallengeConstraintSystem constraint.ConstraintSystem 35var blockChallengeProvingKey groth16.ProvingKey 36var blockChallengeVerifyingKey groth16.VerifyingKey 37 38func init() { 39 blockChallengeConstraintSystem = groth16.NewCS(ecc.BN254) 40 csFile := lo.Must(blockChallengeCircuitFS.Open("proofcircuit/BlockChallenge_ConstraintSystem")) 41 defer csFile.Close() 42 lo.Must(blockChallengeConstraintSystem.ReadFrom(csFile)) 43 44 blockChallengeProvingKey = groth16.NewProvingKey(ecc.BN254) 45 pkFile := lo.Must(blockChallengeCircuitFS.Open("proofcircuit/BlockChallenge_ProvingKey")) 46 defer pkFile.Close() 47 lo.Must(blockChallengeProvingKey.ReadFrom(pkFile)) 48 49 blockChallengeVerifyingKey = groth16.NewVerifyingKey(ecc.BN254) 50 vkFile := lo.Must(blockChallengeCircuitFS.Open("proofcircuit/BlockChallenge_VerifyingKey")) 51 defer vkFile.Close() 52 lo.Must(blockChallengeVerifyingKey.ReadFrom(vkFile)) 53 54 gnarklogger.Set(zerolog.Nop()) 55} 56 57type blockChallengeCoordinator struct { 58 g singleflight.Group[int64, []byte] 59 60 runnerContext context.Context 61 logger cmtlog.Logger 62 63 isConfiguredToBeValidator bool 64 validatorAddress []byte 65 txFactory *transaction.Factory 66 blockHeaderGetter store.BlockHeaderGetter 67 68 sharedWitnessDataCache *theine.LoadingCache[int64, proof.BlockChallengeCircuit] 69} 70 71func newBlockChallengeCoordinator(runnerContext context.Context, logger cmtlog.Logger, txFactory *transaction.Factory, headerGetter store.BlockHeaderGetter, pubKey crypto.PubKey) (*blockChallengeCoordinator, error) { 72 c := &blockChallengeCoordinator{ 73 runnerContext: runnerContext, 74 logger: logger, 75 txFactory: txFactory, 76 blockHeaderGetter: headerGetter, 77 isConfiguredToBeValidator: pubKey != nil, 78 } 79 if c.isConfiguredToBeValidator { 80 c.validatorAddress = pubKey.Address() 81 } 82 83 var err error 84 c.sharedWitnessDataCache, err = theine.NewBuilder[int64, proof.BlockChallengeCircuit](5). 85 Loading( 86 func(ctx context.Context, height int64) (theine.Loaded[proof.BlockChallengeCircuit], error) { 87 var zeroValue theine.Loaded[proof.BlockChallengeCircuit] 88 89 anyTx := ctx.Value(contextTxKey{}) 90 if anyTx == nil { 91 return zeroValue, stacktrace.NewError("transaction not found in context") 92 } 93 tx, ok := anyTx.(transaction.Read) 94 if !ok { 95 return zeroValue, stacktrace.NewError("invalid transaction in context") 96 } 97 98 operationData, lastCommitHash, err := c.blockChallengeOperationDataForHeight(tx, height) 99 if err != nil { 100 return zeroValue, stacktrace.Propagate(err) 101 } 102 103 sharedPart := buildBlockChallengeCircuitAssignmentShared(lastCommitHash, [proof.OperationDataLength]byte(operationData)) 104 105 return theine.Loaded[proof.BlockChallengeCircuit]{ 106 Value: sharedPart, 107 }, nil 108 }).Build() 109 if err != nil { 110 return nil, stacktrace.Propagate(err) 111 } 112 113 return c, nil 114} 115 116type contextTxKey struct{} 117 118// in challengeManager method arguments, `height` is always the height of the "current" block C which uses data from block C-1: 119// - the consensus tree from block C-1 120// - the lastCommitHash from block C-1 121// to solve/prove the challenge for block C, which gets stored as the challenge for block C 122// C might be the height of the block that is currently still being prepared/proposed/voted on 123// the proofs should be stable regardless of how many proposal rounds we go through, because they only use data from C-1 124 125func (c *blockChallengeCoordinator) notifyOfIncomingBlockHeight(height int64) { 126 if !c.isConfiguredToBeValidator { 127 return 128 } 129 go func() { 130 _, err := c.loadOrComputeBlockChallengeProof(c.runnerContext, height) 131 if err != nil { 132 c.logger.Error("failed to compute block challenge", "height", height, "error", stacktrace.Propagate(err)) 133 } 134 }() 135} 136 137func (c *blockChallengeCoordinator) loadOrComputeBlockChallengeProof(ctx context.Context, height int64) ([]byte, error) { 138 if !c.isConfiguredToBeValidator { 139 return nil, stacktrace.NewError("node is not configured to be a validator") 140 } 141 proof, _, err := c.g.Do(ctx, height, func(ctx context.Context) ([]byte, error) { 142 // we need to read the tree as it was on the block prior 143 tx, err := c.txFactory.ReadHeight(time.Now(), height-1) 144 if err != nil { 145 return nil, stacktrace.Propagate(err) 146 } 147 148 proof, err := store.Consensus.BlockChallengeProof(tx, uint64(height)) 149 if err != nil { 150 return nil, stacktrace.Propagate(err) 151 } 152 if proof == nil { 153 st := time.Now() 154 // compute and store 155 proof, err = c.computeBlockChallengeProof(tx, height) 156 if err != nil { 157 return nil, stacktrace.Propagate(err) 158 } 159 160 wtx, err := tx.UpgradeForIndexOnly() 161 if err != nil { 162 return nil, stacktrace.Propagate(err) 163 } 164 defer wtx.Rollback() 165 166 err = store.Consensus.StoreBlockChallengeProof(wtx, uint64(height), proof) 167 if err != nil { 168 return nil, stacktrace.Propagate(err) 169 } 170 171 err = wtx.Commit() 172 if err != nil { 173 return nil, stacktrace.Propagate(err) 174 } 175 176 c.logger.Debug("computed and stored block challenge", "height", height, "took", time.Since(st)) 177 } 178 return proof, nil 179 }) 180 return proof, stacktrace.Propagate(err) 181} 182 183func (c *blockChallengeCoordinator) computeBlockChallengeProof(tx transaction.Read, height int64) ([]byte, error) { 184 witness, err := c.buildPrivateChallengeWitnessForHeight(tx, height) 185 if err != nil { 186 return nil, stacktrace.Propagate(err) 187 } 188 proof, err := groth16.Prove(blockChallengeConstraintSystem, blockChallengeProvingKey, witness) 189 if err != nil { 190 return nil, stacktrace.Propagate(err) 191 } 192 193 buf := new(bytes.Buffer) 194 195 _, err = proof.WriteTo(buf) 196 if err != nil { 197 return nil, stacktrace.Propagate(err) 198 } 199 200 return buf.Bytes(), nil 201} 202 203var bn254IDScalarField = bn254.ID.ScalarField() 204 205var errInvalidBlockChallengeProof = errors.New("invalid block challenge proof") 206 207func (c *blockChallengeCoordinator) verifyBlockChallengeProof(height int64, validatorAddress []byte, proofBytes []byte) error { 208 // timestamp shouldn't matter for this 209 // it is however important that we read the tree exactly as it was on the height prior to the one where the proof was supposedly generated 210 // this is because operations can change over time (nullification) and also the returned data for the highest operation indexes will be different 211 tx, err := c.txFactory.ReadHeight(time.Time{}, height-1) 212 if err != nil { 213 return stacktrace.Propagate(err) 214 } 215 216 sharedPart, err := c.fetchOrBuildBlockChallengeCircuitAssignmentShared(tx, height) 217 if err != nil { 218 return stacktrace.Propagate(err) 219 } 220 221 assignment := buildBlockChallengeCircuitAssignmentFull(sharedPart, validatorAddress) 222 223 witness, err := frontend.NewWitness(assignment, bn254IDScalarField) 224 if err != nil { 225 return stacktrace.Propagate(err) 226 } 227 228 publicWitness, err := witness.Public() 229 if err != nil { 230 return stacktrace.Propagate(err) 231 } 232 233 proof := groth16.NewProof(bn254.ID) 234 235 _, err = proof.ReadFrom(bytes.NewBuffer(proofBytes)) 236 if err != nil { 237 return stacktrace.Propagate(errors.Join(errInvalidBlockChallengeProof, err)) 238 } 239 240 err = groth16.Verify(proof, blockChallengeVerifyingKey, publicWitness) 241 if err != nil { 242 return stacktrace.Propagate(errors.Join(errInvalidBlockChallengeProof, err)) 243 } 244 return nil 245} 246 247func (c *blockChallengeCoordinator) buildPrivateChallengeWitnessForHeight(tx transaction.Read, height int64) (witness.Witness, error) { 248 sharedPart, err := c.fetchOrBuildBlockChallengeCircuitAssignmentShared(tx, height) 249 if err != nil { 250 return nil, stacktrace.Propagate(err) 251 } 252 253 assignment := buildBlockChallengeCircuitAssignmentFull(sharedPart, c.validatorAddress) 254 255 witness, err := frontend.NewWitness(assignment, bn254IDScalarField) 256 return witness, stacktrace.Propagate(err) 257} 258 259func (c *blockChallengeCoordinator) blockChallengeOperationDataForHeight(tx transaction.Read, height int64) ([]byte, []byte, error) { 260 if height <= 1 { 261 return make([]byte, proof.OperationDataLength), make([]byte, 32), nil 262 } 263 264 blockHeader, err := c.blockHeaderGetter(height - 1) 265 if err != nil { 266 return nil, nil, stacktrace.Propagate(err) 267 } 268 lastCommitHash := blockHeader.LastCommitHash 269 lastCommitHashBigInt := new(big.Int).SetBytes(lastCommitHash) 270 271 highestOp, err := tx.CountOperations() 272 if err != nil { 273 return nil, nil, stacktrace.Propagate(err) 274 } 275 276 initialOpDataLen := proof.OperationDataLength + int(lastCommitHash[0]) 277 operationData := make([]byte, initialOpDataLen) 278 operationDataCursor := 0 279 if highestOp > 0 { 280 startOpIdxBigInt := new(big.Int).Mod(lastCommitHashBigInt, new(big.Int).SetUint64(highestOp-1)) 281 startOpIdx := startOpIdxBigInt.Uint64() 282 // the starting operation sequence is startOpIdx+1 283 // because operations sequences start at 1 but the result of the modulus is (0, n( 284 // but that's ok because the OperationsIterator cursor parameter is "after" (i.e. the lower bound is exclusive) 285 // so we indeed want a 0-indexed parameter 286 for _, rawOpValue := range store.Consensus.OperationsIterator(tx, startOpIdx, &err) { 287 operationDataCursor += copy(operationData[operationDataCursor:], rawOpValue) 288 if operationDataCursor >= initialOpDataLen { 289 break 290 } 291 } 292 if err != nil { 293 return nil, nil, stacktrace.Propagate(err) 294 } 295 } 296 297 return operationData[lastCommitHash[0]:], lastCommitHash, nil 298} 299 300func (c *blockChallengeCoordinator) fetchOrBuildBlockChallengeCircuitAssignmentShared(tx transaction.Read, height int64) (proof.BlockChallengeCircuit, error) { 301 ctx := context.WithValue(c.runnerContext, contextTxKey{}, tx) 302 v, err := c.sharedWitnessDataCache.Get(ctx, height) 303 return v, stacktrace.Propagate(err) 304} 305 306func buildBlockChallengeCircuitAssignmentShared(lastCommitHash []byte, data [proof.OperationDataLength]byte) proof.BlockChallengeCircuit { 307 var assignment proof.BlockChallengeCircuit 308 309 if len(lastCommitHash) > 31 { 310 lastCommitHash = lastCommitHash[len(lastCommitHash)-31:] 311 } 312 assignment.LastCommitHash = lastCommitHash 313 314 for i := 0; i < len(assignment.OperationData); i++ { 315 d := data[31*i : 31*(i+1)] 316 assignment.OperationData[i] = d 317 } 318 319 return assignment 320} 321 322func buildBlockChallengeCircuitAssignmentFull(shared proof.BlockChallengeCircuit, validatorAddress []byte) *proof.BlockChallengeCircuit { 323 h := mimc.NewMiMC() 324 325 h.Write(validatorAddress) 326 h.Write(shared.LastCommitHash.([]byte)) 327 328 for i := 0; i < len(shared.OperationData); i++ { 329 h.Write(shared.OperationData[i].([]byte)) 330 } 331 332 shared.ValidatorAddress = validatorAddress 333 shared.SpecificHash = h.Sum(nil) 334 335 return &shared 336}