diff --git a/pkg/crypto/protocol/proof_path.go b/pkg/crypto/protocol/proof_path.go new file mode 100644 index 000000000..61f7e23ce --- /dev/null +++ b/pkg/crypto/protocol/proof_path.go @@ -0,0 +1,33 @@ +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() + if _, err := hasher.Write(append(blockHash, []byte(sessionId)...)); err != nil { + panic(err) + } + + return hasher.Sum(nil) +} diff --git a/pkg/relayer/session/proof.go b/pkg/relayer/session/proof.go index 35ff96649..d45d25c47 100644 --- a/pkg/relayer/session/proof.go +++ b/pkg/relayer/session/proof.go @@ -5,13 +5,13 @@ import ( "fmt" "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/crypto/protocol" "github.com/pokt-network/poktroll/pkg/either" "github.com/pokt-network/poktroll/pkg/observable" "github.com/pokt-network/poktroll/pkg/observable/channel" "github.com/pokt-network/poktroll/pkg/observable/filter" "github.com/pokt-network/poktroll/pkg/observable/logging" "github.com/pokt-network/poktroll/pkg/relayer" - proofkeeper "github.com/pokt-network/poktroll/x/proof/keeper" "github.com/pokt-network/poktroll/x/shared" ) @@ -229,6 +229,8 @@ func (rs *relayerSessionsManager) goProveClaims( proofsGeneratedCh chan<- []relayer.SessionTree, failSubmitProofsSessionsCh chan<- []relayer.SessionTree, ) { + logger := rs.logger.With("method", "goProveClaims") + // Separate the sessionTrees into those that failed to generate a proof // and those that succeeded, then send them on their respective channels. failedProofs := []relayer.SessionTree{} @@ -241,13 +243,15 @@ func (rs *relayerSessionsManager) goProveClaims( } // Generate the proof path for the sessionTree using the previously committed // sessionPathBlock hash. - path := proofkeeper.GetPathForProof( + path := protocol.GetPathForProof( sessionPathBlock.Hash(), sessionTree.GetSessionHeader().GetSessionId(), ) // If the proof cannot be generated, add the sessionTree to the failedProofs. if _, err := sessionTree.ProveClosest(path); err != nil { + logger.Error().Err(err).Msg("failed to generate proof") + failedProofs = append(failedProofs, sessionTree) continue } diff --git a/tests/integration/tokenomics/relay_mining_difficulty_test.go b/tests/integration/tokenomics/relay_mining_difficulty_test.go index 8eb6a57c8..1b543c00a 100644 --- a/tests/integration/tokenomics/relay_mining_difficulty_test.go +++ b/tests/integration/tokenomics/relay_mining_difficulty_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/pokt-network/poktroll/cmd/poktrolld/cmd" + "github.com/pokt-network/poktroll/pkg/crypto/protocol" testutilevents "github.com/pokt-network/poktroll/testutil/events" "github.com/pokt-network/poktroll/testutil/integration" testutil "github.com/pokt-network/poktroll/testutil/integration" @@ -28,7 +29,7 @@ func init() { } func TestUpdateRelayMiningDifficulty_NewServiceSeenForTheFirstTime(t *testing.T) { - var claimWindowOpenBlockHash, proofWindowOpenBlockHash []byte + var claimWindowOpenBlockHash, proofWindowOpenBlockHash, proofPathSeedBlockHash []byte // Create a new integration app integrationApp := integration.NewCompleteIntegrationApp(t) @@ -89,7 +90,7 @@ func TestUpdateRelayMiningDifficulty_NewServiceSeenForTheFirstTime(t *testing.T) createProofMsg := prooftypes.MsgSubmitProof{ SupplierAddress: integrationApp.DefaultSupplier.Address, SessionHeader: session.Header, - Proof: getProof(t, trie), + Proof: getProof(t, trie, proofPathSeedBlockHash, session.GetHeader().GetSessionId()), } result = integrationApp.RunMsg(t, &createProofMsg, @@ -202,11 +203,16 @@ func prepareSMST( // getProof returns a proof for the given session for the empty path. // If there is only one relay in the trie, the proof will be for that single // relay since it is "closest" to any path provided, empty or not. -func getProof(t *testing.T, trie *smt.SMST) []byte { +func getProof( + t *testing.T, + trie *smt.SMST, + pathSeedBlockHash []byte, + sessionId string, +) []byte { t.Helper() - emptyPath := make([]byte, trie.PathHasherSize()) - proof, err := trie.ProveClosest(emptyPath) + path := protocol.GetPathForProof(pathSeedBlockHash, sessionId) + proof, err := trie.ProveClosest(path) require.NoError(t, err) proofBz, err := proof.Marshal() diff --git a/x/proof/keeper/msg_server_submit_proof.go b/x/proof/keeper/msg_server_submit_proof.go index 4e0095319..c36fb6a7b 100644 --- a/x/proof/keeper/msg_server_submit_proof.go +++ b/x/proof/keeper/msg_server_submit_proof.go @@ -7,9 +7,7 @@ package keeper import ( "bytes" "context" - "crypto/sha256" "fmt" - "hash" cosmoscryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" cosmostypes "github.com/cosmos/cosmos-sdk/types" @@ -26,22 +24,6 @@ import ( sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) -// SMT specification used for the proof verification. -var ( - hasher hash.Hash - 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 to avoid hashing the value twice. - hasher = sha256.New() - SmtSpec = smt.NewTrieSpec( - hasher, true, - smt.WithValueHasher(nil), - ) -} - // SubmitProof is the server handler to submit and store a proof on-chain. // A proof that's stored on-chain is what leads to rewards (i.e. inflation) // downstream, making the series of checks a critical part of the protocol. @@ -165,7 +147,7 @@ func (k msgServer) SubmitProof( // TODO_MAINNET(#427): Utilize smt.VerifyCompactClosestProof here to // reduce on-chain storage requirements for proofs. // Get the relay request and response from the proof.GetClosestMerkleProof. - relayBz := sparseMerkleClosestProof.GetValueHash(&SmtSpec) + relayBz := sparseMerkleClosestProof.GetValueHash(&protocol.SmtSpec) relay := &servicetypes.Relay{} if err = k.cdc.Unmarshal(relayBz, relay); err != nil { return nil, status.Error( @@ -454,7 +436,7 @@ func verifyClosestProof( proof *smt.SparseMerkleClosestProof, claimRootHash []byte, ) error { - valid, err := smt.VerifyClosestProof(proof, claimRootHash, &SmtSpec) + valid, err := smt.VerifyClosestProof(proof, claimRootHash, &protocol.SmtSpec) if err != nil { return err } @@ -528,10 +510,10 @@ func (k msgServer) validateClosestPath( // error that may occur due to block height differing from the off-chain part. k.logger.Info("E2E_DEBUG: height for block hash when verifying the proof", earliestSupplierProofCommitHeight, sessionHeader.GetSessionId()) - expectedProofPath := GetPathForProof(proofPathSeedBlockHash, sessionHeader.GetSessionId()) + expectedProofPath := protocol.GetPathForProof(proofPathSeedBlockHash, sessionHeader.GetSessionId()) if !bytes.Equal(proof.Path, expectedProofPath) { return types.ErrProofInvalidProof.Wrapf( - "the proof for the path provided (%x) does not match one expected by the on-chain protocol (%x)", + "the path of the proof provided (%x) does not match one expected by the on-chain protocol (%x)", proof.Path, expectedProofPath, ) @@ -539,13 +521,3 @@ func (k msgServer) validateClosestPath( return nil } - -func GetPathForProof(blockHash []byte, sessionId string) []byte { - // TODO_BLOCKER(@Olshansk): We need to replace the return - // statement below and change all relevant parts in the codebase. - // See the conversation in the following thread for more details: https://github.com/pokt-network/poktroll/pull/406#discussion_r1520790083 - path := make([]byte, SmtSpec.PathHasherSize()) - copy(path, blockHash) - return path - // return pathHasher.Sum(append(blockHash, []byte(sessionId)...)) -} diff --git a/x/proof/keeper/msg_server_submit_proof_test.go b/x/proof/keeper/msg_server_submit_proof_test.go index 236f87ff3..98d880a52 100644 --- a/x/proof/keeper/msg_server_submit_proof_test.go +++ b/x/proof/keeper/msg_server_submit_proof_test.go @@ -55,12 +55,10 @@ var ( func init() { // The CometBFT header hash is 32 bytes: https://docs.cometbft.com/main/spec/core/data_structures blockHeaderHash = make([]byte, 32) - expectedMerkleProofPath = keeper.GetPathForProof(blockHeaderHash, "TODO_BLOCKER_session_id_currently_unused") + expectedMerkleProofPath = protocol.GetPathForProof(blockHeaderHash, "TODO_BLOCKER_session_id_currently_unused") } func TestMsgServer_SubmitProof_Success(t *testing.T) { - var claimWindowOpenBlockHash []byte - tests := []struct { desc string getProofMsgHeight func( @@ -75,7 +73,7 @@ func TestMsgServer_SubmitProof_Success(t *testing.T) { return shared.GetEarliestSupplierProofCommitHeight( sharedParams, queryHeight, - claimWindowOpenBlockHash, + blockHeaderHash, supplierAddr, ) }, @@ -165,11 +163,10 @@ func TestMsgServer_SubmitProof_Success(t *testing.T) { claimMsgHeight := shared.GetEarliestSupplierClaimCommitHeight( &sharedParams, sessionHeader.GetSessionEndBlockHeight(), - claimWindowOpenBlockHash, + blockHeaderHash, supplierAddr, ) - sdkCtx = sdkCtx.WithBlockHeight(claimMsgHeight) - ctx = sdkCtx + ctx = keepertest.SetBlockHeight(ctx, claimMsgHeight) // Create a valid claim. claim := createClaimAndStoreBlockHash( @@ -183,10 +180,25 @@ func TestMsgServer_SubmitProof_Success(t *testing.T) { keepers, ) + // Advance the block height to the proof path seed height. + earliestSupplierProofCommitHeight := shared.GetEarliestSupplierProofCommitHeight( + &sharedParams, + sessionHeader.GetSessionEndBlockHeight(), + blockHeaderHash, + supplierAddr, + ) + ctx = keepertest.SetBlockHeight(ctx, earliestSupplierProofCommitHeight-1) + + // Store proof path seed block hash in the session keeper so that it can + // look it up during proof validation. + keepers.StoreBlockHash(ctx) + + // Compute expected proof path. + expectedMerkleProofPath := protocol.GetPathForProof(blockHeaderHash, sessionHeader.GetSessionId()) + // Advance the block height to the test proof msg height. proofMsgHeight := test.getProofMsgHeight(&sharedParams, sessionHeader.GetSessionEndBlockHeight(), supplierAddr) - sdkCtx = sdkCtx.WithBlockHeight(proofMsgHeight) - ctx = sdkCtx + ctx = keepertest.SetBlockHeight(ctx, proofMsgHeight) proofMsg := newTestProofMsg(t, supplierAddr, @@ -388,12 +400,10 @@ func TestMsgServer_SubmitProof_Error_OutsideOfWindow(t *testing.T) { } func TestMsgServer_SubmitProof_Error(t *testing.T) { - var claimWindowOpenBlockHash, proofCommitBlockHash []byte - opts := []keepertest.ProofKeepersOpt{ // Set block hash such that on-chain closest merkle proof validation // uses the expected path. - keepertest.WithBlockHash(expectedMerkleProofPath), + keepertest.WithBlockHash(blockHeaderHash), // Set block height to 1 so there is a valid session on-chain. keepertest.WithBlockHeight(1), } @@ -507,7 +517,7 @@ func TestMsgServer_SubmitProof_Error(t *testing.T) { claimMsgHeight := shared.GetEarliestSupplierClaimCommitHeight( &sharedParams, validSessionHeader.GetSessionEndBlockHeight(), - claimWindowOpenBlockHash, + blockHeaderHash, supplierAddr, ) sdkCtx := cosmostypes.UnwrapSDKContext(ctx) @@ -986,8 +996,7 @@ func TestMsgServer_SubmitProof_Error(t *testing.T) { require.NoError(t, err) // Re-set the block height to the earliest claim commit height to create a new claim. - claimCtx := cosmostypes.UnwrapSDKContext(ctx) - claimCtx = claimCtx.WithBlockHeight(claimMsgHeight) + claimCtx := keepertest.SetBlockHeight(ctx, claimMsgHeight) // Create a valid claim with the expected merkle root. claimMsg := newTestClaimMsg(t, @@ -1008,9 +1017,9 @@ func TestMsgServer_SubmitProof_Error(t *testing.T) { expectedErr: status.Error( codes.FailedPrecondition, prooftypes.ErrProofInvalidProof.Wrapf( - "the proof for the path provided (%x) does not match one expected by the on-chain protocol (%x)", + "the path of the proof provided (%x) does not match one expected by the on-chain protocol (%x)", wrongClosestProofPath, - blockHeaderHash, + protocol.GetPathForProof(sdkCtx.HeaderHash(), validSessionHeader.GetSessionId()), ).Error(), ), }, @@ -1074,6 +1083,12 @@ func TestMsgServer_SubmitProof_Error(t *testing.T) { _, err = unclaimedSessionTree.Flush() require.NoError(t, err) + // Compute expected proof path for the unclaimed session. + expectedMerkleProofPath := protocol.GetPathForProof( + blockHeaderHash, + unclaimedSessionHeader.GetSessionId(), + ) + // Construct new proof message using the supplier & session header // from the session which is *not* expected to be claimed. return newTestProofMsg(t, @@ -1109,8 +1124,7 @@ func TestMsgServer_SubmitProof_Error(t *testing.T) { require.NoError(t, err) // Re-set the block height to the earliest claim commit height to create a new claim. - claimCtx := cosmostypes.UnwrapSDKContext(ctx) - claimCtx = claimCtx.WithBlockHeight(claimMsgHeight) + claimCtx := keepertest.SetBlockHeight(ctx, claimMsgHeight) // Create a claim with the incorrect Merkle root. wrongMerkleRootClaimMsg := newTestClaimMsg(t, @@ -1124,6 +1138,25 @@ func TestMsgServer_SubmitProof_Error(t *testing.T) { _, err = srv.CreateClaim(claimCtx, wrongMerkleRootClaimMsg) require.NoError(t, err) + // Construct a valid session tree with 5 relays. + validSessionTree := newFilledSessionTree( + ctx, t, + uint(5), + supplierUid, supplierAddr, + validSessionHeader, validSessionHeader, validSessionHeader, + keyRing, + ringClient, + ) + + _, err = validSessionTree.Flush() + require.NoError(t, err) + + // Compute expected proof path for the session. + expectedMerkleProofPath := protocol.GetPathForProof( + blockHeaderHash, + validSessionHeader.GetSessionId(), + ) + return newTestProofMsg(t, supplierAddr, validSessionHeader, @@ -1162,16 +1195,23 @@ func TestMsgServer_SubmitProof_Error(t *testing.T) { // Submit the corresponding proof. for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - // Increment the block height to the test proof height. proofMsg := test.newProofMsg(t) - proofMsgHeight := shared.GetEarliestSupplierProofCommitHeight( + + // Advance the block height to the proof path seed height. + earliestSupplierProofCommitHeight := shared.GetEarliestSupplierProofCommitHeight( &sharedParams, proofMsg.GetSessionHeader().GetSessionEndBlockHeight(), - proofCommitBlockHash, + blockHeaderHash, proofMsg.GetSupplierAddress(), ) + ctx = keepertest.SetBlockHeight(ctx, earliestSupplierProofCommitHeight-1) + + // Store proof path seed block hash in the session keeper so that it can + // look it up during proof validation. + keepers.StoreBlockHash(ctx) - ctx = cosmostypes.UnwrapSDKContext(ctx).WithBlockHeight(proofMsgHeight) + // Advance the block height to the earliest proof commit height. + ctx = keepertest.SetBlockHeight(ctx, earliestSupplierProofCommitHeight) submitProofRes, err := srv.SubmitProof(ctx, proofMsg) @@ -1385,7 +1425,7 @@ func getClosestRelayDifficultyBits( // Extract the Relay (containing the RelayResponse & RelayRequest) from the merkle proof. relay := new(servicetypes.Relay) - relayBz := closestMerkleProof.GetValueHash(&keeper.SmtSpec) + relayBz := closestMerkleProof.GetValueHash(&protocol.SmtSpec) err = relay.Unmarshal(relayBz) require.NoError(t, err) diff --git a/x/tokenomics/keeper/update_relay_mining_difficulty.go b/x/tokenomics/keeper/update_relay_mining_difficulty.go index 606e275ff..340ac77e3 100644 --- a/x/tokenomics/keeper/update_relay_mining_difficulty.go +++ b/x/tokenomics/keeper/update_relay_mining_difficulty.go @@ -10,7 +10,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - proofkeeper "github.com/pokt-network/poktroll/x/proof/keeper" + "github.com/pokt-network/poktroll/pkg/crypto/protocol" prooftypes "github.com/pokt-network/poktroll/x/proof/types" "github.com/pokt-network/poktroll/x/tokenomics/types" ) @@ -144,14 +144,14 @@ func ComputeNewDifficultyTargetHash(targetNumRelays, newRelaysEma uint64) []byte // (0.5)^x = (T/R) // x = -ln2(T/R) numLeadingZeroBits := int(-log2(float64(targetNumRelays) / float64(newRelaysEma))) - numBytes := proofkeeper.SmtSpec.PathHasherSize() + numBytes := protocol.SmtSpec.PathHasherSize() return LeadingZeroBitsToTargetDifficultyHash(numLeadingZeroBits, numBytes) } // defaultDifficultyTargetHash returns the default difficulty target hash with // the default number of leading zero bits. func defaultDifficultyTargetHash() []byte { - numBytes := proofkeeper.SmtSpec.PathHasherSize() + numBytes := protocol.SmtSpec.PathHasherSize() numDefaultLeadingZeroBits := int(prooftypes.DefaultMinRelayDifficultyBits) return LeadingZeroBitsToTargetDifficultyHash(numDefaultLeadingZeroBits, numBytes) }