From c5c7aa39bd35659da85090e67bdc430c16fe097f Mon Sep 17 00:00:00 2001 From: srdtrk <59252793+srdtrk@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:54:51 +0200 Subject: [PATCH] imp: add proof caching for aggregation (#110) * feat: added caching * imp: limited code to membership and added tests * imp: improved contract and test * style: renamed tests * lint: fixed linter issues * style: addressed review items --- contracts/src/SP1ICS07Tendermint.sol | 33 ++++++++++++++++--- contracts/test/Membership.t.sol | 37 ++++++++++++++++++--- contracts/test/UcAndMembership.t.sol | 48 +++++++++++++++++++++++++--- 3 files changed, 105 insertions(+), 13 deletions(-) diff --git a/contracts/src/SP1ICS07Tendermint.sol b/contracts/src/SP1ICS07Tendermint.sol index ea02b4d..4d72f8e 100644 --- a/contracts/src/SP1ICS07Tendermint.sol +++ b/contracts/src/SP1ICS07Tendermint.sol @@ -42,6 +42,8 @@ contract SP1ICS07Tendermint is ClientState private clientState; /// @notice The mapping from height to consensus state keccak256 hashes. mapping(uint32 height => bytes32 hash) private consensusStateHashes; + /// @notice The collection of verified SP1 proofs for caching. + mapping(bytes32 sp1ProofHash => bool isVerified) private verifiedProofs; /// @notice Allowed clock drift in seconds. /// @inheritdoc ISP1ICS07Tendermint @@ -178,7 +180,6 @@ contract SP1ICS07Tendermint is bytes calldata kvValue ) private - view returns (uint256) { if (proofHeight.revisionNumber != clientState.latestHeight.revisionNumber) { @@ -222,7 +223,12 @@ contract SP1ICS07Tendermint is validateMembershipOutput(output.commitmentRoot, proofHeight.revisionHeight, proof.trustedConsensusState); - verifySP1Proof(proof.sp1Proof); + // We avoid the cost of caching for single kv pairs, as reusing the proof is not necessary + if (output.kvPairs.length == 1) { + verifySP1Proof(proof.sp1Proof); + } else { + verifySP1ProofCached(proof.sp1Proof); + } return proof.trustedConsensusState.timestamp; } @@ -234,6 +240,7 @@ contract SP1ICS07Tendermint is /// @param kvPath The path of the key-value pair. /// @param kvValue The value of the key-value pair. /// @return The timestamp of the new consensus state. + // solhint-disable-next-line code-complexity function handleSP1UpdateClientAndMembership( Height calldata proofHeight, bytes memory proofBytes, @@ -270,7 +277,12 @@ contract SP1ICS07Tendermint is validateUpdateClientPublicValues(output.updateClientOutput); - verifySP1Proof(proof.sp1Proof); + // We avoid the cost of caching for single kv pairs, as reusing the proof is not necessary + if (output.kvPairs.length == 1) { + verifySP1Proof(proof.sp1Proof); + } else { + verifySP1ProofCached(proof.sp1Proof); + } } // check update result @@ -438,12 +450,25 @@ contract SP1ICS07Tendermint is } } - /// @notice Verifies the SP1 proof. + /// @notice Verifies the SP1 proof /// @param proof The SP1 proof. function verifySP1Proof(SP1Proof memory proof) private view { VERIFIER.verifyProof(proof.vKey, proof.publicValues, proof.proof); } + /// @notice Verifies the SP1 proof and stores the hash of the proof. + /// @dev If the proof is already cached, it does not verify the proof again. + /// @param proof The SP1 proof. + function verifySP1ProofCached(SP1Proof memory proof) private { + bytes32 proofHash = keccak256(abi.encode(proof)); + if (verifiedProofs[proofHash]) { + return; + } + + VERIFIER.verifyProof(proof.vKey, proof.publicValues, proof.proof); + verifiedProofs[proofHash] = true; + } + /// @notice A dummy function to generate the ABI for the parameters. /// @param o1 The MembershipOutput. /// @param o2 The UcAndMembershipOutput. diff --git a/contracts/test/Membership.t.sol b/contracts/test/Membership.t.sol index f956837..49723da 100644 --- a/contracts/test/Membership.t.sol +++ b/contracts/test/Membership.t.sol @@ -41,24 +41,51 @@ contract SP1ICS07MembershipTest is MembershipTest { ics07Tendermint.membership(membershipMsg); - // to console - console.log("VerifyMultiMembership gas used: ", vm.lastCallGas().gasTotalUsed); + console.log("VerifyMembership gas used: ", vm.lastCallGas().gasTotalUsed); } // Modify the proof to make it a non-membership proof. function test_ValidVerifyNonMembership() public { - MsgMembership memory membershipMsg = MsgMembership({ + MsgMembership memory nonMembershipMsg = MsgMembership({ proof: abi.encode(fixture.membershipProof), proofHeight: fixture.proofHeight, path: verifyNonMembershipPath, value: bytes("") }); - ics07Tendermint.membership(membershipMsg); - // to console + ics07Tendermint.membership(nonMembershipMsg); + console.log("VerifyNonMembership gas used: ", vm.lastCallGas().gasTotalUsed); } + function test_ValidCachedMembership() public { + MsgMembership memory membershipMsg = MsgMembership({ + proof: abi.encode(fixture.membershipProof), + proofHeight: fixture.proofHeight, + path: verifyMembershipPath, + value: verifyMembershipValue() + }); + + ics07Tendermint.membership(membershipMsg); + + // resubmit the same proof + ics07Tendermint.membership(membershipMsg); + + console.log("Cached VerifyMembership gas used: ", vm.lastCallGas().gasTotalUsed); + + // resubmit the same proof as non-membership + MsgMembership memory nonMembershipMsg = MsgMembership({ + proof: abi.encode(fixture.membershipProof), + proofHeight: fixture.proofHeight, + path: verifyNonMembershipPath, + value: bytes("") + }); + + ics07Tendermint.membership(nonMembershipMsg); + + console.log("Cached VerifyNonMembership gas used: ", vm.lastCallGas().gasTotalUsed); + } + // Confirm that submitting an invalid proof with the real verifier fails. function test_Invalid_VerifyMembership() public { SP1MembershipProof memory proofMsg = proof; diff --git a/contracts/test/UcAndMembership.t.sol b/contracts/test/UcAndMembership.t.sol index 860a324..1cae5aa 100644 --- a/contracts/test/UcAndMembership.t.sol +++ b/contracts/test/UcAndMembership.t.sol @@ -44,7 +44,6 @@ contract SP1ICS07UpdateClientAndMembershipTest is MembershipTest { // run verify ics07Tendermint.membership(membershipMsg); - // to console console.log("UpdateClientAndVerifyMembership gas used: ", vm.lastCallGas().gasTotalUsed); ClientState memory clientState = ics07Tendermint.getClientState(); @@ -62,7 +61,7 @@ contract SP1ICS07UpdateClientAndMembershipTest is MembershipTest { // set a correct timestamp vm.warp(output.updateClientOutput.env.now + 300); - MsgMembership memory membershipMsg = MsgMembership({ + MsgMembership memory nonMembershipMsg = MsgMembership({ proof: abi.encode(fixture.membershipProof), proofHeight: fixture.proofHeight, path: verifyNonMembershipPath, @@ -70,9 +69,8 @@ contract SP1ICS07UpdateClientAndMembershipTest is MembershipTest { }); // run verify - ics07Tendermint.membership(membershipMsg); + ics07Tendermint.membership(nonMembershipMsg); - // to console console.log("UpdateClientAndVerifyNonMembership gas used: ", vm.lastCallGas().gasTotalUsed); ClientState memory clientState = ics07Tendermint.getClientState(); @@ -84,6 +82,48 @@ contract SP1ICS07UpdateClientAndMembershipTest is MembershipTest { assert(consensusHash == keccak256(abi.encode(output.updateClientOutput.newConsensusState))); } + // Confirm that submitting a real proof passes the verifier. + function test_Valid_CachedUpdateClientAndMembership() public { + UcAndMembershipOutput memory output = abi.decode(proof.sp1Proof.publicValues, (UcAndMembershipOutput)); + // set a correct timestamp + vm.warp(output.updateClientOutput.env.now + 300); + + MsgMembership memory membershipMsg = MsgMembership({ + proof: abi.encode(fixture.membershipProof), + proofHeight: fixture.proofHeight, + path: verifyMembershipPath, + value: verifyMembershipValue() + }); + + // run verify + ics07Tendermint.membership(membershipMsg); + + ClientState memory clientState = ics07Tendermint.getClientState(); + assert(clientState.latestHeight.revisionHeight == output.updateClientOutput.newHeight.revisionHeight); + assert(clientState.isFrozen == false); + + bytes32 consensusHash = + ics07Tendermint.getConsensusStateHash(output.updateClientOutput.newHeight.revisionHeight); + assert(consensusHash == keccak256(abi.encode(output.updateClientOutput.newConsensusState))); + + // resubmit the same proof + ics07Tendermint.membership(membershipMsg); + + console.log("Cached UpdateClientAndVerifyMembership gas used: ", vm.lastCallGas().gasTotalUsed); + + MsgMembership memory nonMembershipMsg = MsgMembership({ + proof: abi.encode(fixture.membershipProof), + proofHeight: fixture.proofHeight, + path: verifyNonMembershipPath, + value: bytes("") + }); + + // run verify + ics07Tendermint.membership(nonMembershipMsg); + + console.log("Cached UpdateClientAndNonVerifyMembership gas used: ", vm.lastCallGas().gasTotalUsed); + } + // Confirm that submitting a real proof passes the verifier. function test_Invalid_UpdateClientAndMembership() public { UcAndMembershipOutput memory output = abi.decode(proof.sp1Proof.publicValues, (UcAndMembershipOutput));