Skip to content
This repository has been archived by the owner on Nov 29, 2024. It is now read-only.

Commit

Permalink
imp: cache membership using transient store (#130)
Browse files Browse the repository at this point in the history
* feat: implemented

* imp: ran forge fmt

* imp: regenerated abi

* imp: generated new fixtures

* test: added large membership test

* style: forge fmt

* fix: removed broken test

* docs: improved natspec
  • Loading branch information
srdtrk authored Nov 11, 2024
1 parent c7462ad commit 07e23bb
Show file tree
Hide file tree
Showing 14 changed files with 363 additions and 36 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
- TestWithSP1ICS07TendermintTestSuite/TestDoubleSignMisbehaviour_Plonk
- TestWithSP1ICS07TendermintTestSuite/TestBreakingTimeMonotonicityMisbehaviour_Groth16
- TestWithSP1ICS07TendermintTestSuite/TestBreakingTimeMonotonicityMisbehaviour_Plonk
- TestWithSP1ICS07TendermintTestSuite/Test100Membership_Groth16
- TestWithSP1ICS07TendermintTestSuite/Test25Membership_Plonk
name: ${{ matrix.test }}
runs-on: ubuntu-latest
steps:
Expand Down
51 changes: 51 additions & 0 deletions contracts/abi/SP1ICS07Tendermint.json
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,25 @@
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "multicall",
"inputs": [
{
"name": "data",
"type": "bytes[]",
"internalType": "bytes[]"
}
],
"outputs": [
{
"name": "results",
"type": "bytes[]",
"internalType": "bytes[]"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "updateClient",
Expand Down Expand Up @@ -912,6 +931,17 @@
"outputs": [],
"stateMutability": "pure"
},
{
"type": "error",
"name": "AddressEmptyCode",
"inputs": [
{
"name": "target",
"type": "address",
"internalType": "address"
}
]
},
{
"type": "error",
"name": "CannotHandleMisbehavior",
Expand Down Expand Up @@ -986,6 +1016,11 @@
}
]
},
{
"type": "error",
"name": "FailedCall",
"inputs": []
},
{
"type": "error",
"name": "FeatureNotSupported",
Expand All @@ -1001,6 +1036,22 @@
"name": "InvalidMembershipProof",
"inputs": []
},
{
"type": "error",
"name": "KeyValuePairNotInCache",
"inputs": [
{
"name": "path",
"type": "bytes[]",
"internalType": "bytes[]"
},
{
"name": "value",
"type": "bytes",
"internalType": "bytes"
}
]
},
{
"type": "error",
"name": "LengthIsOutOfRange",
Expand Down
10 changes: 10 additions & 0 deletions contracts/fixtures/membership_100-groth16_fixture.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions contracts/fixtures/membership_25-plonk_fixture.json

Large diffs are not rendered by default.

77 changes: 51 additions & 26 deletions contracts/src/SP1ICS07Tendermint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ import { IUpdateClientMsgs } from "./msgs/IUpdateClientMsgs.sol";
import { IMembershipMsgs } from "./msgs/IMembershipMsgs.sol";
import { IUpdateClientAndMembershipMsgs } from "./msgs/IUcAndMembershipMsgs.sol";
import { IMisbehaviourMsgs } from "./msgs/IMisbehaviourMsgs.sol";
import { ISP1Verifier } from "@sp1-contracts/ISP1Verifier.sol";
import { ISP1ICS07TendermintErrors } from "./errors/ISP1ICS07TendermintErrors.sol";
import { ISP1ICS07Tendermint } from "./ISP1ICS07Tendermint.sol";
import { ILightClientMsgs } from "solidity-ibc/msgs/ILightClientMsgs.sol";
import { ILightClient } from "solidity-ibc/interfaces/ILightClient.sol";

import { Paths } from "./utils/Paths.sol";
import { UnionMembership } from "./utils/UnionMembership.sol";

import { ILightClientMsgs } from "solidity-ibc/msgs/ILightClientMsgs.sol";
import { ILightClient } from "solidity-ibc/interfaces/ILightClient.sol";

import { ISP1Verifier } from "@sp1-contracts/ISP1Verifier.sol";
import { SP1Verifier as SP1VerifierPlonk } from "@sp1-contracts/v3.0.0/SP1VerifierPlonk.sol";
import { SP1Verifier as SP1VerifierGroth16 } from "@sp1-contracts/v3.0.0/SP1VerifierGroth16.sol";

import { Multicall } from "@openzeppelin/utils/Multicall.sol";
import { TransientSlot } from "@openzeppelin/utils/TransientSlot.sol";

/// @title SP1 ICS07 Tendermint Light Client
/// @author srdtrk
/// @notice This contract implements an ICS07 IBC tendermint light client using SP1.
Expand All @@ -28,8 +34,11 @@ contract SP1ICS07Tendermint is
IMisbehaviourMsgs,
ISP1ICS07TendermintErrors,
ILightClientMsgs,
ISP1ICS07Tendermint
ISP1ICS07Tendermint,
Multicall
{
using TransientSlot for *;

/// @inheritdoc ISP1ICS07Tendermint
bytes32 public immutable UPDATE_CLIENT_PROGRAM_VKEY;
/// @inheritdoc ISP1ICS07Tendermint
Expand All @@ -45,8 +54,6 @@ 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
Expand Down Expand Up @@ -141,6 +148,13 @@ contract SP1ICS07Tendermint is
/// @return timestamp The timestamp of the trusted consensus state.
/// @inheritdoc ILightClient
function membership(MsgMembership calldata msgMembership) public returns (uint256 timestamp) {
if (msgMembership.proof.length == 0) {
// cached proof
return getCachedKvPair(
msgMembership.proofHeight.revisionHeight, KVPair(msgMembership.path, msgMembership.value)
);
}

MembershipProof memory membershipProof = abi.decode(msgMembership.proof, (MembershipProof));
if (membershipProof.proofType == MembershipProofType.SP1MembershipProof) {
return handleSP1MembershipProof(
Expand Down Expand Up @@ -260,13 +274,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);
if (output.kvPairs.length > 1) {
cacheKvPairs(proofHeight.revisionHeight, output.kvPairs, proof.trustedConsensusState.timestamp);
}

return proof.trustedConsensusState.timestamp;
}

Expand Down Expand Up @@ -314,12 +327,7 @@ contract SP1ICS07Tendermint is

validateUpdateClientPublicValues(output.updateClientOutput);

// 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);
}
verifySP1Proof(proof.sp1Proof);
}

// check update result
Expand Down Expand Up @@ -365,6 +373,12 @@ contract SP1ICS07Tendermint is
output.updateClientOutput.newConsensusState
);

