A very experimental PLC implementation which uses BFT consensus for decentralization

Begin work on range challenges

gbl08ma.com 686b6364 c64bc7d9

verified
+288 -1
+7
abciapp/app.go
··· 44 44 aocsByPLC map[string]*authoritativeOperationsCache 45 45 46 46 blockChallengeCoordinator *blockChallengeCoordinator 47 + rangeChallengeCoordinator *rangeChallengeCoordinator 47 48 } 48 49 49 50 // store and plc must be able to share transaction objects ··· 193 194 if err != nil { 194 195 return stacktrace.Propagate(err, "") 195 196 } 197 + 198 + d.rangeChallengeCoordinator, err = newRangeChallengeCoordinator(d.runnerContext, d.txFactory, blockStore, pubKey) 199 + if err != nil { 200 + return stacktrace.Propagate(err, "") 201 + } 202 + 196 203 return nil 197 204 } 198 205
+1 -1
abciapp/block_challenge.go
··· 81 81 82 82 anyTx := ctx.Value(contextTxKey{}) 83 83 if anyTx == nil { 84 - return theine.Loaded[proof.BlockChallengeCircuit]{}, stacktrace.NewError("transaction not found in context") 84 + return zeroValue, stacktrace.NewError("transaction not found in context") 85 85 } 86 86 tx, ok := anyTx.(transaction.Read) 87 87 if !ok {
+160
abciapp/range_challenge.go
··· 1 + package abciapp 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/binary" 7 + "slices" 8 + 9 + "github.com/Yiling-J/theine-go" 10 + "github.com/cometbft/cometbft/crypto" 11 + bftstore "github.com/cometbft/cometbft/store" 12 + "github.com/cosmos/iavl" 13 + "github.com/cosmos/iavl/db" 14 + ics23 "github.com/cosmos/ics23/go" 15 + "github.com/palantir/stacktrace" 16 + "tangled.org/gbl08ma.com/didplcbft/store" 17 + "tangled.org/gbl08ma.com/didplcbft/transaction" 18 + ) 19 + 20 + type rangeChallengeCoordinator struct { 21 + runnerContext context.Context 22 + 23 + validatorAddress []byte 24 + txFactory *transaction.Factory 25 + nodeBlockStore *bftstore.BlockStore 26 + 27 + treeCache *theine.LoadingCache[treeCacheKey, cachedTree] 28 + } 29 + 30 + func newRangeChallengeCoordinator(runnerContext context.Context, txFactory *transaction.Factory, blockStore *bftstore.BlockStore, pubKey crypto.PubKey) (*rangeChallengeCoordinator, error) { 31 + c := &rangeChallengeCoordinator{ 32 + txFactory: txFactory, 33 + runnerContext: runnerContext, 34 + nodeBlockStore: blockStore, 35 + validatorAddress: pubKey.Address(), 36 + } 37 + 38 + var err error 39 + c.treeCache, err = theine.NewBuilder[treeCacheKey, cachedTree](2).Loading(c.proofTreeLoader).Build() 40 + if err != nil { 41 + return nil, stacktrace.Propagate(err, "") 42 + } 43 + 44 + return c, nil 45 + } 46 + 47 + type treeCacheKey struct { 48 + startHeight int64 49 + endHeight int64 50 + } 51 + 52 + type cachedTree struct { 53 + tree *iavl.ImmutableTree 54 + root []byte 55 + } 56 + 57 + type rangeChallengeProof struct { 58 + treeRoot []byte // the tree root we commit to. must be the same between the "commit proof" and the "confirmation proof" 59 + membershipProof *ics23.CommitmentProof 60 + } 61 + 62 + func (c *rangeChallengeCoordinator) computeRangeChallengeProof(ctx context.Context, tx transaction.Read, startHeight, endHeight, proveHeight int64) (rangeChallengeProof, error) { 63 + ctx = context.WithValue(ctx, contextTxKey{}, tx) 64 + ct, err := c.treeCache.Get(ctx, treeCacheKey{ 65 + startHeight: startHeight, 66 + endHeight: endHeight, 67 + }) 68 + if err != nil { 69 + return rangeChallengeProof{}, stacktrace.Propagate(err, "") 70 + } 71 + 72 + proofKey := binary.BigEndian.AppendUint64(nil, uint64(proveHeight)) 73 + 74 + membershipProof, err := ct.tree.GetMembershipProof(proofKey) 75 + if err != nil { 76 + return rangeChallengeProof{}, stacktrace.Propagate(err, "") 77 + } 78 + 79 + return rangeChallengeProof{ 80 + treeRoot: ct.root, 81 + membershipProof: membershipProof, 82 + }, nil 83 + } 84 + 85 + func verifyMembershipOfRangeChallengeProofs(ctx context.Context, proofs ...rangeChallengeProof) (bool, error) { 86 + if len(proofs) < 1 { 87 + return false, stacktrace.NewError("insufficient proofs") 88 + } 89 + 90 + treeRoot := proofs[0].treeRoot 91 + 92 + for _, proof := range proofs { 93 + if !bytes.Equal(proof.treeRoot, treeRoot) { 94 + // did not commit to the same root in all proofs 95 + return false, nil 96 + } 97 + 98 + // just use the key and value claimed in the proof as those should have been validated previously 99 + if exist := proof.membershipProof.GetExist(); exist != nil { 100 + if !ics23.VerifyMembership(ics23.IavlSpec, treeRoot, proof.membershipProof, exist.Key, exist.Value) { 101 + return false, nil 102 + } 103 + } else { 104 + return false, stacktrace.NewError("proof is not an existence proof") 105 + } 106 + } 107 + 108 + return true, nil 109 + } 110 + 111 + func (c *rangeChallengeCoordinator) proofTreeLoader(ctx context.Context, cacheKey treeCacheKey) (theine.Loaded[cachedTree], error) { 112 + var zeroValue theine.Loaded[cachedTree] 113 + 114 + anyTx := ctx.Value(contextTxKey{}) 115 + if anyTx == nil { 116 + return zeroValue, stacktrace.NewError("transaction not found in context") 117 + } 118 + tx, ok := anyTx.(transaction.Read) 119 + if !ok { 120 + return zeroValue, stacktrace.NewError("invalid transaction in context") 121 + } 122 + 123 + tree := iavl.NewMutableTree(db.NewMemDB(), 16, false, iavl.NewNopLogger(), iavl.AsyncPruningOption(false)) 124 + 125 + var err error 126 + for proofHeight, proof := range store.Consensus.BlockChalengeProofsIterator(tx, uint64(max(cacheKey.startHeight-1, 0)), &err) { 127 + if proofHeight > uint64(cacheKey.endHeight) { 128 + break 129 + } 130 + 131 + _, err := tree.Set(binary.BigEndian.AppendUint64(nil, proofHeight), slices.Clone(proof)) 132 + if err != nil { 133 + return zeroValue, stacktrace.Propagate(err, "") 134 + } 135 + } 136 + if err != nil { 137 + return zeroValue, stacktrace.Propagate(err, "") 138 + } 139 + 140 + rootHash, treeVersion, err := tree.SaveVersion() 141 + if err != nil { 142 + return zeroValue, stacktrace.Propagate(err, "") 143 + } 144 + 145 + immutableTree, err := tree.GetImmutable(treeVersion) 146 + if err != nil { 147 + return zeroValue, stacktrace.Propagate(err, "") 148 + } 149 + 150 + if immutableTree.Size() != cacheKey.endHeight-cacheKey.startHeight+1 { 151 + return zeroValue, stacktrace.NewError("missing block challenge proofs in requested range") 152 + } 153 + 154 + return theine.Loaded[cachedTree]{ 155 + Value: cachedTree{ 156 + tree: immutableTree, 157 + root: rootHash, 158 + }, 159 + }, nil 160 + }
+34
abciapp/tx.go
··· 5 5 "context" 6 6 7 7 abcitypes "github.com/cometbft/cometbft/abci/types" 8 + "github.com/cometbft/cometbft/crypto" 8 9 cbornode "github.com/ipfs/go-ipld-cbor" 9 10 "github.com/palantir/stacktrace" 10 11 "github.com/samber/mo" ··· 46 47 type Transaction[ArgType any] struct { 47 48 Action TransactionAction `json:"action" refmt:"action"` 48 49 Arguments ArgType `json:"arguments,omitempty" refmt:"arguments,omitempty"` 50 + 51 + // Only some operations require this. The signature is verified against a context-dependent identity (usually specified within Arguments) 52 + Signature []byte `json:"sig,omitempty" refmt:"sig,omitempty"` 49 53 } 50 54 51 55 func UnmarshalTransaction[ArgType ArgumentType](txBytes []byte) (Transaction[ArgType], error) { ··· 83 87 return false 84 88 } 85 89 return bytes.Equal(txBytes, s) 90 + } 91 + 92 + func SignTransaction[ArgType ArgumentType](privKey crypto.PrivKey, tx Transaction[ArgType]) (Transaction[ArgType], error) { 93 + var zeroValue Transaction[ArgType] 94 + 95 + tx.Signature = nil 96 + 97 + bytesToSign, err := cbornode.DumpObject(tx) 98 + if err != nil { 99 + return zeroValue, stacktrace.Propagate(err, "") 100 + } 101 + 102 + tx.Signature, err = privKey.Sign(bytesToSign) 103 + if err != nil { 104 + return zeroValue, stacktrace.Propagate(err, "") 105 + } 106 + 107 + return tx, nil 108 + } 109 + 110 + func VerifyTransactionSignature[ArgType ArgumentType](publicKey crypto.PubKey, tx Transaction[ArgType]) (bool, error) { 111 + sig := tx.Signature 112 + tx.Signature = nil 113 + 114 + bytesToSign, err := cbornode.DumpObject(tx) 115 + if err != nil { 116 + return false, stacktrace.Propagate(err, "") 117 + } 118 + 119 + return publicKey.VerifySignature(bytesToSign, sig), nil 86 120 } 87 121 88 122 type processResult struct {
+86
abciapp/tx_challenge.go
··· 1 + package abciapp 2 + 3 + import ( 4 + "context" 5 + 6 + "github.com/cometbft/cometbft/crypto" 7 + cmtjson "github.com/cometbft/cometbft/libs/json" 8 + cbornode "github.com/ipfs/go-ipld-cbor" 9 + "github.com/palantir/stacktrace" 10 + ) 11 + 12 + var TransactionActionCommitToChallenge = registerTransactionAction[CommitToChallengeArguments]("CommitToChallenge", processCommitToChallengeTx) 13 + 14 + type CommitToChallengeArguments struct { 15 + ValidatorPubKey []byte `json:"validator" refmt:"validator"` 16 + FromHeight int64 `json:"fromHeight" refmt:"fromHeight"` 17 + 18 + // ToHeight should not be more than N blocks behind the block where this transaction is included 19 + // (i.e. this transaction "expires" once ToHeight is more than N blocks old, and shouldn't be included after that) 20 + // TODO determine N 21 + ToHeight int64 `json:"toHeight" refmt:"toHeight"` 22 + Root []byte `json:"root" refmt:"root"` 23 + Proof []byte `json:"proof" refmt:"proof"` 24 + } 25 + 26 + func (CommitToChallengeArguments) ForAction() TransactionAction { 27 + return TransactionActionCommitToChallenge 28 + } 29 + 30 + func init() { 31 + cbornode.RegisterCborType(CommitToChallengeArguments{}) 32 + cbornode.RegisterCborType(Transaction[CommitToChallengeArguments]{}) 33 + } 34 + 35 + func processCommitToChallengeTx(ctx context.Context, deps TransactionProcessorDependencies, txBytes []byte) (*processResult, error) { 36 + tx, err := UnmarshalTransaction[CommitToChallengeArguments](txBytes) 37 + if err != nil { 38 + return &processResult{ 39 + Code: 4000, 40 + Info: err.Error(), 41 + }, nil 42 + } 43 + 44 + var pubKey crypto.PubKey 45 + err = cmtjson.Unmarshal(tx.Arguments.ValidatorPubKey, &pubKey) 46 + 47 + verified, err := VerifyTransactionSignature(pubKey, tx) 48 + if err != nil { 49 + return nil, stacktrace.Propagate(err, "") 50 + } 51 + 52 + if !verified { 53 + return &processResult{ 54 + Code: 4200, 55 + Info: "invalid signature", 56 + }, nil 57 + } 58 + 59 + return nil, stacktrace.NewError("not implemented") // TODO 60 + } 61 + 62 + var TransactionActionCompleteChallenge = registerTransactionAction[CommitToChallengeArguments]("CompleteChallenge", processCompleteChallengeTx) 63 + 64 + type CompleteChallengeArguments struct { 65 + Validator []byte `json:"validator" refmt:"validator"` 66 + 67 + // this shall be a membership proof on the same tree the validator previously committed to, 68 + // for the key deterministically-randomly determined by the last_commit_hash of the block _after_ the one 69 + // where the most recent CommitToChallenge transaction for this validator got included 70 + // this transaction must be included within N blocks after the inclusion of that CommitToChallenge transaction 71 + // TODO determine N 72 + Proof []byte `json:"proof" refmt:"proof"` 73 + } 74 + 75 + func (CompleteChallengeArguments) ForAction() TransactionAction { 76 + return TransactionActionCommitToChallenge 77 + } 78 + 79 + func init() { 80 + cbornode.RegisterCborType(CompleteChallengeArguments{}) 81 + cbornode.RegisterCborType(Transaction[CompleteChallengeArguments]{}) 82 + } 83 + 84 + func processCompleteChallengeTx(ctx context.Context, deps TransactionProcessorDependencies, txBytes []byte) (*processResult, error) { 85 + return nil, stacktrace.NewError("not implemented") // TODO 86 + }