Skip to content

Commit

Permalink
[Proof] Implement scalable proof validation (#1031)
Browse files Browse the repository at this point in the history
## Summary

This PR refactors proof validation by distributing it across the proof
submission workflow for better scalability.

The validation process is divided into two stages:
1. **Proof Submission**: Ensures the proof is well-formed and adheres to
the current on-chain state.
2. **Proof Module Endblocker**: Handles computationally intensive tasks
using self-contained parallel verification. It includes:
   - Relay request and response signature verification.
   - Validation of the `ClosestMerkleProof`.

Additionally, the proof module endblocker:
- Deletes all the processed proofs
- Reflects the verification result in the corresponding claim's
`ProofStatus` field.

The potential supplier slashing is retained within the
`SettlePendingClaims` flow, ensuring a clear separation of concerns.

## Issue

- #1013 

## Type of change

Select one or more from the following:

- [x] New feature, functionality or library
- [ ] Consensus breaking; add the `consensus-breaking` label if so. See
#791 for details
- [ ] Bug fix
- [x] Code health or cleanup
- [ ] Documentation
- [ ] Other (specify)

## Testing

- [x] **Unit Tests**: `make go_develop_and_test`
- [x] **LocalNet E2E Tests**: `make test_e2e`
- [ ] **DevNet E2E Tests**: Add the `devnet-test-e2e` label to the PR.

## Sanity Checklist

- [x] I have tested my changes using the available tooling
- [x] I have commented my code
- [x] I have performed a self-review of my own code; both comments &
source code
- [ ] I create and reference any new tickets, if applicable
- [x] I have left TODOs throughout the codebase, if applicable

---------

Co-authored-by: Dmitry K <[email protected]>
Co-authored-by: Daniel Olshansky <[email protected]>
  • Loading branch information
3 people authored Jan 30, 2025
1 parent 3345bd2 commit 2055b0f
Show file tree
Hide file tree
Showing 25 changed files with 1,942 additions and 348 deletions.
763 changes: 727 additions & 36 deletions api/poktroll/proof/event.pulsar.go

Large diffs are not rendered by default.

193 changes: 157 additions & 36 deletions api/poktroll/proof/types.pulsar.go

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion pkg/crypto/protocol/hasher.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package protocol

import "crypto/sha256"
import (
"crypto/sha256"

"github.com/pokt-network/smt"
)

const (
RelayHasherSize = sha256.Size
Expand All @@ -15,3 +19,7 @@ var (
NewRelayHasher = sha256.New
NewTrieHasher = sha256.New
)

func SMTValueHasher() smt.TrieSpecOption {
return smt.WithValueHasher(nil)
}
31 changes: 13 additions & 18 deletions pkg/crypto/protocol/proof_path.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
package protocol

import (
"crypto/sha256"

"github.com/pokt-network/smt"
)

// SMT specification used for the proof verification.
var (
newHasher = sha256.New
SmtSpec smt.TrieSpec
)

func init() {
// Use a spec that does not prehash values in the smst. This returns a nil value
// hasher for the proof verification in order to avoid hashing the value twice.
SmtSpec = smt.NewTrieSpec(
newHasher(), true,
smt.WithValueHasher(nil),
)
}

// GetPathForProof computes the path to be used for proof validation by hashing
// the block hash and session id.
func GetPathForProof(blockHash []byte, sessionId string) []byte {
hasher := newHasher()
hasher := NewTrieHasher()
if _, err := hasher.Write(append(blockHash, []byte(sessionId)...)); err != nil {
panic(err)
}

return hasher.Sum(nil)
}

// NewSMTSpec returns the SMT specification used for proof verification.
// A new hasher is created for each call to prevent concurrency issues
// from shared state.
func NewSMTSpec() *smt.TrieSpec {
trieSpec := smt.NewTrieSpec(
NewTrieHasher(), true,
SMTValueHasher(),
)

return &trieSpec
}
5 changes: 2 additions & 3 deletions pkg/relayer/session/sessiontree.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package session

import (
"bytes"
"crypto/sha256"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -94,7 +93,7 @@ func NewSessionTree(

// Create the SMST from the KVStore and a nil value hasher so the proof would
// contain a non-hashed Relay that could be used to validate the proof onchain.
trie := smt.NewSparseMerkleSumTrie(treeStore, protocol.NewTrieHasher(), smt.WithValueHasher(nil))
trie := smt.NewSparseMerkleSumTrie(treeStore, protocol.NewTrieHasher(), protocol.SMTValueHasher())

logger = logger.With(
"store_path", storePath,
Expand Down Expand Up @@ -175,7 +174,7 @@ func (st *sessionTree) ProveClosest(path []byte) (compactProof *smt.SparseCompac
return nil, err
}

sessionSMT := smt.ImportSparseMerkleSumTrie(st.treeStore, sha256.New(), st.claimedRoot, smt.WithValueHasher(nil))
sessionSMT := smt.ImportSparseMerkleSumTrie(st.treeStore, protocol.NewTrieHasher(), st.claimedRoot, protocol.SMTValueHasher())

// Generate the proof and cache it along with the path for which it was generated.
// There is no ProveClosest variant that generates a compact proof directly.
Expand Down
2 changes: 1 addition & 1 deletion pkg/relayer/session/sessiontree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestSessionTree_CompactProofsAreSmallerThanNonCompactProofs(t *testing.T) {
kvStore, err := pebble.NewKVStore("")
require.NoError(t, err)

trie := smt.NewSparseMerkleSumTrie(kvStore, protocol.NewTrieHasher(), smt.WithValueHasher(nil))
trie := smt.NewSparseMerkleSumTrie(kvStore, protocol.NewTrieHasher(), protocol.SMTValueHasher())

// Insert numLeaf random leaves.
for i := 0; i < numLeafs; i++ {
Expand Down
11 changes: 11 additions & 0 deletions proto/poktroll/proof/event.proto
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,14 @@ message EventProofUpdated {
uint64 num_estimated_compute_units = 5 [(gogoproto.jsontag) = "num_estimated_compute_units"];
cosmos.base.v1beta1.Coin claimed_upokt = 6 [(gogoproto.jsontag) = "claimed_upokt"];
}

// Event emitted after a proof has been checked for validity in the proof module's
// EndBlocker.
message EventProofValidityChecked {
poktroll.proof.Proof proof = 1 [(gogoproto.jsontag) = "proof"];
uint64 block_height = 2 [(gogoproto.jsontag) = "block_height"];
poktroll.proof.ClaimProofStatus proof_status = 3 [(gogoproto.jsontag) = "proof_status"];
// reason is the string representation of the error that led to the proof being
// marked as invalid (e.g. "invalid closest merkle proof", "invalid relay request signature")
string failure_reason = 4 [(gogoproto.jsontag) = "failure_reason"];
}
18 changes: 16 additions & 2 deletions proto/poktroll/proof/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@ message Proof {

// Claim is the serialized object stored onchain for claims pending to be proven
message Claim {
// Address of the supplier's operator that submitted this claim.
string supplier_operator_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // the address of the supplier's operator that submitted this claim
// The session header of the session that this claim is for.

// Session header this claim is for.
poktroll.session.SessionHeader session_header = 2;
// Root hash returned from smt.SMST#Root().

// Root hash from smt.SMST#Root().
bytes root_hash = 3;

// Important: This field MUST only be set by proofKeeper#EnsureValidProofSignaturesAndClosestPath
ClaimProofStatus proof_validation_status = 4;
}

enum ProofRequirementReason {
Expand All @@ -43,3 +49,11 @@ enum ClaimProofStage {
SETTLED = 2;
EXPIRED = 3;
}

// Status of proof validation for a claim
// Default is PENDING_VALIDATION regardless of proof requirement
enum ClaimProofStatus {
PENDING_VALIDATION = 0;
VALIDATED = 1;
INVALID = 2;
}
2 changes: 1 addition & 1 deletion tests/integration/service/relay_mining_difficulty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func prepareSMST(
// TODO_TECHDEBT(#446): Centralize the configuration for the SMT spec.
kvStore, err := pebble.NewKVStore("")
require.NoError(t, err)
trie := smt.NewSparseMerkleSumTrie(kvStore, protocol.NewTrieHasher(), smt.WithValueHasher(nil))
trie := smt.NewSparseMerkleSumTrie(kvStore, protocol.NewTrieHasher(), protocol.SMTValueHasher())

for i := uint64(0); i < numRelays; i++ {
// DEV_NOTE: A signed mined relay is a MinedRelay type with the appropriate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func prepareRealClaim(
require.NoError(t, err)

// Prepare an SMST
trie := smt.NewSparseMerkleSumTrie(kvStore, protocol.NewTrieHasher(), smt.WithValueHasher(nil))
trie := smt.NewSparseMerkleSumTrie(kvStore, protocol.NewTrieHasher(), protocol.SMTValueHasher())

// Insert the mined relays into the SMST
for i := uint64(0); i < numRelays; i++ {
Expand Down
1 change: 1 addition & 0 deletions testutil/testtree/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,6 @@ func NewClaim(
SupplierOperatorAddress: supplierOperatorAddr,
SessionHeader: sessionHeader,
RootHash: rootHash,
ProofValidationStatus: prooftypes.ClaimProofStatus_PENDING_VALIDATION,
}
}
Loading

0 comments on commit 2055b0f

Please sign in to comment.