// We avoid the cost of caching for single kv pairs, as reusing the proof is not necessary
if (output.kvPairs.length > 1) {
cacheKvPairs(
proofHeight.revisionHeight, output.kvPairs, output.updateClientOutput.newConsensusState.timestamp
);
}
return output.updateClientOutput.newConsensusState.timestamp;
}

Expand Down Expand Up @@ -502,17 +516,28 @@ contract SP1ICS07Tendermint is
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;
/// @notice Caches the key-value pairs to the transient storage with the timestamp.
/// @param proofHeight The height of the proof.
/// @param kvPairs The key-value pairs.
/// @param timestamp The timestamp of the trusted consensus state.
/// @dev WARNING: Transient store is not reverted even if a message within a transaction reverts.
/// @dev WARNING: This function must be called after all proof and validation checks.
function cacheKvPairs(uint32 proofHeight, KVPair[] memory kvPairs, uint256 timestamp) private {
for (uint8 i = 0; i < kvPairs.length; i++) {
bytes32 kvPairHash = keccak256(abi.encode(proofHeight, kvPairs[i]));
kvPairHash.asUint256().tstore(timestamp);
}
}

VERIFIER.verifyProof(proof.vKey, proof.publicValues, proof.proof);
verifiedProofs[proofHash] = true;
/// @notice Gets the timestamp of the cached key-value pair from the transient storage.
/// @param proofHeight The height of the proof.
/// @param kvPair The key-value pair.
/// @return The timestamp of the cached key-value pair.
function getCachedKvPair(uint32 proofHeight, KVPair memory kvPair) private view returns (uint256) {
bytes32 kvPairHash = keccak256(abi.encode(proofHeight, kvPair));
uint256 timestamp = kvPairHash.asUint256().tload();
require(timestamp != 0, KeyValuePairNotInCache(kvPair.path, kvPair.value));
return timestamp;
}

/// @notice A dummy function to generate the ABI for the parameters.
Expand Down
5 changes: 5 additions & 0 deletions contracts/src/errors/ISP1ICS07TendermintErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,9 @@ interface ISP1ICS07TendermintErrors {

/// @notice Returned when the membership proof is invalid.
error InvalidMembershipProof();

/// @notice Returned when a key-value pair is not in the cache.
/// @param path The path of the key-value pair.
/// @param value The value of the key-value pair.
error KeyValuePairNotInCache(bytes[] path, bytes value);
}
64 changes: 64 additions & 0 deletions contracts/test/LargeMembership.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

// solhint-disable-next-line no-global-import
import "forge-std/console.sol";
import { MembershipTest } from "./MembershipTest.sol";
import { ILightClient } from "solidity-ibc/interfaces/ILightClient.sol";

contract SP1ICS07LargeMembershipTest is MembershipTest {
SP1MembershipProof public proof;

function setUpLargeMembershipTestWithFixture(string memory fileName) public {
setUpTestWithFixtures(fileName);

proof = abi.decode(fixture.membershipProof.proof, (SP1MembershipProof));
}

function getOutput() public view returns (MembershipOutput memory) {
return abi.decode(proof.sp1Proof.publicValues, (MembershipOutput));
}

function test_ValidLargeCachedVerifyMembership_25_plonk() public {
ValidCachedMulticallMembershipTest("membership_25-plonk_fixture.json", 25, "25 key-value pairs with plonk");
}

function test_ValidLargeCachedVerifyMembership_100_groth16() public {
ValidCachedMulticallMembershipTest(
"membership_100-groth16_fixture.json", 100, "100 key-value pairs with groth16"
);
}

function ValidCachedMulticallMembershipTest(string memory fileName, uint32 n, string memory metadata) public {
require(n > 0, "n must be greater than 0");

setUpLargeMembershipTestWithFixture(fileName);

bytes[] memory multicallData = new bytes[](n);

multicallData[0] = abi.encodeCall(
ILightClient.membership,
MsgMembership({
proof: abi.encode(fixture.membershipProof),
proofHeight: fixture.proofHeight,
path: getOutput().kvPairs[0].path,
value: getOutput().kvPairs[0].value
})
);

for (uint32 i = 1; i < n; i++) {
multicallData[i] = abi.encodeCall(
ILightClient.membership,
MsgMembership({
proof: bytes(""), // cached kv pairs
proofHeight: fixture.proofHeight,
path: getOutput().kvPairs[i].path,
value: getOutput().kvPairs[i].value
})
);
}

ics07Tendermint.multicall(multicallData);
console.log("Proved", metadata, ", gas used: ", vm.lastCallGas().gasTotalUsed);
}
}
28 changes: 22 additions & 6 deletions contracts/test/Membership.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,38 @@ contract SP1ICS07MembershipTest is MembershipTest {

ics07Tendermint.membership(membershipMsg);

// resubmit the same proof
ics07Tendermint.membership(membershipMsg);
// resubmit cached membership proof
MsgMembership memory cachedMembershipMsg = MsgMembership({
proof: bytes(""),
proofHeight: fixture.proofHeight,
path: verifyMembershipPath,
value: VERIFY_MEMBERSHIP_VALUE
});
ics07Tendermint.membership(cachedMembershipMsg);

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),
// resubmit cached non-membership proof
MsgMembership memory cachedNonMembershipMsg = MsgMembership({
proof: bytes(""),
proofHeight: fixture.proofHeight,
path: verifyNonMembershipPath,
value: bytes("")
});

ics07Tendermint.membership(nonMembershipMsg);
ics07Tendermint.membership(cachedNonMembershipMsg);

console.log("Cached VerifyNonMembership gas used: ", vm.lastCallGas().gasTotalUsed);

// resubmit invalid cached membership proof
MsgMembership memory invalidCachedMembershipMsg = MsgMembership({
proof: bytes(""),
proofHeight: fixture.proofHeight,
path: verifyMembershipPath,
value: bytes("invalid")
});
vm.expectRevert(abi.encodeWithSelector(KeyValuePairNotInCache.selector, verifyMembershipPath, bytes("invalid")));
ics07Tendermint.membership(invalidCachedMembershipMsg);
}

// Confirm that submitting an invalid proof with the real verifier fails.
Expand Down
13 changes: 10 additions & 3 deletions contracts/test/UcAndMembership.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,20 @@ contract SP1ICS07UpdateClientAndMembershipTest is MembershipTest {
ics07Tendermint.getConsensusStateHash(output.updateClientOutput.newHeight.revisionHeight);
assert(consensusHash == keccak256(abi.encode(output.updateClientOutput.newConsensusState)));

// resubmit the same proof
ics07Tendermint.membership(membershipMsg);
// submit cached membership proof
MsgMembership memory cachedMembershipMsg = MsgMembership({
proof: bytes(""),
proofHeight: fixture.proofHeight,
path: verifyMembershipPath,
value: VERIFY_MEMBERSHIP_VALUE
});
ics07Tendermint.membership(cachedMembershipMsg);

console.log("Cached UpdateClientAndVerifyMembership gas used: ", vm.lastCallGas().gasTotalUsed);

// submit cached non-membership proof
MsgMembership memory nonMembershipMsg = MsgMembership({
proof: abi.encode(fixture.membershipProof),
proof: bytes(""),
proofHeight: fixture.proofHeight,
path: verifyNonMembershipPath,
value: bytes("")
Expand Down
1 change: 1 addition & 0 deletions e2e/interchaintestv8/operator/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func StartOperator(args ...string) error {
args = append([]string{"start"}, args...)
cmd := exec.Command("target/release/operator", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

Expand Down
Loading

0 comments on commit 07e23bb

Please sign in to comment.