From 208b4c4b243ca082fceeeb789e8968cf903fc697 Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Tue, 3 Dec 2024 15:43:05 +0000 Subject: [PATCH 01/22] feat(prover): support UUPS proxy and storage solution --- bun.lockb | Bin 265100 -> 279188 bytes contracts/core/Secp256k1Prover.sol | 145 ----------- contracts/core/SedaCorePermissioned.sol | 2 +- contracts/core/SedaCoreV1.sol | 4 +- .../abstract/RequestHandlerBase.sol | 4 +- .../{ => core}/abstract/ResultHandlerBase.sol | 6 +- .../core/{ => handlers}/RequestHandler.sol | 2 +- .../core/{ => handlers}/ResultHandler.sol | 2 +- contracts/interfaces/IProver.sol | 2 +- contracts/libraries/SedaDataTypes.sol | 12 +- contracts/provers/Secp256k1ProverV1.sol | 244 ++++++++++++++++++ .../{ => provers}/abstract/ProverBase.sol | 10 +- hardhat.config.ts | 3 +- package.json | 4 +- 14 files changed, 273 insertions(+), 167 deletions(-) delete mode 100644 contracts/core/Secp256k1Prover.sol rename contracts/{ => core}/abstract/RequestHandlerBase.sol (79%) rename contracts/{ => core}/abstract/ResultHandlerBase.sol (82%) rename contracts/core/{ => handlers}/RequestHandler.sol (97%) rename contracts/core/{ => handlers}/ResultHandler.sol (97%) create mode 100644 contracts/provers/Secp256k1ProverV1.sol rename contracts/{ => provers}/abstract/ProverBase.sol (63%) diff --git a/bun.lockb b/bun.lockb index 578eb77f7386b13b86900338f7571f189c09e498..fb3855c97f020668e94de411a22fcbdacfeb2f67 100755 GIT binary patch delta 60032 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 delta 52071 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 BX^sE@ diff --git a/contracts/core/Secp256k1Prover.sol b/contracts/core/Secp256k1Prover.sol deleted file mode 100644 index 8d0b696..0000000 --- a/contracts/core/Secp256k1Prover.sol +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; -import {ProverBase} from "../abstract/ProverBase.sol"; -import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; - -/// @title Secp256k1Prover -/// @notice Implements the ProverBase for Secp256k1 signature verification in the Seda protocol -/// @dev This contract manages batch updates and result proof verification using Secp256k1 signatures. -/// Batch validity is determined by consensus among validators, requiring: -/// - Increasing batch and block heights -/// - Valid validator proofs and signatures -/// - Sufficient voting power to meet the consensus threshold -contract Secp256k1Prover is ProverBase { - error ConsensusNotReached(); - - // The percentage of voting power required for consensus (66.666666%, represented as parts per 100,000,000) - uint32 public constant CONSENSUS_PERCENTAGE = 66_666_666; - // Domain separator for Secp256k1 Merkle Tree leaves - bytes1 internal constant SECP256K1_DOMAIN_SEPARATOR = 0x01; - - // Mapping to store results roots by their batch height - mapping(uint64 => bytes32) public batchToResultsRoot; - - // The height of the last processed batch - uint64 public lastBatchHeight; - // The validator root of the last processed batch - bytes32 public lastValidatorsRoot; - - /// @notice Initializes the contract with a predefined batch size - /// @param initialBatch The initial batch data - constructor(SedaDataTypes.Batch memory initialBatch) { - batchToResultsRoot[initialBatch.batchHeight] = initialBatch.resultsRoot; - lastBatchHeight = initialBatch.batchHeight; - lastValidatorsRoot = initialBatch.validatorsRoot; - emit BatchPosted( - initialBatch.batchHeight, - SedaDataTypes.deriveBatchId(initialBatch) - ); - } - - /// @inheritdoc ProverBase - /// @notice Posts a new batch with new data, ensuring validity through consensus - /// @dev Validates a new batch by checking: - /// 1. Higher batch height than the current batch - /// 2. Matching number of signatures and validator proofs - /// 3. Valid validator proofs (verified against the batch's validator root) - /// 4. Valid signatures (signed by the corresponding validators) - /// 5. Sufficient voting power to meet or exceed the consensus threshold - /// @param newBatch The new batch data to be validated and set as current - /// @param signatures Array of signatures from validators approving the new batch - /// @param validatorProofs Array of validator proofs corresponding to the signatures - function postBatch( - SedaDataTypes.Batch calldata newBatch, - bytes[] calldata signatures, - SedaDataTypes.ValidatorProof[] calldata validatorProofs - ) public override { - // Check that new batch invariants hold - if (newBatch.batchHeight <= lastBatchHeight) { - revert InvalidBatchHeight(); - } - if (signatures.length != validatorProofs.length) { - revert MismatchedSignaturesAndProofs(); - } - - // Derive Batch Id - bytes32 batchId = SedaDataTypes.deriveBatchId(newBatch); - - // Check that all validator proofs are valid and accumulate voting power - uint64 votingPower = 0; - for (uint256 i = 0; i < validatorProofs.length; i++) { - if ( - !_verifyValidatorProof(validatorProofs[i], lastValidatorsRoot) - ) { - revert InvalidValidatorProof(); - } - if ( - !_verifySignature( - batchId, - signatures[i], - validatorProofs[i].signer - ) - ) { - revert InvalidSignature(); - } - votingPower += validatorProofs[i].votingPower; - } - - // Check voting power consensus - if (votingPower < CONSENSUS_PERCENTAGE) { - revert ConsensusNotReached(); - } - - // Update current batch - lastBatchHeight = newBatch.batchHeight; - lastValidatorsRoot = newBatch.validatorsRoot; - batchToResultsRoot[newBatch.batchHeight] = newBatch.resultsRoot; - emit BatchPosted(newBatch.batchHeight, batchId); - } - - /// @inheritdoc ProverBase - function verifyResultProof( - bytes32 resultId, - uint64 batchHeight, - bytes32[] calldata merkleProof - ) public view override returns (bool) { - bytes32 leaf = keccak256( - abi.encodePacked(RESULT_DOMAIN_SEPARATOR, resultId) - ); - return MerkleProof.verify(merkleProof, batchToResultsRoot[batchHeight], leaf); - } - - /// @notice Verifies a validator proof - /// @param proof The validator proof to verify - /// @return bool Returns true if the proof is valid, false otherwise - function _verifyValidatorProof( - SedaDataTypes.ValidatorProof memory proof, - bytes32 validatorsRoot - ) internal pure returns (bool) { - bytes32 leaf = keccak256( - abi.encodePacked( - SECP256K1_DOMAIN_SEPARATOR, - proof.signer, - proof.votingPower - ) - ); - - return MerkleProof.verify(proof.merkleProof, validatorsRoot, leaf); - } - - /// @notice Verifies a signature against a message hash and its address - /// @param messageHash The hash of the message that was signed - /// @param signature The signature to verify - /// @param signer The validator Secp256k1 address signer - /// @return bool Returns true if the signature is valid, false otherwise - function _verifySignature( - bytes32 messageHash, - bytes calldata signature, - address signer - ) internal pure returns (bool) { - return ECDSA.recover(messageHash, signature) == signer; - } -} diff --git a/contracts/core/SedaCorePermissioned.sol b/contracts/core/SedaCorePermissioned.sol index 9c1e4fd..e7c9186 100644 --- a/contracts/core/SedaCorePermissioned.sol +++ b/contracts/core/SedaCorePermissioned.sol @@ -6,7 +6,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {IResultHandler} from "../interfaces/IResultHandler.sol"; -import {RequestHandlerBase} from "../abstract/RequestHandlerBase.sol"; +import {RequestHandlerBase} from "./abstract/RequestHandlerBase.sol"; import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; /// @title SedaCorePermissioned diff --git a/contracts/core/SedaCoreV1.sol b/contracts/core/SedaCoreV1.sol index a908027..b76ca09 100644 --- a/contracts/core/SedaCoreV1.sol +++ b/contracts/core/SedaCoreV1.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.9; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {RequestHandler} from "./handlers/RequestHandler.sol"; +import {ResultHandler} from "./handlers/ResultHandler.sol"; import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; -import {ResultHandler} from "./ResultHandler.sol"; -import {RequestHandler} from "./RequestHandler.sol"; /// @title SedaCoreV1 /// @notice Core contract for the Seda protocol, managing requests and results diff --git a/contracts/abstract/RequestHandlerBase.sol b/contracts/core/abstract/RequestHandlerBase.sol similarity index 79% rename from contracts/abstract/RequestHandlerBase.sol rename to contracts/core/abstract/RequestHandlerBase.sol index 56e5c2e..702c0bb 100644 --- a/contracts/abstract/RequestHandlerBase.sol +++ b/contracts/core/abstract/RequestHandlerBase.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; -import {IRequestHandler} from "../interfaces/IRequestHandler.sol"; -import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; +import {IRequestHandler} from "../../interfaces/IRequestHandler.sol"; +import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; abstract contract RequestHandlerBase is IRequestHandler { /// @inheritdoc IRequestHandler diff --git a/contracts/abstract/ResultHandlerBase.sol b/contracts/core/abstract/ResultHandlerBase.sol similarity index 82% rename from contracts/abstract/ResultHandlerBase.sol rename to contracts/core/abstract/ResultHandlerBase.sol index b30ebed..f49dd82 100644 --- a/contracts/abstract/ResultHandlerBase.sol +++ b/contracts/core/abstract/ResultHandlerBase.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; -import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; -import {IProver} from "../interfaces/IProver.sol"; -import {IResultHandler} from "../interfaces/IResultHandler.sol"; +import {IProver} from "../../interfaces/IProver.sol"; +import {IResultHandler} from "../../interfaces/IResultHandler.sol"; +import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; abstract contract ResultHandlerBase is IResultHandler { IProver public sedaProver; diff --git a/contracts/core/RequestHandler.sol b/contracts/core/handlers/RequestHandler.sol similarity index 97% rename from contracts/core/RequestHandler.sol rename to contracts/core/handlers/RequestHandler.sol index fd0a245..d449b0b 100644 --- a/contracts/core/RequestHandler.sol +++ b/contracts/core/handlers/RequestHandler.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; -import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; +import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; import {RequestHandlerBase} from "../abstract/RequestHandlerBase.sol"; /// @title RequestHandler diff --git a/contracts/core/ResultHandler.sol b/contracts/core/handlers/ResultHandler.sol similarity index 97% rename from contracts/core/ResultHandler.sol rename to contracts/core/handlers/ResultHandler.sol index 0e2d7da..a45407e 100644 --- a/contracts/core/ResultHandler.sol +++ b/contracts/core/handlers/ResultHandler.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; -import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; +import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; import {ResultHandlerBase} from "../abstract/ResultHandlerBase.sol"; /// @title ResultHandler diff --git a/contracts/interfaces/IProver.sol b/contracts/interfaces/IProver.sol index 9db48db..202d5b1 100644 --- a/contracts/interfaces/IProver.sol +++ b/contracts/interfaces/IProver.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.24; import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; diff --git a/contracts/libraries/SedaDataTypes.sol b/contracts/libraries/SedaDataTypes.sol index f9ac0b5..8391d90 100644 --- a/contracts/libraries/SedaDataTypes.sol +++ b/contracts/libraries/SedaDataTypes.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.9; /// @title SedaDataTypes Library /// @notice Contains data structures and utility functions for the SEDA protocol library SedaDataTypes { - string public constant VERSION = "0.0.1"; + string internal constant VERSION = "0.0.1"; /// @notice Input parameters for creating a data request struct RequestInputs { @@ -105,7 +105,7 @@ library SedaDataTypes { /// @notice Derives a unique batch ID from a Batch struct /// @param batch The Batch struct to derive the ID from /// @return The derived batch ID - function deriveBatchId(Batch calldata batch) public pure returns (bytes32) { + function deriveBatchId(Batch memory batch) internal pure returns (bytes32) { return keccak256( bytes.concat( @@ -122,8 +122,8 @@ library SedaDataTypes { /// @param result The Result struct to derive the ID from /// @return The derived result ID function deriveResultId( - SedaDataTypes.Result calldata result - ) public pure returns (bytes32) { + Result memory result + ) internal pure returns (bytes32) { return keccak256( bytes.concat( @@ -145,8 +145,8 @@ library SedaDataTypes { /// @param inputs The RequestInputs struct to derive the ID from /// @return The derived request ID function deriveRequestId( - SedaDataTypes.RequestInputs calldata inputs - ) public pure returns (bytes32) { + RequestInputs memory inputs + ) internal pure returns (bytes32) { return keccak256( bytes.concat( diff --git a/contracts/provers/Secp256k1ProverV1.sol b/contracts/provers/Secp256k1ProverV1.sol new file mode 100644 index 0000000..b2baaa5 --- /dev/null +++ b/contracts/provers/Secp256k1ProverV1.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import {ProverBase} from "./abstract/ProverBase.sol"; +import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; + +/// @title Secp256k1ProverV1 +/// @notice Implements the ProverBase for Secp256k1 signature verification in the Seda protocol +/// @dev This contract manages batch updates and result proof verification using Secp256k1 signatures. +/// Batch validity is determined by consensus among validators, requiring: +/// - Increasing batch and block heights +/// - Valid validator proofs and signatures +/// - Sufficient voting power to meet the consensus threshold +contract Secp256k1ProverV1 is + ProverBase, + Initializable, + UUPSUpgradeable, + OwnableUpgradeable +{ + // ============ Errors ============ + // Error thrown when consensus is not reached + error ConsensusNotReached(); + + // ============ Constants ============ + + // The percentage of voting power required for consensus (66.666666%, represented as parts per 100,000,000) + uint32 public constant CONSENSUS_PERCENTAGE = 66_666_666; + // Domain separator for Secp256k1 Merkle Tree leaves + bytes1 internal constant SECP256K1_DOMAIN_SEPARATOR = 0x01; + // Constant storage slot for the state following the ERC-7201 standard + bytes32 private constant STORAGE_SLOT = + keccak256( + abi.encode(uint256(keccak256("secp256k1prover.v1.storage")) - 1) + ) & ~bytes32(uint256(0xff)); + + // ============ Storage ============ + + /// @custom:storage-location secp256k1prover.v1.storage + struct Secp256k1ProverStorage { + uint64 lastBatchHeight; + bytes32 lastValidatorsRoot; + mapping(uint64 => bytes32) batchToResultsRoot; + } + + // ============ Constructor & Initializer ============ + + /// @notice Initializes the contract with initial batch data + /// @dev Sets up the contract's initial state and initializes inherited contracts + /// @param initialBatch The initial batch data containing height, validators root, and results root + function initialize( + SedaDataTypes.Batch memory initialBatch + ) public initializer { + // Initialize inherited contracts + __Ownable_init(msg.sender); + __UUPSUpgradeable_init(); + + // Existing initialization code + Secp256k1ProverStorage storage s = _storage(); + s.batchToResultsRoot[initialBatch.batchHeight] = initialBatch + .resultsRoot; + s.lastBatchHeight = initialBatch.batchHeight; + s.lastValidatorsRoot = initialBatch.validatorsRoot; + emit BatchPosted( + initialBatch.batchHeight, + SedaDataTypes.deriveBatchId(initialBatch) + ); + } + + // ============ External Functions ============ + + /// @inheritdoc ProverBase + /// @notice Posts a new batch with new data, ensuring validity through consensus + /// @dev Validates a new batch by checking: + /// 1. Higher batch height than the current batch + /// 2. Matching number of signatures and validator proofs + /// 3. Valid validator proofs (verified against the batch's validator root) + /// 4. Valid signatures (signed by the corresponding validators) + /// 5. Sufficient voting power to meet or exceed the consensus threshold + /// @param newBatch The new batch data to be validated and set as current + /// @param signatures Array of signatures from validators approving the new batch + /// @param validatorProofs Array of validator proofs corresponding to the signatures + function postBatch( + SedaDataTypes.Batch calldata newBatch, + bytes[] calldata signatures, + SedaDataTypes.ValidatorProof[] calldata validatorProofs + ) public override { + Secp256k1ProverStorage storage s = _storage(); + // Check that new batch invariants hold + if (newBatch.batchHeight <= s.lastBatchHeight) { + revert InvalidBatchHeight(); + } + if (signatures.length != validatorProofs.length) { + revert MismatchedSignaturesAndProofs(); + } + + // Derive Batch Id + bytes32 batchId = SedaDataTypes.deriveBatchId(newBatch); + + // Check that all validator proofs are valid and accumulate voting power + uint64 votingPower = 0; + for (uint256 i = 0; i < validatorProofs.length; i++) { + if ( + !_verifyValidatorProof(validatorProofs[i], s.lastValidatorsRoot) + ) { + revert InvalidValidatorProof(); + } + if ( + !_verifySignature( + batchId, + signatures[i], + validatorProofs[i].signer + ) + ) { + revert InvalidSignature(); + } + votingPower += validatorProofs[i].votingPower; + } + + // Check voting power consensus + if (votingPower < CONSENSUS_PERCENTAGE) { + revert ConsensusNotReached(); + } + + // Update current batch + s.lastBatchHeight = newBatch.batchHeight; + s.lastValidatorsRoot = newBatch.validatorsRoot; + s.batchToResultsRoot[newBatch.batchHeight] = newBatch.resultsRoot; + emit BatchPosted(newBatch.batchHeight, batchId); + } + + // ============ Public View Functions ============ + + /// @notice Verifies a result proof against a batch's results root + /// @param resultId The ID of the result to verify + /// @param batchHeight The height of the batch containing the result + /// @param merkleProof The Merkle proof for the result + /// @return bool Returns true if the proof is valid, false otherwise + function verifyResultProof( + bytes32 resultId, + uint64 batchHeight, + bytes32[] calldata merkleProof + ) public view override returns (bool) { + Secp256k1ProverStorage storage s = _storage(); + bytes32 leaf = keccak256( + abi.encodePacked(RESULT_DOMAIN_SEPARATOR, resultId) + ); + return + MerkleProof.verify( + merkleProof, + s.batchToResultsRoot[batchHeight], + leaf + ); + } + + /// @notice Returns the last processed batch height + /// @return The height of the last batch + function getLastBatchHeight() public view returns (uint64) { + return _storage().lastBatchHeight; + } + + /// @notice Returns the last validators root hash + /// @return The Merkle root of the last validator set + function getLastValidatorsRoot() public view returns (bytes32) { + return _storage().lastValidatorsRoot; + } + + /// @notice Returns the results root for a specific batch height + /// @param batchHeight The batch height to query + /// @return The results root for the specified batch + function getBatchResultsRoot( + uint64 batchHeight + ) public view returns (bytes32) { + return _storage().batchToResultsRoot[batchHeight]; + } + + // ============ Internal Functions ============ + + /// @notice Returns the storage struct for the contract + /// @dev Uses ERC-7201 storage pattern to access the storage struct at a specific slot + /// @return s The storage struct containing the contract's state variables + function _storage() + internal + pure + returns (Secp256k1ProverStorage storage s) + { + bytes32 slot = STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + s.slot := slot + } + } + + /// @notice Verifies a validator proof against the validators root + /// @dev Constructs a leaf using SECP256K1_DOMAIN_SEPARATOR and verifies it against the validators root + /// @param proof The validator proof containing signer, voting power, and Merkle proof + /// @param validatorsRoot The root hash to verify against + /// @return bool Returns true if the proof is valid, false otherwise + function _verifyValidatorProof( + SedaDataTypes.ValidatorProof memory proof, + bytes32 validatorsRoot + ) internal pure returns (bool) { + bytes32 leaf = keccak256( + abi.encodePacked( + SECP256K1_DOMAIN_SEPARATOR, + proof.signer, + proof.votingPower + ) + ); + + return MerkleProof.verify(proof.merkleProof, validatorsRoot, leaf); + } + + /// @notice Verifies a signature against a message hash and its address + /// @param messageHash The hash of the message that was signed + /// @param signature The signature to verify + /// @param signer The validator Secp256k1 address signer + /// @return bool Returns true if the signature is valid, false otherwise + function _verifySignature( + bytes32 messageHash, + bytes calldata signature, + address signer + ) internal pure returns (bool) { + return ECDSA.recover(messageHash, signature) == signer; + } + + /// @dev Required override for UUPSUpgradeable. Ensures only the owner can upgrade the implementation. + /// @inheritdoc UUPSUpgradeable + /// @param newImplementation Address of the new implementation contract + function _authorizeUpgrade( + address newImplementation + ) + internal + virtual + override + onlyOwner + // solhint-disable-next-line no-empty-blocks + {} +} diff --git a/contracts/abstract/ProverBase.sol b/contracts/provers/abstract/ProverBase.sol similarity index 63% rename from contracts/abstract/ProverBase.sol rename to contracts/provers/abstract/ProverBase.sol index 592689c..da677a3 100644 --- a/contracts/abstract/ProverBase.sol +++ b/contracts/provers/abstract/ProverBase.sol @@ -1,15 +1,19 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.24; -import {IProver} from "../interfaces/IProver.sol"; -import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; +import {IProver} from "../../interfaces/IProver.sol"; +import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; +/// @title ProverBase +/// @notice Base contract for implementing proof verification logic +/// @dev This abstract contract defines the basic structure and error handling for proof verification abstract contract ProverBase is IProver { error InvalidBatchHeight(); error InvalidSignature(); error InvalidValidatorProof(); error MismatchedSignaturesAndProofs(); + // Domain separator used to prevent cross-domain replay attacks when hashing result IDs bytes1 internal constant RESULT_DOMAIN_SEPARATOR = 0x00; /// @inheritdoc IProver diff --git a/hardhat.config.ts b/hardhat.config.ts index 6d52d63..421ca5c 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,6 +1,7 @@ import type { HardhatUserConfig } from 'hardhat/config'; import '@nomicfoundation/hardhat-toolbox'; import { getEtherscanConfig, getNetworksConfig } from './config'; +import '@openzeppelin/hardhat-upgrades'; const gasReporterConfig = { currency: 'USD', @@ -14,7 +15,7 @@ const config: HardhatUserConfig = { enabled: false, }, solidity: { - version: '0.8.25', + version: '0.8.24', settings: { optimizer: { enabled: true, diff --git a/package.json b/package.json index f08874c..af5d763 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@openzeppelin/merkle-tree": "^1.0.7", + "@openzeppelin/hardhat-upgrades": "^3.6.0", "@nomicfoundation/hardhat-toolbox": "^5.0.0", "hardhat": "^2.22.16", "dotenv": "^16.4.5", @@ -29,6 +30,7 @@ "solhint": "^5.0.3" }, "dependencies": { - "@openzeppelin/contracts": "5.1.0" + "@openzeppelin/contracts": "5.1.0", + "@openzeppelin/contracts-upgradeable": "5.1.0" } } From ee8cd26d94d306f6624ef8a509e389c24455a26e Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Tue, 3 Dec 2024 15:43:49 +0000 Subject: [PATCH 02/22] test: update contracts to latest changes --- test/RequestHandler.test.ts | 10 +------- test/ResultHandler.test.ts | 19 ++++----------- test/Secp256k1Prover.test.ts | 45 +++++++++++++++-------------------- test/SedaCoreV1.test.ts | 14 ++++------- test/SedaPermissioned.test.ts | 7 +----- test/utils.ts | 22 ++++++++--------- 6 files changed, 40 insertions(+), 77 deletions(-) diff --git a/test/RequestHandler.test.ts b/test/RequestHandler.test.ts index f973923..ea5b422 100644 --- a/test/RequestHandler.test.ts +++ b/test/RequestHandler.test.ts @@ -9,16 +9,8 @@ describe('RequestHandler', () => { async function deployRequestHandlerFixture() { const { requests } = generateDataFixtures(4); - // Deploy the SedaDataTypes library first - const DataTypesFactory = await ethers.getContractFactory('SedaDataTypes'); - const dataTypes = await DataTypesFactory.deploy(); - // Deploy the RequestHandler contract - const RequestHandlerFactory = await ethers.getContractFactory('RequestHandler', { - libraries: { - SedaDataTypes: await dataTypes.getAddress(), - }, - }); + const RequestHandlerFactory = await ethers.getContractFactory('RequestHandler'); const handler = await RequestHandlerFactory.deploy(); return { handler, requests }; diff --git a/test/ResultHandler.test.ts b/test/ResultHandler.test.ts index 95e6f26..9ecbfde 100644 --- a/test/ResultHandler.test.ts +++ b/test/ResultHandler.test.ts @@ -32,23 +32,12 @@ describe('ResultHandler', () => { provingMetadata: ethers.ZeroHash, }; - // Deploy the SedaDataTypes library first - const DataTypesFactory = await ethers.getContractFactory('SedaDataTypes'); - const dataTypes = await DataTypesFactory.deploy(); - // Deploy the contract - const ProverFactory = await ethers.getContractFactory('Secp256k1Prover', { - libraries: { - SedaDataTypes: await dataTypes.getAddress(), - }, - }); - const prover = await ProverFactory.deploy(initialBatch); + const ProverFactory = await ethers.getContractFactory('Secp256k1ProverV1'); + const prover = await ProverFactory.deploy(); + await prover.initialize(initialBatch); - const ResultHandlerFactory = await ethers.getContractFactory('ResultHandler', { - libraries: { - SedaDataTypes: await dataTypes.getAddress(), - }, - }); + const ResultHandlerFactory = await ethers.getContractFactory('ResultHandler'); const handler = await ResultHandlerFactory.deploy(prover.getAddress()); return { handler, data }; diff --git a/test/Secp256k1Prover.test.ts b/test/Secp256k1Prover.test.ts index 74986b3..79296b6 100644 --- a/test/Secp256k1Prover.test.ts +++ b/test/Secp256k1Prover.test.ts @@ -73,16 +73,15 @@ describe('Secp256k1Prover', () => { }; // Deploy the SedaDataTypes library first - const DataTypesFactory = await ethers.getContractFactory('SedaDataTypes'); - const dataTypes = await DataTypesFactory.deploy(); + // const DataTypesFactory = await ethers.getContractFactory('SedaDataTypes'); + // const dataTypes = await DataTypesFactory.deploy(); // Deploy the contract - const ProverFactory = await ethers.getContractFactory('Secp256k1Prover', { - libraries: { - SedaDataTypes: await dataTypes.getAddress(), - }, - }); - const prover = await ProverFactory.deploy(initialBatch); + const ProverFactory = await ethers.getContractFactory('Secp256k1ProverV1'); + const prover = await ProverFactory.deploy(); + await prover.initialize(initialBatch); + + // const prover = await upgrades.deployProxy(ProverFactory, [initialBatch]); return { prover, wallets, data }; } @@ -100,7 +99,7 @@ describe('Secp256k1Prover', () => { prover, data: { initialBatch }, } = await loadFixture(deployProverFixture); - const lastBatchHeight = await prover.lastBatchHeight(); + const lastBatchHeight = await prover.getLastBatchHeight(); expect(lastBatchHeight).to.equal(initialBatch.batchHeight); }); }); @@ -112,8 +111,8 @@ describe('Secp256k1Prover', () => { const { newBatch, signatures } = await generateAndSignBatch(wallets, data.initialBatch, [0]); await prover.postBatch(newBatch, signatures, [data.validatorProofs[0]]); - const lastBatchHeight = await prover.lastBatchHeight(); - const lastValidatorsRoot = await prover.lastValidatorsRoot(); + const lastBatchHeight = await prover.getLastBatchHeight(); + const lastValidatorsRoot = await prover.getLastValidatorsRoot(); expect(lastBatchHeight).to.equal(newBatch.batchHeight); expect(lastValidatorsRoot).to.equal(newBatch.validatorsRoot); }); @@ -124,8 +123,8 @@ describe('Secp256k1Prover', () => { const { newBatch, signatures } = await generateAndSignBatch(wallets, data.initialBatch, [1, 2, 3]); await prover.postBatch(newBatch, signatures, data.validatorProofs.slice(1)); - const lastBatchHeight = await prover.lastBatchHeight(); - const lastValidatorsRoot = await prover.lastValidatorsRoot(); + const lastBatchHeight = await prover.getLastBatchHeight(); + const lastValidatorsRoot = await prover.getLastValidatorsRoot(); expect(lastBatchHeight).to.equal(newBatch.batchHeight); expect(lastValidatorsRoot).to.equal(newBatch.validatorsRoot); }); @@ -151,7 +150,7 @@ describe('Secp256k1Prover', () => { 'ConsensusNotReached', ); - const lastBatchHeight = await prover.lastBatchHeight(); + const lastBatchHeight = await prover.getLastBatchHeight(); expect(lastBatchHeight).to.equal(data.initialBatch.batchHeight); }); @@ -218,7 +217,7 @@ describe('Secp256k1Prover', () => { await prover.postBatch(newBatch, signatures, data.validatorProofs); - const lastBatchHeight = await prover.lastBatchHeight(); + const lastBatchHeight = await prover.getLastBatchHeight(); expect(lastBatchHeight).to.equal(newBatch.batchHeight); }); }); @@ -338,7 +337,7 @@ describe('Secp256k1Prover', () => { wallets.slice(0, validatorCount).map((wallet) => wallet.signingKey.sign(newBatchId).serialized), ); await prover.postBatch(newBatch, signatures, data.validatorProofs.slice(0, validatorCount)); - const lastBatchHeight = await prover.lastBatchHeight(); + const lastBatchHeight = await prover.getLastBatchHeight(); expect(lastBatchHeight).to.equal(newBatch.batchHeight); } } @@ -364,17 +363,11 @@ describe('Secp256k1Prover', () => { const expectedBatchId = deriveBatchId(testBatch); expect(expectedBatchId).to.equal('0x9b8a1c156da9096bc89288e9d64df3c897435e962ae7402f0c25c97f3de76e94'); - // Deploy the SedaDataTypes library first - const DataTypesFactory = await ethers.getContractFactory('SedaDataTypes'); - const dataTypes = await DataTypesFactory.deploy(); - // Deploy the contract - const ProverFactory = await ethers.getContractFactory('Secp256k1Prover', { - libraries: { - SedaDataTypes: await dataTypes.getAddress(), - }, - }); - const prover = await ProverFactory.deploy(testBatch); + const ProverFactory = await ethers.getContractFactory('Secp256k1ProverV1'); + const prover = await ProverFactory.deploy(); + await prover.initialize(testBatch); + expect(prover).to.emit(prover, 'BatchPosted').withArgs(testBatch.batchHeight, expectedBatchId); }); }); diff --git a/test/SedaCoreV1.test.ts b/test/SedaCoreV1.test.ts index 44d42e6..df24819 100644 --- a/test/SedaCoreV1.test.ts +++ b/test/SedaCoreV1.test.ts @@ -25,17 +25,11 @@ describe('SedaCoreV1', () => { provingMetadata: ethers.ZeroHash, }; - const DataTypesFactory = await ethers.getContractFactory('SedaDataTypes'); - const dataTypes = await DataTypesFactory.deploy(); + const ProverFactory = await ethers.getContractFactory('Secp256k1ProverV1'); + const prover = await ProverFactory.deploy(); + await prover.initialize(initialBatch); - const ProverFactory = await ethers.getContractFactory('Secp256k1Prover', { - libraries: { SedaDataTypes: await dataTypes.getAddress() }, - }); - const prover = await ProverFactory.deploy(initialBatch); - - const CoreFactory = await ethers.getContractFactory('SedaCoreV1', { - libraries: { SedaDataTypes: await dataTypes.getAddress() }, - }); + const CoreFactory = await ethers.getContractFactory('SedaCoreV1'); const core = await CoreFactory.deploy(await prover.getAddress()); return { prover, core, data }; diff --git a/test/SedaPermissioned.test.ts b/test/SedaPermissioned.test.ts index a44f9a7..f3497de 100644 --- a/test/SedaPermissioned.test.ts +++ b/test/SedaPermissioned.test.ts @@ -15,12 +15,7 @@ describe('SedaCorePermissioned', () => { anyone, }; - const SedaDataTypes = await ethers.getContractFactory('SedaDataTypes'); - const dataTypes = await SedaDataTypes.deploy(); - - const PermissionedFactory = await ethers.getContractFactory('SedaCorePermissioned', { - libraries: { SedaDataTypes: await dataTypes.getAddress() }, - }); + const PermissionedFactory = await ethers.getContractFactory('SedaCorePermissioned'); const core = await PermissionedFactory.deploy([relayer.address], MAX_REPLICATION_FACTOR); return { core, signers }; diff --git a/test/utils.ts b/test/utils.ts index 98fd610..16d6bae 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -69,6 +69,17 @@ export function deriveDataResultId(dataResult: SedaDataTypes.ResultStruct): stri ); } +export function computeResultLeafHash(resultId: string): string { + return ethers.solidityPackedKeccak256(['bytes1', 'bytes32'], [RESULT_DOMAIN_SEPARATOR, ethers.getBytes(resultId)]); +} + +export function computeValidatorLeafHash(validator: string, votingPower: number): string { + return ethers.solidityPackedKeccak256( + ['bytes1', 'bytes', 'uint32'], + [SECP256K1_DOMAIN_SEPARATOR, validator, votingPower], + ); +} + export function generateDataFixtures(length: number): { requests: SedaDataTypes.RequestInputsStruct[]; results: SedaDataTypes.ResultStruct[]; @@ -104,14 +115,3 @@ export function generateDataFixtures(length: number): { return { requests, results }; } - -export function computeResultLeafHash(resultId: string): string { - return ethers.solidityPackedKeccak256(['bytes1', 'bytes32'], [RESULT_DOMAIN_SEPARATOR, ethers.getBytes(resultId)]); -} - -export function computeValidatorLeafHash(validator: string, votingPower: number): string { - return ethers.solidityPackedKeccak256( - ['bytes1', 'bytes', 'uint32'], - [SECP256K1_DOMAIN_SEPARATOR, validator, votingPower], - ); -} From 58dc5c8c2470c42b61d6ef2dc581f055f0d94133 Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Tue, 3 Dec 2024 15:44:11 +0000 Subject: [PATCH 03/22] test: add UPPS proxy upgrade tests --- contracts/mocks/MockSecp256k1ProverV2.sol | 42 ++++++++++++ test/proxy/Secp256k1Prover.behavior.ts | 79 +++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 contracts/mocks/MockSecp256k1ProverV2.sol create mode 100644 test/proxy/Secp256k1Prover.behavior.ts diff --git a/contracts/mocks/MockSecp256k1ProverV2.sol b/contracts/mocks/MockSecp256k1ProverV2.sol new file mode 100644 index 0000000..1699ad6 --- /dev/null +++ b/contracts/mocks/MockSecp256k1ProverV2.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Secp256k1ProverV1} from "../provers/Secp256k1ProverV1.sol"; + +/// @title MockSecp256k1ProverV2 +/// @notice Mock version of Secp256k1Prover for testing purposes +/// @dev This contract is a mock and should not be used in production +contract MockSecp256k1ProverV2 is Secp256k1ProverV1 { + error ContractNotUpgradeable(); + + bytes32 private constant V2_STORAGE_SLOT = + keccak256("secp256k1prover.v2.storage"); + + struct V2Storage { + string version; + } + + function _v2Storage() internal pure returns (V2Storage storage s) { + bytes32 slot = V2_STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + s.slot := slot + } + } + + /// @notice Returns the version string from V2 storage + /// @return version The version string + function getVersion() external view returns (string memory) { + return _v2Storage().version; + } + + function initialize() external reinitializer(2) onlyOwner { + V2Storage storage s = _v2Storage(); + s.version = "2.0.0"; + } + + // /// @dev Override the _authorizeUpgrade function + // function _authorizeUpgrade(address) internal virtual override onlyOwner { + // revert ContractNotUpgradeable(); + // } +} diff --git a/test/proxy/Secp256k1Prover.behavior.ts b/test/proxy/Secp256k1Prover.behavior.ts new file mode 100644 index 0000000..48da4c0 --- /dev/null +++ b/test/proxy/Secp256k1Prover.behavior.ts @@ -0,0 +1,79 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers'; +import { expect } from 'chai'; +import { ethers, upgrades } from 'hardhat'; +import { generateDataFixtures } from '../utils'; + +describe('Proxy: Secp256k1Prover', () => { + async function deployProxyFixture() { + const [owner] = await ethers.getSigners(); + + // Generate initial batch data + const initialBatch = { + batchHeight: 0, + blockHeight: 0, + validatorsRoot: ethers.ZeroHash, + resultsRoot: ethers.ZeroHash, + provingMetadata: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + }; + + // Deploy V1 through proxy + const ProverV1Factory = await ethers.getContractFactory('Secp256k1ProverV1', owner); + const proxy = await upgrades.deployProxy(ProverV1Factory, [initialBatch], { initializer: 'initialize' }); + await proxy.waitForDeployment(); + + // Get V2 factory + const ProverV2Factory = await ethers.getContractFactory('MockSecp256k1ProverV2', owner); + + return { proxy, ProverV2Factory, initialBatch }; + } + + describe('upgrade', () => { + it('should maintain state after upgrade', async () => { + const { proxy, ProverV2Factory, initialBatch } = await loadFixture(deployProxyFixture); + + // Check initial state + const heightBeforeUpgrade = await proxy.getLastBatchHeight(); + expect(heightBeforeUpgrade).to.equal(initialBatch.batchHeight); + + // Upgrade to V2 + const proxyV2 = await upgrades.upgradeProxy(await proxy.getAddress(), ProverV2Factory); + + // Check state is maintained + const heightAfterUpgrade = await proxyV2.getLastBatchHeight(); + expect(heightAfterUpgrade).to.equal(heightBeforeUpgrade); + }); + + it('should maintain owner after upgrade', async () => { + const { proxy, ProverV2Factory } = await loadFixture(deployProxyFixture); + const [owner] = await ethers.getSigners(); + + // Check owner before upgrade + const ownerBeforeUpgrade = await proxy.owner(); + expect(ownerBeforeUpgrade).to.equal(owner.address); + + // Upgrade to V2 + const proxyV2 = await upgrades.upgradeProxy(await proxy.getAddress(), ProverV2Factory); + + // Check owner is maintained after upgrade + const ownerAfterUpgrade = await proxyV2.owner(); + expect(ownerAfterUpgrade).to.equal(owner.address); + }); + + it('should have new functionality after upgrade', async () => { + const { proxy, ProverV2Factory } = await loadFixture(deployProxyFixture); + + // Verify V1 doesn't have getVersion() + const V1Contract = proxy.connect(await ethers.provider.getSigner()); + // @ts-expect-error - getVersion shouldn't exist on V1 + expect(V1Contract.getVersion).to.be.undefined; + + // Upgrade to V2 + const proxyV2 = await upgrades.upgradeProxy(await proxy.getAddress(), ProverV2Factory); + await proxyV2.initialize(); + + // Check new V2 functionality + const version = await proxyV2.getVersion(); + expect(version).to.equal('2.0.0'); + }); + }); +}); From 622cd6d0aea341ce716a78569b717eff9e61d82b Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Tue, 3 Dec 2024 15:54:20 +0000 Subject: [PATCH 04/22] chore(ignition): fix modules to UUPS changes --- ignition/modules/SedaCorePermissioned.ts | 7 +------ ignition/modules/SedaCoreV1.ts | 24 ++++++++++-------------- ignition/modules/parameters.json | 8 ++++---- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/ignition/modules/SedaCorePermissioned.ts b/ignition/modules/SedaCorePermissioned.ts index 3bcd01d..71058a9 100644 --- a/ignition/modules/SedaCorePermissioned.ts +++ b/ignition/modules/SedaCorePermissioned.ts @@ -5,13 +5,8 @@ const SedaProverModule = buildModule('SedaCorePermissioned', (m) => { const relayers = [m.getAccount(0)]; const maxReplicationFactor = m.getParameter('maxReplicationFactor'); - // Deploy SedaDataTypes library - const dataTypesLib = m.library('SedaDataTypes'); - // Deploy SedaCorePermissioned contract with the library - const coreContract = m.contract('SedaCorePermissioned', [relayers, maxReplicationFactor], { - libraries: { SedaDataTypes: dataTypesLib }, - }); + const coreContract = m.contract('SedaCorePermissioned', [relayers, maxReplicationFactor]); return { coreContract }; }); diff --git a/ignition/modules/SedaCoreV1.ts b/ignition/modules/SedaCoreV1.ts index 192f568..7bc3fb6 100644 --- a/ignition/modules/SedaCoreV1.ts +++ b/ignition/modules/SedaCoreV1.ts @@ -5,24 +5,20 @@ const SedaCoreV1Module = buildModule('SedaCoreV1', (m) => { // Constructor arguments const initialBatch = m.getParameter('initialBatch'); - // Deploy SedaDataTypes library - const dataTypesLib = m.library('SedaDataTypes'); - // Deploy Secp256k1Prover contract - const proverContract = m.contract('Secp256k1Prover', [initialBatch], { - libraries: { - SedaDataTypes: dataTypesLib, - }, - }); + const proverContract = m.contract('Secp256k1ProverV1'); + + // Initialize the UUPS upgradeable contract + m.call( + proverContract, + 'initialize', + [initialBatch] + ); // Deploy SedaCoreV1 contract - const coreV1Contract = m.contract('SedaCoreV1', [proverContract], { - libraries: { - SedaDataTypes: dataTypesLib, - }, - }); + const coreV1Contract = m.contract('SedaCoreV1', [proverContract]); - return { dataTypesLib, proverContract, coreV1Contract }; + return { proverContract, coreV1Contract }; }); export default SedaCoreV1Module; diff --git a/ignition/modules/parameters.json b/ignition/modules/parameters.json index 407181a..61fa36d 100644 --- a/ignition/modules/parameters.json +++ b/ignition/modules/parameters.json @@ -1,10 +1,10 @@ { "SedaCoreV1": { "initialBatch": { - "batchHeight": 0, - "blockHeight": 0, - "validatorsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", - "resultsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "batchHeight": 3, + "blockHeight": 31, + "validatorsRoot": "0x6b8a7c6cd54c814f4e30b89b5f2e91b9d96860e24eb39366f4c260400fcb47db", + "resultsRoot": "0x56c4f39b7564ea6a32877fc98743652998753c4cd8a4b455c26dcb3b92774b73", "provingMetadata": "0x0000000000000000000000000000000000000000000000000000000000000000" } }, From 8b386964cf2f87af95419dfa6c8c9891babfad7d Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Thu, 5 Dec 2024 11:41:04 +0000 Subject: [PATCH 05/22] chore: add prettier as sol formatter --- .prettierrc | 15 +++++++++++++++ bun.lockb | Bin 279188 -> 280060 bytes package.json | 10 +++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..3a9c8a9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "printWidth": 120, + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "avoid", + "overrides": [ + { + "files": "*.sol", + "options": { + "singleQuote": false + } + } + ], + "plugins": ["prettier-plugin-solidity"] +} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index fb3855c97f020668e94de411a22fcbdacfeb2f67..cd52cac3b57ba4efd4f74c59b3fd4e2a74c9118b 100755 GIT binary patch delta 52236 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

)m;R$wep!~ws=FpeE+yc8WL3Z<&vmUb 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 Date: Thu, 5 Dec 2024 11:49:20 +0000 Subject: [PATCH 06/22] feat(core): add UUPS support --- contracts/core/SedaCorePermissioned.sol | 39 +++------ contracts/core/SedaCoreV1.sol | 50 ++++++----- .../core/abstract/RequestHandlerBase.sol | 57 +++++++++++-- contracts/core/abstract/ResultHandlerBase.sol | 72 +++++++++++++--- contracts/core/handlers/RequestHandler.sol | 70 ---------------- contracts/core/handlers/ResultHandler.sol | 83 ------------------- contracts/interfaces/IRequestHandler.sol | 8 +- contracts/interfaces/IResultHandler.sol | 4 +- contracts/libraries/SedaDataTypes.sol | 8 +- contracts/mocks/MockSecp256k1ProverV2.sol | 2 +- contracts/provers/Secp256k1ProverV1.sol | 71 ++++------------ 11 files changed, 173 insertions(+), 291 deletions(-) delete mode 100644 contracts/core/handlers/RequestHandler.sol delete mode 100644 contracts/core/handlers/ResultHandler.sol diff --git a/contracts/core/SedaCorePermissioned.sol b/contracts/core/SedaCorePermissioned.sol index e7c9186..d417262 100644 --- a/contracts/core/SedaCorePermissioned.sol +++ b/contracts/core/SedaCorePermissioned.sol @@ -12,12 +12,7 @@ import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; /// @title SedaCorePermissioned /// @notice Core contract for the Seda protocol with permissioned access, managing requests and results /// @dev Implements RequestHandlerBase, IResultHandler, AccessControl, Pausable, and ReentrancyGuard functionalities -contract SedaCorePermissioned is - RequestHandlerBase, - IResultHandler, - AccessControl, - Pausable -{ +contract SedaCorePermissioned is RequestHandlerBase, IResultHandler, AccessControl, Pausable { using EnumerableSet for EnumerableSet.Bytes32Set; // Constants @@ -26,7 +21,6 @@ contract SedaCorePermissioned is // State variables uint16 public maxReplicationFactor; - mapping(bytes32 => SedaDataTypes.Request) public requests; mapping(bytes32 => SedaDataTypes.Result) public results; EnumerableSet.Bytes32Set private pendingRequests; @@ -49,9 +43,7 @@ contract SedaCorePermissioned is /// @notice Sets the maximum replication factor that can be used for requests /// @param newMaxReplicationFactor The new maximum replication factor - function setMaxReplicationFactor( - uint16 newMaxReplicationFactor - ) external onlyRole(ADMIN_ROLE) { + function setMaxReplicationFactor(uint16 newMaxReplicationFactor) external onlyRole(ADMIN_ROLE) { maxReplicationFactor = newMaxReplicationFactor; } @@ -60,11 +52,9 @@ contract SedaCorePermissioned is /// @return requestId The ID of the posted request function postRequest( SedaDataTypes.RequestInputs calldata inputs - ) external override whenNotPaused returns (bytes32) { + ) public override(RequestHandlerBase) whenNotPaused returns (bytes32) { uint16 replicationFactor = inputs.replicationFactor; - if ( - replicationFactor > maxReplicationFactor || replicationFactor == 0 - ) { + if (replicationFactor > maxReplicationFactor || replicationFactor == 0) { revert InvalidReplicationFactor(); } @@ -97,7 +87,7 @@ contract SedaCorePermissioned is /// @param result The result data function postResult( SedaDataTypes.Result calldata result, - uint64 , + uint64, bytes32[] calldata ) external override onlyRole(RELAYER_ROLE) whenNotPaused returns (bytes32) { bytes32 resultId = SedaDataTypes.deriveResultId(result); @@ -115,7 +105,7 @@ contract SedaCorePermissioned is /// @return The requested data function getRequest( bytes32 requestId - ) external view override returns (SedaDataTypes.Request memory) { + ) external view override(RequestHandlerBase) returns (SedaDataTypes.Request memory) { SedaDataTypes.Request memory request = requests[requestId]; if (bytes(request.version).length == 0) { revert RequestNotFound(requestId); @@ -127,9 +117,7 @@ contract SedaCorePermissioned is /// @notice Retrieves a result by its ID /// @param requestId The unique identifier of the result /// @return The result data associated with the given ID - function getResult( - bytes32 requestId - ) external view override returns (SedaDataTypes.Result memory) { + function getResult(bytes32 requestId) external view override returns (SedaDataTypes.Result memory) { if (results[requestId].drId == bytes32(0)) { revert ResultNotFound(requestId); } @@ -141,21 +129,14 @@ contract SedaCorePermissioned is /// @param offset The starting index in the pendingRequests set /// @param limit The maximum number of request IDs to return /// @return An array of pending request IDs - function getPendingRequests( - uint256 offset, - uint256 limit - ) public view returns (SedaDataTypes.Request[] memory) { + function getPendingRequests(uint256 offset, uint256 limit) public view returns (SedaDataTypes.Request[] memory) { uint256 totalRequests = pendingRequests.length(); if (offset >= totalRequests) { return new SedaDataTypes.Request[](0); } - uint256 actualLimit = (offset + limit > totalRequests) - ? totalRequests - offset - : limit; - SedaDataTypes.Request[] memory queriedPendingRequests = new SedaDataTypes.Request[]( - actualLimit - ); + uint256 actualLimit = (offset + limit > totalRequests) ? totalRequests - offset : limit; + SedaDataTypes.Request[] memory queriedPendingRequests = new SedaDataTypes.Request[](actualLimit); for (uint256 i = 0; i < actualLimit; i++) { bytes32 requestId = pendingRequests.at(offset + i); // Get request ID queriedPendingRequests[i] = requests[requestId]; diff --git a/contracts/core/SedaCoreV1.sol b/contracts/core/SedaCoreV1.sol index b76ca09..b17699b 100644 --- a/contracts/core/SedaCoreV1.sol +++ b/contracts/core/SedaCoreV1.sol @@ -2,14 +2,17 @@ pragma solidity ^0.8.9; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {RequestHandler} from "./handlers/RequestHandler.sol"; -import {ResultHandler} from "./handlers/ResultHandler.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import {RequestHandlerBase} from "./abstract/RequestHandlerBase.sol"; +import {ResultHandlerBase} from "./abstract/ResultHandlerBase.sol"; import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; /// @title SedaCoreV1 /// @notice Core contract for the Seda protocol, managing requests and results /// @dev Implements ResultHandler and RequestHandler functionalities, and manages active requests -contract SedaCoreV1 is RequestHandler, ResultHandler { +contract SedaCoreV1 is RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, OwnableUpgradeable { using EnumerableSet for EnumerableSet.Bytes32Set; // Enumerable Set to store the request IDs that are pending @@ -21,7 +24,12 @@ contract SedaCoreV1 is RequestHandler, ResultHandler { /// @notice Initializes the SedaCoreV1 contract /// @param sedaProverAddress The address of the Seda prover contract - constructor(address sedaProverAddress) ResultHandler(sedaProverAddress) {} + /// @dev This function replaces the constructor for proxy compatibility and can only be called once + function initialize(address sedaProverAddress) public initializer { + __ResultHandler_init(sedaProverAddress); + __Ownable_init(msg.sender); + __UUPSUpgradeable_init(); + } /// @notice Retrieves a list of active requests /// @dev This function is gas-intensive due to iteration over the pendingRequests array. @@ -29,22 +37,14 @@ contract SedaCoreV1 is RequestHandler, ResultHandler { /// @param offset The starting index in the pendingRequests array /// @param limit The maximum number of requests to return /// @return An array of SedaDataTypes.Request structs - function getPendingRequests( - uint256 offset, - uint256 limit - ) public view returns (SedaDataTypes.Request[] memory) { + function getPendingRequests(uint256 offset, uint256 limit) public view returns (SedaDataTypes.Request[] memory) { uint256 totalRequests = pendingRequests.length(); if (offset >= totalRequests) { return new SedaDataTypes.Request[](0); } - uint256 actualLimit = (offset + limit > totalRequests) - ? totalRequests - offset - : limit; - SedaDataTypes.Request[] - memory queriedPendingRequests = new SedaDataTypes.Request[]( - actualLimit - ); + uint256 actualLimit = (offset + limit > totalRequests) ? totalRequests - offset : limit; + SedaDataTypes.Request[] memory queriedPendingRequests = new SedaDataTypes.Request[](actualLimit); for (uint256 i = 0; i < actualLimit; i++) { bytes32 requestId = pendingRequests.at(offset + i); queriedPendingRequests[i] = requests[requestId]; @@ -53,24 +53,24 @@ contract SedaCoreV1 is RequestHandler, ResultHandler { return queriedPendingRequests; } - /// @inheritdoc RequestHandler + /// @inheritdoc RequestHandlerBase /// @dev Overrides the base implementation to also add the request ID to the pendingRequests array function postRequest( SedaDataTypes.RequestInputs calldata inputs - ) public override(RequestHandler) returns (bytes32) { + ) public override(RequestHandlerBase) returns (bytes32) { bytes32 requestId = super.postRequest(inputs); _addRequest(requestId); return requestId; } - /// @inheritdoc ResultHandler + /// @inheritdoc ResultHandlerBase /// @dev Overrides the base implementation to also remove the request ID from the pendingRequests array if it exists function postResult( SedaDataTypes.Result calldata result, uint64 batchHeight, bytes32[] calldata proof - ) public override(ResultHandler) returns (bytes32) { + ) public override(ResultHandlerBase) returns (bytes32) { bytes32 resultId = super.postResult(result, batchHeight, proof); _removeRequest(result.drId); @@ -92,4 +92,16 @@ contract SedaCoreV1 is RequestHandler, ResultHandler { function _removeRequest(bytes32 requestId) internal { pendingRequests.remove(requestId); } + + /// @dev Required override for UUPSUpgradeable. Ensures only the owner can upgrade the implementation. + /// @inheritdoc UUPSUpgradeable + /// @param newImplementation Address of the new implementation contract + function _authorizeUpgrade( + address newImplementation + ) + internal + virtual + override + onlyOwner // solhint-disable-next-line no-empty-blocks + {} } diff --git a/contracts/core/abstract/RequestHandlerBase.sol b/contracts/core/abstract/RequestHandlerBase.sol index 702c0bb..7dc48c2 100644 --- a/contracts/core/abstract/RequestHandlerBase.sol +++ b/contracts/core/abstract/RequestHandlerBase.sol @@ -1,22 +1,63 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; -import {IRequestHandler} from "../../interfaces/IRequestHandler.sol"; import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; +import {IRequestHandler} from "../../interfaces/IRequestHandler.sol"; +/// @title RequestHandler +/// @notice Implements the RequestHandlerBase for managing Seda protocol requests abstract contract RequestHandlerBase is IRequestHandler { + // Mapping of request IDs to Request structs + mapping(bytes32 => SedaDataTypes.Request) public requests; + /// @inheritdoc IRequestHandler function postRequest( SedaDataTypes.RequestInputs calldata inputs - ) external virtual override(IRequestHandler) returns (bytes32); + ) public virtual override(IRequestHandler) returns (bytes32) { + if (inputs.replicationFactor == 0) { + revert InvalidReplicationFactor(); + } + + bytes32 requestId = SedaDataTypes.deriveRequestId(inputs); + if (bytes(requests[requestId].version).length != 0) { + revert RequestAlreadyExists(requestId); + } + + requests[requestId] = SedaDataTypes.Request({ + version: SedaDataTypes.VERSION, + execProgramId: inputs.execProgramId, + execInputs: inputs.execInputs, + execGasLimit: inputs.execGasLimit, + tallyProgramId: inputs.tallyProgramId, + tallyInputs: inputs.tallyInputs, + tallyGasLimit: inputs.tallyGasLimit, + replicationFactor: inputs.replicationFactor, + consensusFilter: inputs.consensusFilter, + gasPrice: inputs.gasPrice, + memo: inputs.memo + }); + + emit RequestPosted(requestId); + return requestId; + } /// @inheritdoc IRequestHandler function getRequest( bytes32 requestId - ) - external - view - virtual - override(IRequestHandler) - returns (SedaDataTypes.Request memory); + ) external view virtual override(IRequestHandler) returns (SedaDataTypes.Request memory) { + SedaDataTypes.Request memory request = requests[requestId]; + // Version field is always set + if (bytes(request.version).length == 0) { + revert RequestNotFound(requestId); + } + + return requests[requestId]; + } + + /// @notice Derives a request ID from the given inputs + /// @param inputs The request inputs + /// @return The derived request ID + function deriveRequestId(SedaDataTypes.RequestInputs calldata inputs) public pure returns (bytes32) { + return SedaDataTypes.deriveRequestId(inputs); + } } diff --git a/contracts/core/abstract/ResultHandlerBase.sol b/contracts/core/abstract/ResultHandlerBase.sol index f49dd82..7d489b2 100644 --- a/contracts/core/abstract/ResultHandlerBase.sol +++ b/contracts/core/abstract/ResultHandlerBase.sol @@ -1,17 +1,26 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; import {IProver} from "../../interfaces/IProver.sol"; import {IResultHandler} from "../../interfaces/IResultHandler.sol"; -import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; -abstract contract ResultHandlerBase is IResultHandler { +/// @title ResultHandler +/// @notice Implements the ResultHandlerBase for managing Seda protocol results +abstract contract ResultHandlerBase is IResultHandler, Initializable { IProver public sedaProver; - /// @notice Initializes the ResultHandlerBase contract - /// @dev Sets the address of the SEDA Prover contract - /// @param sedaProverAddress The address of the SEDA Prover contract - constructor(address sedaProverAddress) { + // Mapping of request IDs to Result structs + mapping(bytes32 => SedaDataTypes.Result) public results; + + // Remove constructor and add initialization function + /// @notice Initializes the ResultHandler contract + /// @dev Sets up the contract with the provided Seda prover address + /// @param sedaProverAddress The address of the Seda prover contract + // solhint-disable-next-line func-name-mixedcase + function __ResultHandler_init(address sedaProverAddress) internal onlyInitializing { sedaProver = IProver(sedaProverAddress); } @@ -20,10 +29,53 @@ abstract contract ResultHandlerBase is IResultHandler { SedaDataTypes.Result calldata result, uint64 batchHeight, bytes32[] calldata proof - ) external virtual override(IResultHandler) returns (bytes32); + ) public virtual override(IResultHandler) returns (bytes32) { + bytes32 resultId = SedaDataTypes.deriveResultId(result); + if (results[result.drId].drId != bytes32(0)) { + revert ResultAlreadyExists(resultId); + } + if (!sedaProver.verifyResultProof(resultId, batchHeight, proof)) { + revert InvalidResultProof(resultId); + } + + results[result.drId] = result; + + emit ResultPosted(resultId); + return resultId; + } /// @inheritdoc IResultHandler - function getResult( - bytes32 requestId - ) external view virtual override(IResultHandler) returns (SedaDataTypes.Result memory); + function getResult(bytes32 requestId) public view override(IResultHandler) returns (SedaDataTypes.Result memory) { + SedaDataTypes.Result memory result = results[requestId]; + if (bytes(result.version).length == 0) { + revert ResultNotFound(requestId); + } + + return results[requestId]; + } + + /// @notice Verifies the result without storing it + /// @param result The result to verify + /// @param batchHeight The height of the batch the result belongs to + /// @param proof The proof associated with the result + /// @return A boolean indicating whether the result is valid + function verifyResult( + SedaDataTypes.Result calldata result, + uint64 batchHeight, + bytes32[] calldata proof + ) public view returns (bytes32) { + bytes32 resultId = SedaDataTypes.deriveResultId(result); + if (!sedaProver.verifyResultProof(resultId, batchHeight, proof)) { + revert InvalidResultProof(resultId); + } + + return resultId; + } + + /// @notice Derives a result ID from the given result + /// @param result The result data + /// @return The derived result ID + function deriveResultId(SedaDataTypes.Result calldata result) public pure returns (bytes32) { + return SedaDataTypes.deriveResultId(result); + } } diff --git a/contracts/core/handlers/RequestHandler.sol b/contracts/core/handlers/RequestHandler.sol deleted file mode 100644 index d449b0b..0000000 --- a/contracts/core/handlers/RequestHandler.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; -import {RequestHandlerBase} from "../abstract/RequestHandlerBase.sol"; - -/// @title RequestHandler -/// @notice Implements the RequestHandlerBase for managing Seda protocol requests -contract RequestHandler is RequestHandlerBase { - // Mapping of request IDs to Request structs - mapping(bytes32 => SedaDataTypes.Request) public requests; - - /// @inheritdoc RequestHandlerBase - function postRequest( - SedaDataTypes.RequestInputs calldata inputs - ) public virtual override(RequestHandlerBase) returns (bytes32) { - if (inputs.replicationFactor == 0) { - revert InvalidReplicationFactor(); - } - - bytes32 requestId = SedaDataTypes.deriveRequestId(inputs); - if (bytes(requests[requestId].version).length != 0) { - revert RequestAlreadyExists(requestId); - } - - requests[requestId] = SedaDataTypes.Request({ - version: SedaDataTypes.VERSION, - execProgramId: inputs.execProgramId, - execInputs: inputs.execInputs, - execGasLimit: inputs.execGasLimit, - tallyProgramId: inputs.tallyProgramId, - tallyInputs: inputs.tallyInputs, - tallyGasLimit: inputs.tallyGasLimit, - replicationFactor: inputs.replicationFactor, - consensusFilter: inputs.consensusFilter, - gasPrice: inputs.gasPrice, - memo: inputs.memo - }); - - emit RequestPosted(requestId); - return requestId; - } - - /// @inheritdoc RequestHandlerBase - function getRequest( - bytes32 requestId - ) - external - view - override(RequestHandlerBase) - returns (SedaDataTypes.Request memory) - { - SedaDataTypes.Request memory request = requests[requestId]; - // Version field is always set - if (bytes(request.version).length == 0) { - revert RequestNotFound(requestId); - } - - return requests[requestId]; - } - - /// @notice Derives a request ID from the given inputs - /// @param inputs The request inputs - /// @return The derived request ID - function deriveRequestId( - SedaDataTypes.RequestInputs calldata inputs - ) public pure returns (bytes32) { - return SedaDataTypes.deriveRequestId(inputs); - } -} diff --git a/contracts/core/handlers/ResultHandler.sol b/contracts/core/handlers/ResultHandler.sol deleted file mode 100644 index a45407e..0000000 --- a/contracts/core/handlers/ResultHandler.sol +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; -import {ResultHandlerBase} from "../abstract/ResultHandlerBase.sol"; - -/// @title ResultHandler -/// @notice Implements the ResultHandlerBase for managing Seda protocol results -contract ResultHandler is ResultHandlerBase { - // Mapping of request IDs to Result structs - mapping(bytes32 => SedaDataTypes.Result) public results; - - /// @notice Initializes the ResultHandler contract - /// @dev Sets up the contract with the provided Seda prover address - /// @param sedaProverAddress The address of the Seda prover contract - constructor( - address sedaProverAddress - ) ResultHandlerBase(sedaProverAddress) {} - - /// @inheritdoc ResultHandlerBase - function postResult( - SedaDataTypes.Result calldata result, - uint64 batchHeight, - bytes32[] calldata proof - ) public virtual override(ResultHandlerBase) returns (bytes32) { - bytes32 resultId = SedaDataTypes.deriveResultId(result); - if (results[result.drId].drId != bytes32(0)) { - revert ResultAlreadyExists(resultId); - } - if (!sedaProver.verifyResultProof(resultId, batchHeight, proof)) { - revert InvalidResultProof(resultId); - } - - results[result.drId] = result; - - emit ResultPosted(resultId); - return resultId; - } - - /// @inheritdoc ResultHandlerBase - function getResult( - bytes32 requestId - ) - public - view - override(ResultHandlerBase) - returns (SedaDataTypes.Result memory) - { - SedaDataTypes.Result memory result = results[requestId]; - if (bytes(result.version).length == 0) { - revert ResultNotFound(requestId); - } - - return results[requestId]; - } - - /// @notice Verifies the result without storing it - /// @param result The result to verify - /// @param batchHeight The height of the batch the result belongs to - /// @param proof The proof associated with the result - /// @return A boolean indicating whether the result is valid - function verifyResult( - SedaDataTypes.Result calldata result, - uint64 batchHeight, - bytes32[] calldata proof - ) public view returns (bytes32) { - bytes32 resultId = SedaDataTypes.deriveResultId(result); - if (!sedaProver.verifyResultProof(resultId, batchHeight, proof)) { - revert InvalidResultProof(resultId); - } - - return resultId; - } - - /// @notice Derives a result ID from the given result - /// @param result The result data - /// @return The derived result ID - function deriveResultId( - SedaDataTypes.Result calldata result - ) public pure returns (bytes32) { - return SedaDataTypes.deriveResultId(result); - } -} diff --git a/contracts/interfaces/IRequestHandler.sol b/contracts/interfaces/IRequestHandler.sol index e57c996..6b689cd 100644 --- a/contracts/interfaces/IRequestHandler.sol +++ b/contracts/interfaces/IRequestHandler.sol @@ -15,14 +15,10 @@ interface IRequestHandler { /// @notice Allows users to post a new data request. /// @param inputs The input parameters for the data request. /// @return requestId The unique identifier for the posted request. - function postRequest( - SedaDataTypes.RequestInputs calldata inputs - ) external returns (bytes32); + function postRequest(SedaDataTypes.RequestInputs calldata inputs) external returns (bytes32); /// @notice Retrieves a stored data request by its unique identifier. /// @param id The unique identifier of the request to retrieve. /// @return request The details of the requested data. - function getRequest( - bytes32 id - ) external view returns (SedaDataTypes.Request memory); + function getRequest(bytes32 id) external view returns (SedaDataTypes.Request memory); } diff --git a/contracts/interfaces/IResultHandler.sol b/contracts/interfaces/IResultHandler.sol index 7bddd0c..3d1b952 100644 --- a/contracts/interfaces/IResultHandler.sol +++ b/contracts/interfaces/IResultHandler.sol @@ -25,7 +25,5 @@ interface IResultHandler { /// @notice Retrieves a result by its ID /// @param requestId The unique identifier of the request /// @return The result data associated with the given ID - function getResult( - bytes32 requestId - ) external view returns (SedaDataTypes.Result memory); + function getResult(bytes32 requestId) external view returns (SedaDataTypes.Result memory); } diff --git a/contracts/libraries/SedaDataTypes.sol b/contracts/libraries/SedaDataTypes.sol index 8391d90..c4d3c94 100644 --- a/contracts/libraries/SedaDataTypes.sol +++ b/contracts/libraries/SedaDataTypes.sol @@ -121,9 +121,7 @@ library SedaDataTypes { /// @notice Derives a unique result ID from a Result struct /// @param result The Result struct to derive the ID from /// @return The derived result ID - function deriveResultId( - Result memory result - ) internal pure returns (bytes32) { + function deriveResultId(Result memory result) internal pure returns (bytes32) { return keccak256( bytes.concat( @@ -144,9 +142,7 @@ library SedaDataTypes { /// @notice Derives a unique request ID from RequestInputs /// @param inputs The RequestInputs struct to derive the ID from /// @return The derived request ID - function deriveRequestId( - RequestInputs memory inputs - ) internal pure returns (bytes32) { + function deriveRequestId(RequestInputs memory inputs) internal pure returns (bytes32) { return keccak256( bytes.concat( diff --git a/contracts/mocks/MockSecp256k1ProverV2.sol b/contracts/mocks/MockSecp256k1ProverV2.sol index 1699ad6..cfeefdf 100644 --- a/contracts/mocks/MockSecp256k1ProverV2.sol +++ b/contracts/mocks/MockSecp256k1ProverV2.sol @@ -10,7 +10,7 @@ contract MockSecp256k1ProverV2 is Secp256k1ProverV1 { error ContractNotUpgradeable(); bytes32 private constant V2_STORAGE_SLOT = - keccak256("secp256k1prover.v2.storage"); + keccak256(abi.encode(uint256(keccak256("secp256k1prover.v2.storage")) - 1)) & ~bytes32(uint256(0xff)); struct V2Storage { string version; diff --git a/contracts/provers/Secp256k1ProverV1.sol b/contracts/provers/Secp256k1ProverV1.sol index b2baaa5..e17df72 100644 --- a/contracts/provers/Secp256k1ProverV1.sol +++ b/contracts/provers/Secp256k1ProverV1.sol @@ -17,12 +17,7 @@ import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; /// - Increasing batch and block heights /// - Valid validator proofs and signatures /// - Sufficient voting power to meet the consensus threshold -contract Secp256k1ProverV1 is - ProverBase, - Initializable, - UUPSUpgradeable, - OwnableUpgradeable -{ +contract Secp256k1ProverV1 is ProverBase, Initializable, UUPSUpgradeable, OwnableUpgradeable { // ============ Errors ============ // Error thrown when consensus is not reached error ConsensusNotReached(); @@ -34,10 +29,8 @@ contract Secp256k1ProverV1 is // Domain separator for Secp256k1 Merkle Tree leaves bytes1 internal constant SECP256K1_DOMAIN_SEPARATOR = 0x01; // Constant storage slot for the state following the ERC-7201 standard - bytes32 private constant STORAGE_SLOT = - keccak256( - abi.encode(uint256(keccak256("secp256k1prover.v1.storage")) - 1) - ) & ~bytes32(uint256(0xff)); + bytes32 private constant PROVER_V1_STORAGE_SLOT = + keccak256(abi.encode(uint256(keccak256("secp256k1prover.v1.storage")) - 1)) & ~bytes32(uint256(0xff)); // ============ Storage ============ @@ -53,23 +46,17 @@ contract Secp256k1ProverV1 is /// @notice Initializes the contract with initial batch data /// @dev Sets up the contract's initial state and initializes inherited contracts /// @param initialBatch The initial batch data containing height, validators root, and results root - function initialize( - SedaDataTypes.Batch memory initialBatch - ) public initializer { + function initialize(SedaDataTypes.Batch memory initialBatch) public initializer { // Initialize inherited contracts __Ownable_init(msg.sender); __UUPSUpgradeable_init(); // Existing initialization code Secp256k1ProverStorage storage s = _storage(); - s.batchToResultsRoot[initialBatch.batchHeight] = initialBatch - .resultsRoot; + s.batchToResultsRoot[initialBatch.batchHeight] = initialBatch.resultsRoot; s.lastBatchHeight = initialBatch.batchHeight; s.lastValidatorsRoot = initialBatch.validatorsRoot; - emit BatchPosted( - initialBatch.batchHeight, - SedaDataTypes.deriveBatchId(initialBatch) - ); + emit BatchPosted(initialBatch.batchHeight, SedaDataTypes.deriveBatchId(initialBatch)); } // ============ External Functions ============ @@ -105,18 +92,10 @@ contract Secp256k1ProverV1 is // Check that all validator proofs are valid and accumulate voting power uint64 votingPower = 0; for (uint256 i = 0; i < validatorProofs.length; i++) { - if ( - !_verifyValidatorProof(validatorProofs[i], s.lastValidatorsRoot) - ) { + if (!_verifyValidatorProof(validatorProofs[i], s.lastValidatorsRoot)) { revert InvalidValidatorProof(); } - if ( - !_verifySignature( - batchId, - signatures[i], - validatorProofs[i].signer - ) - ) { + if (!_verifySignature(batchId, signatures[i], validatorProofs[i].signer)) { revert InvalidSignature(); } votingPower += validatorProofs[i].votingPower; @@ -147,15 +126,8 @@ contract Secp256k1ProverV1 is bytes32[] calldata merkleProof ) public view override returns (bool) { Secp256k1ProverStorage storage s = _storage(); - bytes32 leaf = keccak256( - abi.encodePacked(RESULT_DOMAIN_SEPARATOR, resultId) - ); - return - MerkleProof.verify( - merkleProof, - s.batchToResultsRoot[batchHeight], - leaf - ); + bytes32 leaf = keccak256(abi.encodePacked(RESULT_DOMAIN_SEPARATOR, resultId)); + return MerkleProof.verify(merkleProof, s.batchToResultsRoot[batchHeight], leaf); } /// @notice Returns the last processed batch height @@ -173,9 +145,7 @@ contract Secp256k1ProverV1 is /// @notice Returns the results root for a specific batch height /// @param batchHeight The batch height to query /// @return The results root for the specified batch - function getBatchResultsRoot( - uint64 batchHeight - ) public view returns (bytes32) { + function getBatchResultsRoot(uint64 batchHeight) public view returns (bytes32) { return _storage().batchToResultsRoot[batchHeight]; } @@ -184,12 +154,8 @@ contract Secp256k1ProverV1 is /// @notice Returns the storage struct for the contract /// @dev Uses ERC-7201 storage pattern to access the storage struct at a specific slot /// @return s The storage struct containing the contract's state variables - function _storage() - internal - pure - returns (Secp256k1ProverStorage storage s) - { - bytes32 slot = STORAGE_SLOT; + function _storage() internal pure returns (Secp256k1ProverStorage storage s) { + bytes32 slot = PROVER_V1_STORAGE_SLOT; // solhint-disable-next-line no-inline-assembly assembly { s.slot := slot @@ -205,13 +171,7 @@ contract Secp256k1ProverV1 is SedaDataTypes.ValidatorProof memory proof, bytes32 validatorsRoot ) internal pure returns (bool) { - bytes32 leaf = keccak256( - abi.encodePacked( - SECP256K1_DOMAIN_SEPARATOR, - proof.signer, - proof.votingPower - ) - ); + bytes32 leaf = keccak256(abi.encodePacked(SECP256K1_DOMAIN_SEPARATOR, proof.signer, proof.votingPower)); return MerkleProof.verify(proof.merkleProof, validatorsRoot, leaf); } @@ -238,7 +198,6 @@ contract Secp256k1ProverV1 is internal virtual override - onlyOwner - // solhint-disable-next-line no-empty-blocks + onlyOwner // solhint-disable-next-line no-empty-blocks {} } From 7fba9d12e0ea51c1378c5cc0517035a6e6b0aa3f Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Thu, 5 Dec 2024 11:50:37 +0000 Subject: [PATCH 07/22] test: reorganize and update tests --- test/{ => core}/RequestHandler.test.ts | 6 +++--- test/{ => core}/ResultHandler.test.ts | 9 +++++---- test/{ => core}/SedaCoreV1.test.ts | 7 ++++--- test/{ => core}/SedaPermissioned.test.ts | 4 ++-- .../Secp256k1Prover.proxy.ts} | 0 test/{ => prover}/Secp256k1Prover.test.ts | 10 ++-------- 6 files changed, 16 insertions(+), 20 deletions(-) rename test/{ => core}/RequestHandler.test.ts (96%) rename test/{ => core}/ResultHandler.test.ts (97%) rename test/{ => core}/SedaCoreV1.test.ts (98%) rename test/{ => core}/SedaPermissioned.test.ts (99%) rename test/{proxy/Secp256k1Prover.behavior.ts => prover/Secp256k1Prover.proxy.ts} (100%) rename test/{ => prover}/Secp256k1Prover.test.ts (97%) diff --git a/test/RequestHandler.test.ts b/test/core/RequestHandler.test.ts similarity index 96% rename from test/RequestHandler.test.ts rename to test/core/RequestHandler.test.ts index ea5b422..0e1bb06 100644 --- a/test/RequestHandler.test.ts +++ b/test/core/RequestHandler.test.ts @@ -2,15 +2,15 @@ import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers'; import { expect } from 'chai'; import { ethers } from 'hardhat'; -import { compareRequests } from './helpers'; -import { deriveRequestId, generateDataFixtures } from './utils'; +import { compareRequests } from '../helpers'; +import { deriveRequestId, generateDataFixtures } from '../utils'; describe('RequestHandler', () => { async function deployRequestHandlerFixture() { const { requests } = generateDataFixtures(4); // Deploy the RequestHandler contract - const RequestHandlerFactory = await ethers.getContractFactory('RequestHandler'); + const RequestHandlerFactory = await ethers.getContractFactory('SedaCoreV1'); const handler = await RequestHandlerFactory.deploy(); return { handler, requests }; diff --git a/test/ResultHandler.test.ts b/test/core/ResultHandler.test.ts similarity index 97% rename from test/ResultHandler.test.ts rename to test/core/ResultHandler.test.ts index 9ecbfde..5309d42 100644 --- a/test/ResultHandler.test.ts +++ b/test/core/ResultHandler.test.ts @@ -3,8 +3,8 @@ import { SimpleMerkleTree } from '@openzeppelin/merkle-tree'; import { expect } from 'chai'; import { ethers } from 'hardhat'; -import { compareResults } from './helpers'; -import { computeResultLeafHash, deriveDataResultId, generateDataFixtures } from './utils'; +import { compareResults } from '../helpers'; +import { computeResultLeafHash, deriveDataResultId, generateDataFixtures } from '../utils'; describe('ResultHandler', () => { async function deployResultHandlerFixture() { @@ -37,8 +37,9 @@ describe('ResultHandler', () => { const prover = await ProverFactory.deploy(); await prover.initialize(initialBatch); - const ResultHandlerFactory = await ethers.getContractFactory('ResultHandler'); - const handler = await ResultHandlerFactory.deploy(prover.getAddress()); + const ResultHandlerFactory = await ethers.getContractFactory('SedaCoreV1'); + const handler = await ResultHandlerFactory.deploy(); + await handler.initialize(prover.getAddress()); return { handler, data }; } diff --git a/test/SedaCoreV1.test.ts b/test/core/SedaCoreV1.test.ts similarity index 98% rename from test/SedaCoreV1.test.ts rename to test/core/SedaCoreV1.test.ts index df24819..043fe56 100644 --- a/test/SedaCoreV1.test.ts +++ b/test/core/SedaCoreV1.test.ts @@ -3,8 +3,8 @@ import { SimpleMerkleTree } from '@openzeppelin/merkle-tree'; import { expect } from 'chai'; import { ethers } from 'hardhat'; -import { compareRequests, compareResults, convertToRequestInputs } from './helpers'; -import { computeResultLeafHash, deriveDataResultId, deriveRequestId, generateDataFixtures } from './utils'; +import { compareRequests, compareResults, convertToRequestInputs } from '../helpers'; +import { computeResultLeafHash, deriveDataResultId, deriveRequestId, generateDataFixtures } from '../utils'; describe('SedaCoreV1', () => { async function deployCoreFixture() { @@ -30,7 +30,8 @@ describe('SedaCoreV1', () => { await prover.initialize(initialBatch); const CoreFactory = await ethers.getContractFactory('SedaCoreV1'); - const core = await CoreFactory.deploy(await prover.getAddress()); + const core = await CoreFactory.deploy(); + await core.initialize(await prover.getAddress()); return { prover, core, data }; } diff --git a/test/SedaPermissioned.test.ts b/test/core/SedaPermissioned.test.ts similarity index 99% rename from test/SedaPermissioned.test.ts rename to test/core/SedaPermissioned.test.ts index f3497de..c43c197 100644 --- a/test/SedaPermissioned.test.ts +++ b/test/core/SedaPermissioned.test.ts @@ -1,8 +1,8 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; import { ethers } from 'hardhat'; -import { compareRequests, compareResults, convertToRequestInputs } from './helpers'; -import { deriveDataResultId, deriveRequestId, generateDataFixtures } from './utils'; +import { compareRequests, compareResults, convertToRequestInputs } from '../helpers'; +import { deriveDataResultId, deriveRequestId, generateDataFixtures } from '../utils'; describe('SedaCorePermissioned', () => { const MAX_REPLICATION_FACTOR = 1; diff --git a/test/proxy/Secp256k1Prover.behavior.ts b/test/prover/Secp256k1Prover.proxy.ts similarity index 100% rename from test/proxy/Secp256k1Prover.behavior.ts rename to test/prover/Secp256k1Prover.proxy.ts diff --git a/test/Secp256k1Prover.test.ts b/test/prover/Secp256k1Prover.test.ts similarity index 97% rename from test/Secp256k1Prover.test.ts rename to test/prover/Secp256k1Prover.test.ts index 79296b6..0ae2099 100644 --- a/test/Secp256k1Prover.test.ts +++ b/test/prover/Secp256k1Prover.test.ts @@ -3,7 +3,7 @@ import { SimpleMerkleTree } from '@openzeppelin/merkle-tree'; import { expect } from 'chai'; import type { Wallet } from 'ethers'; import { ethers } from 'hardhat'; -import type { SedaDataTypes } from '../typechain-types/contracts/libraries'; +import type { SedaDataTypes } from '../../typechain-types/contracts/libraries'; import { computeResultLeafHash, computeValidatorLeafHash, @@ -11,7 +11,7 @@ import { deriveDataResultId, generateDataFixtures, generateNewBatchWithId, -} from './utils'; +} from '../utils'; describe('Secp256k1Prover', () => { async function deployProverFixture(length = 4) { @@ -72,17 +72,11 @@ describe('Secp256k1Prover', () => { results, }; - // Deploy the SedaDataTypes library first - // const DataTypesFactory = await ethers.getContractFactory('SedaDataTypes'); - // const dataTypes = await DataTypesFactory.deploy(); - // Deploy the contract const ProverFactory = await ethers.getContractFactory('Secp256k1ProverV1'); const prover = await ProverFactory.deploy(); await prover.initialize(initialBatch); - // const prover = await upgrades.deployProxy(ProverFactory, [initialBatch]); - return { prover, wallets, data }; } From 5b2bc939b0401c22f5f63ddaa39d423d9438a1ec Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Thu, 5 Dec 2024 11:53:48 +0000 Subject: [PATCH 08/22] chore(ignition): update ignition core v1 module --- ignition/modules/SedaCoreV1.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ignition/modules/SedaCoreV1.ts b/ignition/modules/SedaCoreV1.ts index 7bc3fb6..c721b44 100644 --- a/ignition/modules/SedaCoreV1.ts +++ b/ignition/modules/SedaCoreV1.ts @@ -7,16 +7,13 @@ const SedaCoreV1Module = buildModule('SedaCoreV1', (m) => { // Deploy Secp256k1Prover contract const proverContract = m.contract('Secp256k1ProverV1'); - // Initialize the UUPS upgradeable contract - m.call( - proverContract, - 'initialize', - [initialBatch] - ); + m.call(proverContract, 'initialize', [initialBatch]); // Deploy SedaCoreV1 contract - const coreV1Contract = m.contract('SedaCoreV1', [proverContract]); + const coreV1Contract = m.contract('SedaCoreV1'); + // Initialize the UUPS upgradeable contract + m.call(coreV1Contract, 'initialize', [proverContract]); return { proverContract, coreV1Contract }; }); From 47ac2e3ac1f34f6a54d4bcc7c774d8ebc7e5d9d1 Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Thu, 5 Dec 2024 12:52:54 +0000 Subject: [PATCH 09/22] fix(prover): disable initializers on constructor --- contracts/mocks/MockSecp256k1ProverV2.sol | 31 +++++++++++++++-------- contracts/provers/Secp256k1ProverV1.sol | 5 ++++ test/prover/Secp256k1Prover.test.ts | 10 ++++---- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/contracts/mocks/MockSecp256k1ProverV2.sol b/contracts/mocks/MockSecp256k1ProverV2.sol index cfeefdf..82dde7b 100644 --- a/contracts/mocks/MockSecp256k1ProverV2.sol +++ b/contracts/mocks/MockSecp256k1ProverV2.sol @@ -7,32 +7,43 @@ import {Secp256k1ProverV1} from "../provers/Secp256k1ProverV1.sol"; /// @notice Mock version of Secp256k1Prover for testing purposes /// @dev This contract is a mock and should not be used in production contract MockSecp256k1ProverV2 is Secp256k1ProverV1 { + // ============ Errors ============ error ContractNotUpgradeable(); - bytes32 private constant V2_STORAGE_SLOT = + // ============ Constants ============ + bytes32 private constant PROVER_V2_STORAGE_SLOT = keccak256(abi.encode(uint256(keccak256("secp256k1prover.v2.storage")) - 1)) & ~bytes32(uint256(0xff)); + // ============ Storage ============ struct V2Storage { string version; } - function _v2Storage() internal pure returns (V2Storage storage s) { - bytes32 slot = V2_STORAGE_SLOT; - // solhint-disable-next-line no-inline-assembly - assembly { - s.slot := slot - } + // ============ Constructor & Initializer ============ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize() external reinitializer(2) onlyOwner { + V2Storage storage s = _v2Storage(); + s.version = "2.0.0"; } + // ============ External Functions ============ /// @notice Returns the version string from V2 storage /// @return version The version string function getVersion() external view returns (string memory) { return _v2Storage().version; } - function initialize() external reinitializer(2) onlyOwner { - V2Storage storage s = _v2Storage(); - s.version = "2.0.0"; + // ============ Internal Functions ============ + function _v2Storage() internal pure returns (V2Storage storage s) { + bytes32 slot = PROVER_V2_STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + s.slot := slot + } } // /// @dev Override the _authorizeUpgrade function diff --git a/contracts/provers/Secp256k1ProverV1.sol b/contracts/provers/Secp256k1ProverV1.sol index e17df72..9170c3c 100644 --- a/contracts/provers/Secp256k1ProverV1.sol +++ b/contracts/provers/Secp256k1ProverV1.sol @@ -43,6 +43,11 @@ contract Secp256k1ProverV1 is ProverBase, Initializable, UUPSUpgradeable, Ownabl // ============ Constructor & Initializer ============ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /// @notice Initializes the contract with initial batch data /// @dev Sets up the contract's initial state and initializes inherited contracts /// @param initialBatch The initial batch data containing height, validators root, and results root diff --git a/test/prover/Secp256k1Prover.test.ts b/test/prover/Secp256k1Prover.test.ts index 0ae2099..bd0b0fb 100644 --- a/test/prover/Secp256k1Prover.test.ts +++ b/test/prover/Secp256k1Prover.test.ts @@ -2,7 +2,7 @@ import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers'; import { SimpleMerkleTree } from '@openzeppelin/merkle-tree'; import { expect } from 'chai'; import type { Wallet } from 'ethers'; -import { ethers } from 'hardhat'; +import { ethers, upgrades } from 'hardhat'; import type { SedaDataTypes } from '../../typechain-types/contracts/libraries'; import { computeResultLeafHash, @@ -74,8 +74,8 @@ describe('Secp256k1Prover', () => { // Deploy the contract const ProverFactory = await ethers.getContractFactory('Secp256k1ProverV1'); - const prover = await ProverFactory.deploy(); - await prover.initialize(initialBatch); + const prover = await upgrades.deployProxy(ProverFactory, [initialBatch], { initializer: 'initialize' }); + await prover.waitForDeployment(); return { prover, wallets, data }; } @@ -359,8 +359,8 @@ describe('Secp256k1Prover', () => { // Deploy the contract const ProverFactory = await ethers.getContractFactory('Secp256k1ProverV1'); - const prover = await ProverFactory.deploy(); - await prover.initialize(testBatch); + const prover = await upgrades.deployProxy(ProverFactory, [testBatch], { initializer: 'initialize' }); + await prover.waitForDeployment(); expect(prover).to.emit(prover, 'BatchPosted').withArgs(testBatch.batchHeight, expectedBatchId); }); From ff34277d685d707e6f373a6c25f3b5097ce312fb Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Thu, 5 Dec 2024 14:59:29 +0000 Subject: [PATCH 10/22] feat(core): add support for UUPS proxy --- contracts/core/SedaCorePermissioned.sol | 94 ++++++++++-------- contracts/core/SedaCoreV1.sol | 95 +++++++++++++------ .../core/abstract/RequestHandlerBase.sol | 2 +- contracts/core/abstract/ResultHandlerBase.sol | 60 +++++++++--- 4 files changed, 167 insertions(+), 84 deletions(-) diff --git a/contracts/core/SedaCorePermissioned.sol b/contracts/core/SedaCorePermissioned.sol index d417262..d5f5034 100644 --- a/contracts/core/SedaCorePermissioned.sol +++ b/contracts/core/SedaCorePermissioned.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.24; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -11,19 +11,23 @@ import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; /// @title SedaCorePermissioned /// @notice Core contract for the Seda protocol with permissioned access, managing requests and results -/// @dev Implements RequestHandlerBase, IResultHandler, AccessControl, Pausable, and ReentrancyGuard functionalities +/// @dev Implements RequestHandlerBase, IResultHandler, AccessControl, and Pausable functionalities contract SedaCorePermissioned is RequestHandlerBase, IResultHandler, AccessControl, Pausable { using EnumerableSet for EnumerableSet.Bytes32Set; - // Constants + // ============ Constants ============ + bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE"); bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); - // State variables + // ============ State Variables ============ + uint16 public maxReplicationFactor; mapping(bytes32 => SedaDataTypes.Result) public results; EnumerableSet.Bytes32Set private pendingRequests; + // ============ Constructor ============ + /// @notice Contract constructor /// @param relayers The initial list of relayer addresses to be granted the RELAYER_ROLE /// @param initialMaxReplicationFactor The initial maximum replication factor @@ -41,48 +45,14 @@ contract SedaCorePermissioned is RequestHandlerBase, IResultHandler, AccessContr maxReplicationFactor = initialMaxReplicationFactor; } + // ============ External Functions ============ + /// @notice Sets the maximum replication factor that can be used for requests /// @param newMaxReplicationFactor The new maximum replication factor function setMaxReplicationFactor(uint16 newMaxReplicationFactor) external onlyRole(ADMIN_ROLE) { maxReplicationFactor = newMaxReplicationFactor; } - /// @notice Posts a new request - /// @param inputs The request inputs - /// @return requestId The ID of the posted request - function postRequest( - SedaDataTypes.RequestInputs calldata inputs - ) public override(RequestHandlerBase) whenNotPaused returns (bytes32) { - uint16 replicationFactor = inputs.replicationFactor; - if (replicationFactor > maxReplicationFactor || replicationFactor == 0) { - revert InvalidReplicationFactor(); - } - - bytes32 requestId = SedaDataTypes.deriveRequestId(inputs); - if (bytes(requests[requestId].version).length != 0) { - revert RequestAlreadyExists(requestId); - } - - requests[requestId] = SedaDataTypes.Request({ - version: SedaDataTypes.VERSION, - execProgramId: inputs.execProgramId, - execInputs: inputs.execInputs, - execGasLimit: inputs.execGasLimit, - tallyProgramId: inputs.tallyProgramId, - tallyInputs: inputs.tallyInputs, - tallyGasLimit: inputs.tallyGasLimit, - replicationFactor: inputs.replicationFactor, - consensusFilter: inputs.consensusFilter, - gasPrice: inputs.gasPrice, - memo: inputs.memo - }); - - _addPendingRequest(requestId); - - emit RequestPosted(requestId); - return requestId; - } - /// @notice Posts a result for a request /// @param result The result data function postResult( @@ -125,6 +95,44 @@ contract SedaCorePermissioned is RequestHandlerBase, IResultHandler, AccessContr return results[requestId]; } + // ============ Public Functions ============ + + /// @notice Posts a new request + /// @param inputs The request inputs + /// @return requestId The ID of the posted request + function postRequest( + SedaDataTypes.RequestInputs calldata inputs + ) public override(RequestHandlerBase) whenNotPaused returns (bytes32) { + uint16 replicationFactor = inputs.replicationFactor; + if (replicationFactor > maxReplicationFactor || replicationFactor == 0) { + revert InvalidReplicationFactor(); + } + + bytes32 requestId = SedaDataTypes.deriveRequestId(inputs); + if (bytes(requests[requestId].version).length != 0) { + revert RequestAlreadyExists(requestId); + } + + requests[requestId] = SedaDataTypes.Request({ + version: SedaDataTypes.VERSION, + execProgramId: inputs.execProgramId, + execInputs: inputs.execInputs, + execGasLimit: inputs.execGasLimit, + tallyProgramId: inputs.tallyProgramId, + tallyInputs: inputs.tallyInputs, + tallyGasLimit: inputs.tallyGasLimit, + replicationFactor: inputs.replicationFactor, + consensusFilter: inputs.consensusFilter, + gasPrice: inputs.gasPrice, + memo: inputs.memo + }); + + _addPendingRequest(requestId); + + emit RequestPosted(requestId); + return requestId; + } + /// @notice Retrieves a list of pending request IDs /// @param offset The starting index in the pendingRequests set /// @param limit The maximum number of request IDs to return @@ -138,13 +146,15 @@ contract SedaCorePermissioned is RequestHandlerBase, IResultHandler, AccessContr uint256 actualLimit = (offset + limit > totalRequests) ? totalRequests - offset : limit; SedaDataTypes.Request[] memory queriedPendingRequests = new SedaDataTypes.Request[](actualLimit); for (uint256 i = 0; i < actualLimit; i++) { - bytes32 requestId = pendingRequests.at(offset + i); // Get request ID + bytes32 requestId = pendingRequests.at(offset + i); queriedPendingRequests[i] = requests[requestId]; } return queriedPendingRequests; } + // ============ Admin Functions ============ + /// @notice Adds a relayer /// @param account The address of the relayer to add function addRelayer(address account) external onlyRole(ADMIN_ROLE) { @@ -167,6 +177,8 @@ contract SedaCorePermissioned is RequestHandlerBase, IResultHandler, AccessContr _unpause(); } + // ============ Internal Functions ============ + /// @notice Adds a request ID to the pendingRequests set /// @param requestId The ID of the request to add function _addPendingRequest(bytes32 requestId) internal { diff --git a/contracts/core/SedaCoreV1.sol b/contracts/core/SedaCoreV1.sol index b17699b..81e84a3 100644 --- a/contracts/core/SedaCoreV1.sol +++ b/contracts/core/SedaCoreV1.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.24; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -15,12 +15,30 @@ import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; contract SedaCoreV1 is RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, OwnableUpgradeable { using EnumerableSet for EnumerableSet.Bytes32Set; - // Enumerable Set to store the request IDs that are pending - // `pendingRequests` keeps track of all active data requests that have been posted but not yet fulfilled. - // This set is used to manage the lifecycle of requests, allowing easy retrieval and status tracking. - // When a request is posted, it is added to `pendingRequests`. - // When a result is posted and the request is fulfilled, it is removed from `pendingRequests` - EnumerableSet.Bytes32Set private pendingRequests; + // ============ Constants ============ + + // Constant storage slot for the state following the ERC-7201 standard + bytes32 private constant SEDA_CORE_V1_STORAGE_SLOT = + keccak256(abi.encode(uint256(keccak256("sedacore.storage.v1")) - 1)) & ~bytes32(uint256(0xff)); + + // ============ Storage ============ + + /// @custom:storage-location erc7201:sedacore.storage.v1 + struct SedaCoreStorage { + // Enumerable Set to store the request IDs that are pending + // `pendingRequests` keeps track of all active data requests that have been posted but not yet fulfilled. + // This set is used to manage the lifecycle of requests, allowing easy retrieval and status tracking. + // When a request is posted, it is added to `pendingRequests`. + // When a result is posted and the request is fulfilled, it is removed from `pendingRequests` + EnumerableSet.Bytes32Set pendingRequests; + } + + // ============ Constructor & Initializer ============ + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } /// @notice Initializes the SedaCoreV1 contract /// @param sedaProverAddress The address of the Seda prover contract @@ -31,27 +49,7 @@ contract SedaCoreV1 is RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, O __UUPSUpgradeable_init(); } - /// @notice Retrieves a list of active requests - /// @dev This function is gas-intensive due to iteration over the pendingRequests array. - /// Users should be cautious when using high `limit` values in production environments, as it can result in high gas consumption. - /// @param offset The starting index in the pendingRequests array - /// @param limit The maximum number of requests to return - /// @return An array of SedaDataTypes.Request structs - function getPendingRequests(uint256 offset, uint256 limit) public view returns (SedaDataTypes.Request[] memory) { - uint256 totalRequests = pendingRequests.length(); - if (offset >= totalRequests) { - return new SedaDataTypes.Request[](0); - } - - uint256 actualLimit = (offset + limit > totalRequests) ? totalRequests - offset : limit; - SedaDataTypes.Request[] memory queriedPendingRequests = new SedaDataTypes.Request[](actualLimit); - for (uint256 i = 0; i < actualLimit; i++) { - bytes32 requestId = pendingRequests.at(offset + i); - queriedPendingRequests[i] = requests[requestId]; - } - - return queriedPendingRequests; - } + // ============ External Functions ============ /// @inheritdoc RequestHandlerBase /// @dev Overrides the base implementation to also add the request ID to the pendingRequests array @@ -77,12 +75,49 @@ contract SedaCoreV1 is RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, O return resultId; } + // ============ Public View Functions ============ + + /// @notice Retrieves a list of active requests + /// @dev This function is gas-intensive due to iteration over the pendingRequests array. + /// Users should be cautious when using high `limit` values in production environments, as it can result in high gas consumption. + /// @param offset The starting index in the pendingRequests array + /// @param limit The maximum number of requests to return + /// @return An array of SedaDataTypes.Request structs + function getPendingRequests(uint256 offset, uint256 limit) public view returns (SedaDataTypes.Request[] memory) { + uint256 totalRequests = _storageV1().pendingRequests.length(); + if (offset >= totalRequests) { + return new SedaDataTypes.Request[](0); + } + + uint256 actualLimit = (offset + limit > totalRequests) ? totalRequests - offset : limit; + SedaDataTypes.Request[] memory queriedPendingRequests = new SedaDataTypes.Request[](actualLimit); + for (uint256 i = 0; i < actualLimit; i++) { + bytes32 requestId = _storageV1().pendingRequests.at(offset + i); + queriedPendingRequests[i] = requests[requestId]; + } + + return queriedPendingRequests; + } + + // ============ Internal Functions ============ + + /// @notice Returns the storage struct for the contract + /// @dev Uses ERC-7201 storage pattern to access the storage struct at a specific slot + /// @return s The storage struct containing the contract's state variables + function _storageV1() internal pure returns (SedaCoreStorage storage s) { + bytes32 slot = SEDA_CORE_V1_STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + s.slot := slot + } + } + /// @notice Adds a request ID to the pendingRequests set /// @dev This function is internal to ensure that only the contract's internal logic can add requests, /// preventing unauthorized additions and maintaining proper state management. /// @param requestId The ID of the request to add function _addRequest(bytes32 requestId) internal { - pendingRequests.add(requestId); + _storageV1().pendingRequests.add(requestId); } /// @notice Removes a request ID from the pendingRequests set if it exists @@ -90,7 +125,7 @@ contract SedaCoreV1 is RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, O /// maintaining proper state transitions and preventing unauthorized removals. /// @param requestId The ID of the request to remove function _removeRequest(bytes32 requestId) internal { - pendingRequests.remove(requestId); + _storageV1().pendingRequests.remove(requestId); } /// @dev Required override for UUPSUpgradeable. Ensures only the owner can upgrade the implementation. diff --git a/contracts/core/abstract/RequestHandlerBase.sol b/contracts/core/abstract/RequestHandlerBase.sol index 7dc48c2..5300f1b 100644 --- a/contracts/core/abstract/RequestHandlerBase.sol +++ b/contracts/core/abstract/RequestHandlerBase.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.24; import {SedaDataTypes} from "../../libraries/SedaDataTypes.sol"; import {IRequestHandler} from "../../interfaces/IRequestHandler.sol"; diff --git a/contracts/core/abstract/ResultHandlerBase.sol b/contracts/core/abstract/ResultHandlerBase.sol index 7d489b2..ca01f13 100644 --- a/contracts/core/abstract/ResultHandlerBase.sol +++ b/contracts/core/abstract/ResultHandlerBase.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.24; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; @@ -10,20 +10,41 @@ import {IResultHandler} from "../../interfaces/IResultHandler.sol"; /// @title ResultHandler /// @notice Implements the ResultHandlerBase for managing Seda protocol results abstract contract ResultHandlerBase is IResultHandler, Initializable { - IProver public sedaProver; + // ============ Errors ============ + // Note: Errors are defined in IResultHandler interface - // Mapping of request IDs to Result structs - mapping(bytes32 => SedaDataTypes.Result) public results; + // ============ Constants ============ + + // Define a unique storage slot for ResultHandlerBase + bytes32 private constant RESULT_HANDLER_STORAGE_SLOT = + keccak256(abi.encode(uint256(keccak256("seda.resulthandler.storage")) - 1)) & ~bytes32(uint256(0xff)); + + // ============ Storage ============ + + /// @custom:storage-location erc7201:seda.resulthandler.storage + struct ResultHandlerStorage { + IProver sedaProver; + // Mapping of request IDs to Result structs + mapping(bytes32 => SedaDataTypes.Result) results; + } + + // ============ Constructor & Initializer ============ + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } - // Remove constructor and add initialization function /// @notice Initializes the ResultHandler contract /// @dev Sets up the contract with the provided Seda prover address /// @param sedaProverAddress The address of the Seda prover contract // solhint-disable-next-line func-name-mixedcase function __ResultHandler_init(address sedaProverAddress) internal onlyInitializing { - sedaProver = IProver(sedaProverAddress); + _resultHandlerStorage().sedaProver = IProver(sedaProverAddress); } + // ============ External Functions ============ + /// @inheritdoc IResultHandler function postResult( SedaDataTypes.Result calldata result, @@ -31,27 +52,29 @@ abstract contract ResultHandlerBase is IResultHandler, Initializable { bytes32[] calldata proof ) public virtual override(IResultHandler) returns (bytes32) { bytes32 resultId = SedaDataTypes.deriveResultId(result); - if (results[result.drId].drId != bytes32(0)) { + if (_resultHandlerStorage().results[result.drId].drId != bytes32(0)) { revert ResultAlreadyExists(resultId); } - if (!sedaProver.verifyResultProof(resultId, batchHeight, proof)) { + if (!_resultHandlerStorage().sedaProver.verifyResultProof(resultId, batchHeight, proof)) { revert InvalidResultProof(resultId); } - results[result.drId] = result; + _resultHandlerStorage().results[result.drId] = result; emit ResultPosted(resultId); return resultId; } + // ============ Public View Functions ============ + /// @inheritdoc IResultHandler function getResult(bytes32 requestId) public view override(IResultHandler) returns (SedaDataTypes.Result memory) { - SedaDataTypes.Result memory result = results[requestId]; + SedaDataTypes.Result memory result = _resultHandlerStorage().results[requestId]; if (bytes(result.version).length == 0) { revert ResultNotFound(requestId); } - return results[requestId]; + return _resultHandlerStorage().results[requestId]; } /// @notice Verifies the result without storing it @@ -65,7 +88,7 @@ abstract contract ResultHandlerBase is IResultHandler, Initializable { bytes32[] calldata proof ) public view returns (bytes32) { bytes32 resultId = SedaDataTypes.deriveResultId(result); - if (!sedaProver.verifyResultProof(resultId, batchHeight, proof)) { + if (!_resultHandlerStorage().sedaProver.verifyResultProof(resultId, batchHeight, proof)) { revert InvalidResultProof(resultId); } @@ -78,4 +101,17 @@ abstract contract ResultHandlerBase is IResultHandler, Initializable { function deriveResultId(SedaDataTypes.Result calldata result) public pure returns (bytes32) { return SedaDataTypes.deriveResultId(result); } + + // ============ Internal Functions ============ + + /// @notice Returns the storage struct for the contract + /// @dev Uses ERC-7201 storage pattern to access the storage struct at a specific slot + /// @return s The storage struct containing the contract's state variables + function _resultHandlerStorage() private pure returns (ResultHandlerStorage storage s) { + bytes32 slot = RESULT_HANDLER_STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + s.slot := slot + } + } } From 8fadfa0778033712d30451cbfa1c2b9822468922 Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Thu, 5 Dec 2024 15:00:17 +0000 Subject: [PATCH 11/22] refactor: rename storage internal functions --- contracts/interfaces/IRequestHandler.sol | 2 +- contracts/interfaces/IResultHandler.sol | 2 +- contracts/libraries/SedaDataTypes.sol | 2 +- contracts/mocks/MockSecp256k1ProverV2.sol | 9 +++++---- contracts/provers/Secp256k1ProverV1.sol | 18 +++++++++--------- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/contracts/interfaces/IRequestHandler.sol b/contracts/interfaces/IRequestHandler.sol index 6b689cd..1aa28ee 100644 --- a/contracts/interfaces/IRequestHandler.sol +++ b/contracts/interfaces/IRequestHandler.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.24; import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; diff --git a/contracts/interfaces/IResultHandler.sol b/contracts/interfaces/IResultHandler.sol index 3d1b952..74d7ae7 100644 --- a/contracts/interfaces/IResultHandler.sol +++ b/contracts/interfaces/IResultHandler.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.24; import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; diff --git a/contracts/libraries/SedaDataTypes.sol b/contracts/libraries/SedaDataTypes.sol index c4d3c94..f8731f9 100644 --- a/contracts/libraries/SedaDataTypes.sol +++ b/contracts/libraries/SedaDataTypes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.24; // TODO: Rearrange struct fields to minimize storage gaps and optimize packing diff --git a/contracts/mocks/MockSecp256k1ProverV2.sol b/contracts/mocks/MockSecp256k1ProverV2.sol index 82dde7b..9126ef9 100644 --- a/contracts/mocks/MockSecp256k1ProverV2.sol +++ b/contracts/mocks/MockSecp256k1ProverV2.sol @@ -12,9 +12,10 @@ contract MockSecp256k1ProverV2 is Secp256k1ProverV1 { // ============ Constants ============ bytes32 private constant PROVER_V2_STORAGE_SLOT = - keccak256(abi.encode(uint256(keccak256("secp256k1prover.v2.storage")) - 1)) & ~bytes32(uint256(0xff)); + keccak256(abi.encode(uint256(keccak256("secp256k1prover.storage.v2")) - 1)) & ~bytes32(uint256(0xff)); // ============ Storage ============ + /// @custom:storage-location secp256k1prover.storage.v2 struct V2Storage { string version; } @@ -26,7 +27,7 @@ contract MockSecp256k1ProverV2 is Secp256k1ProverV1 { } function initialize() external reinitializer(2) onlyOwner { - V2Storage storage s = _v2Storage(); + V2Storage storage s = _storageV2(); s.version = "2.0.0"; } @@ -34,11 +35,11 @@ contract MockSecp256k1ProverV2 is Secp256k1ProverV1 { /// @notice Returns the version string from V2 storage /// @return version The version string function getVersion() external view returns (string memory) { - return _v2Storage().version; + return _storageV2().version; } // ============ Internal Functions ============ - function _v2Storage() internal pure returns (V2Storage storage s) { + function _storageV2() internal pure returns (V2Storage storage s) { bytes32 slot = PROVER_V2_STORAGE_SLOT; // solhint-disable-next-line no-inline-assembly assembly { diff --git a/contracts/provers/Secp256k1ProverV1.sol b/contracts/provers/Secp256k1ProverV1.sol index 9170c3c..ea4af57 100644 --- a/contracts/provers/Secp256k1ProverV1.sol +++ b/contracts/provers/Secp256k1ProverV1.sol @@ -30,11 +30,11 @@ contract Secp256k1ProverV1 is ProverBase, Initializable, UUPSUpgradeable, Ownabl bytes1 internal constant SECP256K1_DOMAIN_SEPARATOR = 0x01; // Constant storage slot for the state following the ERC-7201 standard bytes32 private constant PROVER_V1_STORAGE_SLOT = - keccak256(abi.encode(uint256(keccak256("secp256k1prover.v1.storage")) - 1)) & ~bytes32(uint256(0xff)); + keccak256(abi.encode(uint256(keccak256("secp256k1prover.storage.v1")) - 1)) & ~bytes32(uint256(0xff)); // ============ Storage ============ - /// @custom:storage-location secp256k1prover.v1.storage + /// @custom:storage-location secp256k1prover.storage.v1 struct Secp256k1ProverStorage { uint64 lastBatchHeight; bytes32 lastValidatorsRoot; @@ -57,7 +57,7 @@ contract Secp256k1ProverV1 is ProverBase, Initializable, UUPSUpgradeable, Ownabl __UUPSUpgradeable_init(); // Existing initialization code - Secp256k1ProverStorage storage s = _storage(); + Secp256k1ProverStorage storage s = _storageV1(); s.batchToResultsRoot[initialBatch.batchHeight] = initialBatch.resultsRoot; s.lastBatchHeight = initialBatch.batchHeight; s.lastValidatorsRoot = initialBatch.validatorsRoot; @@ -82,7 +82,7 @@ contract Secp256k1ProverV1 is ProverBase, Initializable, UUPSUpgradeable, Ownabl bytes[] calldata signatures, SedaDataTypes.ValidatorProof[] calldata validatorProofs ) public override { - Secp256k1ProverStorage storage s = _storage(); + Secp256k1ProverStorage storage s = _storageV1(); // Check that new batch invariants hold if (newBatch.batchHeight <= s.lastBatchHeight) { revert InvalidBatchHeight(); @@ -130,7 +130,7 @@ contract Secp256k1ProverV1 is ProverBase, Initializable, UUPSUpgradeable, Ownabl uint64 batchHeight, bytes32[] calldata merkleProof ) public view override returns (bool) { - Secp256k1ProverStorage storage s = _storage(); + Secp256k1ProverStorage storage s = _storageV1(); bytes32 leaf = keccak256(abi.encodePacked(RESULT_DOMAIN_SEPARATOR, resultId)); return MerkleProof.verify(merkleProof, s.batchToResultsRoot[batchHeight], leaf); } @@ -138,20 +138,20 @@ contract Secp256k1ProverV1 is ProverBase, Initializable, UUPSUpgradeable, Ownabl /// @notice Returns the last processed batch height /// @return The height of the last batch function getLastBatchHeight() public view returns (uint64) { - return _storage().lastBatchHeight; + return _storageV1().lastBatchHeight; } /// @notice Returns the last validators root hash /// @return The Merkle root of the last validator set function getLastValidatorsRoot() public view returns (bytes32) { - return _storage().lastValidatorsRoot; + return _storageV1().lastValidatorsRoot; } /// @notice Returns the results root for a specific batch height /// @param batchHeight The batch height to query /// @return The results root for the specified batch function getBatchResultsRoot(uint64 batchHeight) public view returns (bytes32) { - return _storage().batchToResultsRoot[batchHeight]; + return _storageV1().batchToResultsRoot[batchHeight]; } // ============ Internal Functions ============ @@ -159,7 +159,7 @@ contract Secp256k1ProverV1 is ProverBase, Initializable, UUPSUpgradeable, Ownabl /// @notice Returns the storage struct for the contract /// @dev Uses ERC-7201 storage pattern to access the storage struct at a specific slot /// @return s The storage struct containing the contract's state variables - function _storage() internal pure returns (Secp256k1ProverStorage storage s) { + function _storageV1() internal pure returns (Secp256k1ProverStorage storage s) { bytes32 slot = PROVER_V1_STORAGE_SLOT; // solhint-disable-next-line no-inline-assembly assembly { From ccf94c52fdcfae35e65fd8e05114310461ec0706 Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Thu, 5 Dec 2024 15:00:41 +0000 Subject: [PATCH 12/22] test(core): use proxy in test deployments --- test/core/ResultHandler.test.ts | 98 ++++++++++++++++----------------- test/core/SedaCoreV1.test.ts | 10 ++-- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/test/core/ResultHandler.test.ts b/test/core/ResultHandler.test.ts index 5309d42..9ac175c 100644 --- a/test/core/ResultHandler.test.ts +++ b/test/core/ResultHandler.test.ts @@ -1,7 +1,7 @@ import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers'; import { SimpleMerkleTree } from '@openzeppelin/merkle-tree'; import { expect } from 'chai'; -import { ethers } from 'hardhat'; +import { ethers, upgrades } from 'hardhat'; import { compareResults } from '../helpers'; import { computeResultLeafHash, deriveDataResultId, generateDataFixtures } from '../utils'; @@ -34,31 +34,31 @@ describe('ResultHandler', () => { // Deploy the contract const ProverFactory = await ethers.getContractFactory('Secp256k1ProverV1'); - const prover = await ProverFactory.deploy(); - await prover.initialize(initialBatch); + const prover = await upgrades.deployProxy(ProverFactory, [initialBatch], { initializer: 'initialize' }); + await prover.waitForDeployment(); - const ResultHandlerFactory = await ethers.getContractFactory('SedaCoreV1'); - const handler = await ResultHandlerFactory.deploy(); - await handler.initialize(prover.getAddress()); + const CoreFactory = await ethers.getContractFactory('SedaCoreV1'); + const core = await upgrades.deployProxy(CoreFactory, [await prover.getAddress()], { initializer: 'initialize' }); + await core.waitForDeployment(); - return { handler, data }; + return { core, data }; } describe('deriveResultId', () => { it('should generate consistent data result IDs', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); const resultIdFromUtils = deriveDataResultId(data.results[0]); - const resultId = await handler.deriveResultId.staticCall(data.results[0]); + const resultId = await core.deriveResultId.staticCall(data.results[0]); expect(resultId).to.equal(resultIdFromUtils); }); it('should generate different IDs for different results', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); - const id1 = await handler.deriveResultId.staticCall(data.results[0]); - const id2 = await handler.deriveResultId.staticCall(data.results[1]); + const id1 = await core.deriveResultId.staticCall(data.results[0]); + const id2 = await core.deriveResultId.staticCall(data.results[1]); expect(id1).to.not.equal(id2); }); @@ -66,117 +66,117 @@ describe('ResultHandler', () => { describe('postResult', () => { it('should successfully post a result and read it back', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); - await handler.postResult(data.results[0], 0, data.proofs[0]); + await core.postResult(data.results[0], 0, data.proofs[0]); - const postedResult = await handler.getResult(data.results[0].drId); + const postedResult = await core.getResult(data.results[0].drId); compareResults(postedResult, data.results[0]); }); it('should fail to post a result that already exists', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); - await handler.postResult(data.results[0], 0, data.proofs[0]); + await core.postResult(data.results[0], 0, data.proofs[0]); const resultId = deriveDataResultId(data.results[0]); - await expect(handler.postResult(data.results[0], 0, data.proofs[0])) - .to.be.revertedWithCustomError(handler, 'ResultAlreadyExists') + await expect(core.postResult(data.results[0], 0, data.proofs[0])) + .to.be.revertedWithCustomError(core, 'ResultAlreadyExists') .withArgs(resultId); }); it('should fail to post a result with invalid proof', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); const resultId = deriveDataResultId(data.results[1]); - await expect(handler.postResult(data.results[1], 0, data.proofs[0])) - .to.be.revertedWithCustomError(handler, 'InvalidResultProof') + await expect(core.postResult(data.results[1], 0, data.proofs[0])) + .to.be.revertedWithCustomError(core, 'InvalidResultProof') .withArgs(resultId); }); it('should emit a ResultPosted event', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); - await expect(handler.postResult(data.results[0], 0, data.proofs[0])) - .to.emit(handler, 'ResultPosted') + await expect(core.postResult(data.results[0], 0, data.proofs[0])) + .to.emit(core, 'ResultPosted') .withArgs(deriveDataResultId(data.results[0])); }); it('should fail to post a result with empty proof', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); const resultId = deriveDataResultId(data.results[0]); - await expect(handler.postResult(data.results[0], 0, [])) - .to.be.revertedWithCustomError(handler, 'InvalidResultProof') + await expect(core.postResult(data.results[0], 0, [])) + .to.be.revertedWithCustomError(core, 'InvalidResultProof') .withArgs(resultId); }); it('should fail to post a result with invalid drId', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); const invalidResult = { ...data.results[0], drId: ethers.ZeroHash }; const resultId = deriveDataResultId(invalidResult); - await expect(handler.postResult(invalidResult, 0, data.proofs[0])) - .to.be.revertedWithCustomError(handler, 'InvalidResultProof') + await expect(core.postResult(invalidResult, 0, data.proofs[0])) + .to.be.revertedWithCustomError(core, 'InvalidResultProof') .withArgs(resultId); }); }); describe('getResult', () => { it('should revert with ResultNotFound for non-existent result id', async () => { - const { handler } = await loadFixture(deployResultHandlerFixture); + const { core } = await loadFixture(deployResultHandlerFixture); const nonExistentId = ethers.ZeroHash; - await expect(handler.getResult(nonExistentId)) - .to.be.revertedWithCustomError(handler, 'ResultNotFound') + await expect(core.getResult(nonExistentId)) + .to.be.revertedWithCustomError(core, 'ResultNotFound') .withArgs(nonExistentId); }); it('should return the correct result for an existing result id', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); - await handler.postResult(data.results[0], 0, data.proofs[0]); - const retrievedResult = await handler.getResult(data.results[0].drId); + await core.postResult(data.results[0], 0, data.proofs[0]); + const retrievedResult = await core.getResult(data.results[0].drId); compareResults(retrievedResult, data.results[0]); }); it('should return the correct result for multiple posted results', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); // Post two results - await handler.postResult(data.results[0], 0, data.proofs[0]); - await handler.postResult(data.results[1], 0, data.proofs[1]); + await core.postResult(data.results[0], 0, data.proofs[0]); + await core.postResult(data.results[1], 0, data.proofs[1]); // Retrieve and verify both results - const retrievedResult1 = await handler.getResult(data.results[0].drId); - const retrievedResult2 = await handler.getResult(data.results[1].drId); + const retrievedResult1 = await core.getResult(data.results[0].drId); + const retrievedResult2 = await core.getResult(data.results[1].drId); compareResults(retrievedResult1, data.results[0]); compareResults(retrievedResult2, data.results[1]); // Try to get a non-existent result const nonExistentId = ethers.randomBytes(32); - await expect(handler.getResult(nonExistentId)) - .to.be.revertedWithCustomError(handler, 'ResultNotFound') + await expect(core.getResult(nonExistentId)) + .to.be.revertedWithCustomError(core, 'ResultNotFound') .withArgs(nonExistentId); }); }); describe('verifyResult', () => { it('should successfully verify a valid result', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); - const resultId = await handler.verifyResult(data.results[0], 0, data.proofs[0]); + const resultId = await core.verifyResult(data.results[0], 0, data.proofs[0]); expect(resultId).to.equal(deriveDataResultId(data.results[0])); }); it('should fail to verify a result with invalid proof', async () => { - const { handler, data } = await loadFixture(deployResultHandlerFixture); + const { core, data } = await loadFixture(deployResultHandlerFixture); const resultId = deriveDataResultId(data.results[1]); - await expect(handler.verifyResult(data.results[1], 0, data.proofs[0])) - .to.be.revertedWithCustomError(handler, 'InvalidResultProof') + await expect(core.verifyResult(data.results[1], 0, data.proofs[0])) + .to.be.revertedWithCustomError(core, 'InvalidResultProof') .withArgs(resultId); }); }); diff --git a/test/core/SedaCoreV1.test.ts b/test/core/SedaCoreV1.test.ts index 043fe56..8e48018 100644 --- a/test/core/SedaCoreV1.test.ts +++ b/test/core/SedaCoreV1.test.ts @@ -1,7 +1,7 @@ import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers'; import { SimpleMerkleTree } from '@openzeppelin/merkle-tree'; import { expect } from 'chai'; -import { ethers } from 'hardhat'; +import { ethers, upgrades } from 'hardhat'; import { compareRequests, compareResults, convertToRequestInputs } from '../helpers'; import { computeResultLeafHash, deriveDataResultId, deriveRequestId, generateDataFixtures } from '../utils'; @@ -26,12 +26,12 @@ describe('SedaCoreV1', () => { }; const ProverFactory = await ethers.getContractFactory('Secp256k1ProverV1'); - const prover = await ProverFactory.deploy(); - await prover.initialize(initialBatch); + const prover = await upgrades.deployProxy(ProverFactory, [initialBatch], { initializer: 'initialize' }); + await prover.waitForDeployment(); const CoreFactory = await ethers.getContractFactory('SedaCoreV1'); - const core = await CoreFactory.deploy(); - await core.initialize(await prover.getAddress()); + const core = await upgrades.deployProxy(CoreFactory, [await prover.getAddress()], { initializer: 'initialize' }); + await core.waitForDeployment(); return { prover, core, data }; } From 48e09a9b888c6c55be3ee8c16f9d716011f63ff9 Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Fri, 6 Dec 2024 11:19:17 +0000 Subject: [PATCH 13/22] chore: bump versions --- bun.lockb | Bin 280060 -> 280060 bytes package.json | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lockb b/bun.lockb index cd52cac3b57ba4efd4f74c59b3fd4e2a74c9118b..f48a9295a4b5b9d270e4e0706d6e7525f25b179e 100755 GIT binary patch delta 1314 zcmeyKfaDtw~uW*?O<*a`dx79k$<1v-Y_;IJ4Q^fawTVbVHaFe!g;6^{KfXO9c z8j}_DI5w{c2r!uZ#Tm?*tkEa{XJ;U>S3ubhGC3!|Y~+Wl(P%*%z0Yvp#EHwcw>mHuIMr|A>e&=@{`cS6w~qNg zTE3U*;YR7=CC)S08VW7Cj_R#m9uj85rOt}^48Vv->oBZ+YYS@__e(A zxliX*%T1CMY39Xz+ZvN}%l>SeC41lHT=$h->2eCyZ>IM)GfLFYkzFS$Gv}Ouex&1X zGlMk>?^er|XfOQxu|R#7(ahy%f0o&{vz->M?sz9@loI2pckyc9?u&uHejjt}l>9Tf z_t_gH=lLT$Pe$2v*O>+LA`U}|!krs=jVK<5c+pERCk8>4GpJHu9UiNc-Ax|w^b z%Z@DZX;3d~3VK^R>EudH{Yx{KJeje^()9i;o6VR1PHl>{d9!HslZ8L4m~xSv7l`7# zEXR+36F+rm*_3<=n$z#HtT4!T>2ANR&G+tF@?U$P6St>V=iiDAl1~iEgR9l_zAR@c z>O0%Awa+Kj@L})eNTWB?--4WXyyjr?@9*xOD@x^@&ISEiar~&^4TJJk>}#`vC%v9N z=c8Zzktz41R(;<8?Y{eu&Bk`S9J@YL?3;0Gw_ER2@y6ueNX`pJb{_McL!8^r9+C2H zGTA!8DQMXmk3ESE@$C|M6K-(&&cA1xWz9D8&sA&5*D+hYemA*B`5lsCid#@*e(kr+ zFZXVzAMM-cwlYR=I6U6fUnJsm%FXF=VdBIF#dUu~9t#{4>Gp$a%SH)k?u7q z-)9wHSmk5C@h)HD@4ikJrU%{^-sO7j?=4pB@I9%qUE{7GcPhie{}*RyNQp4Jl<~Gd z?PJ{jw2vt}i_v^~;7=wgM)U1CKbaf_8BM1T`P(OIF*DWy zEmM5KEWCZ$3T7!YM$_#F?=kx_PXA=jqA*>-o<(rF{R3tlByRkDX2I!eA23Uzig8U> zxX;WsJ?}O%`*gmCSY)EF_9bs0Oeu0gJ SZ#v^E=5@dj+3KfaDtvfvPRcAN4Uv+akx z{?9${7xT{j*#X(w=i*nUR_zRYzOCSf^JFKZ1KXEm%Ki8tv%S@UvB0T5iR;o%+fR$9 z1x(z1P-=;FOa4T@Ki`uzgeG23cy#S+x|`LKtw+yu-4SM;rK0E%`h4cmJ8m@^Y=&+M zu95e&pH835$iTqJ@Q?o=koE;)7+|@rrP=$^cap<;B~t~KjO_Vw54^=Z)+*tLh z!6eGP=hMYyPI|hM<&Teb-#D;Nm6^%i@@tpUt@ViseDmtmtkb9WHZw}pJA`Bi-Ypw|(9jIa80q z_%G}FXMOcXa-Ki3^X8k0ML$1x$IWBQ^Zo}~ca0dX*Dh>SkmPE1vwyF$GFa%f^{_m}r%68DxozrLv0FW_&n?18uO-^z;|-p|f-DfsOg{HXYOj7O0C53#_Ahwoa? z&N|ipw2yK7(>|u`EJoAmfj^m~7|pil{A6+zWHg!Xpvf$=eS#S?8{_s1+{`L+K%sa$ zW^raSJ(KDDTFjEtCcqRQ8PC9=%D~XDf7W%iwZ8;rF)WhGltpb&$C6_n8H!uYJHQ zi7Li5UEw}6+w{EK%4#7i1I0AJO16hSVyh$2 XVfqC&7QX3>ub9^XLuA7%W}j#Pv Date: Fri, 6 Dec 2024 11:20:24 +0000 Subject: [PATCH 14/22] chore(ignition): remove core v1 module --- ignition/modules/SedaCoreV1.ts | 21 --------------------- ignition/modules/parameters.json | 9 --------- package.json | 6 +++--- 3 files changed, 3 insertions(+), 33 deletions(-) delete mode 100644 ignition/modules/SedaCoreV1.ts diff --git a/ignition/modules/SedaCoreV1.ts b/ignition/modules/SedaCoreV1.ts deleted file mode 100644 index c721b44..0000000 --- a/ignition/modules/SedaCoreV1.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'; -import { ethers } from 'hardhat'; - -const SedaCoreV1Module = buildModule('SedaCoreV1', (m) => { - // Constructor arguments - const initialBatch = m.getParameter('initialBatch'); - - // Deploy Secp256k1Prover contract - const proverContract = m.contract('Secp256k1ProverV1'); - // Initialize the UUPS upgradeable contract - m.call(proverContract, 'initialize', [initialBatch]); - - // Deploy SedaCoreV1 contract - const coreV1Contract = m.contract('SedaCoreV1'); - // Initialize the UUPS upgradeable contract - m.call(coreV1Contract, 'initialize', [proverContract]); - - return { proverContract, coreV1Contract }; -}); - -export default SedaCoreV1Module; diff --git a/ignition/modules/parameters.json b/ignition/modules/parameters.json index 61fa36d..9bc7384 100644 --- a/ignition/modules/parameters.json +++ b/ignition/modules/parameters.json @@ -1,13 +1,4 @@ { - "SedaCoreV1": { - "initialBatch": { - "batchHeight": 3, - "blockHeight": 31, - "validatorsRoot": "0x6b8a7c6cd54c814f4e30b89b5f2e91b9d96860e24eb39366f4c260400fcb47db", - "resultsRoot": "0x56c4f39b7564ea6a32877fc98743652998753c4cd8a4b455c26dcb3b92774b73", - "provingMetadata": "0x0000000000000000000000000000000000000000000000000000000000000000" - } - }, "SedaCorePermissioned": { "maxReplicationFactor": 1 } diff --git a/package.json b/package.json index f260573..ce3f960 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,12 @@ "version": "0.0.4", "main": "index.js", "scripts": { + "check": "bun run lint && bun run format:sol", "clean": "rimraf artifacts cache typechain-types", "compile": "hardhat compile", "deploy:core:permissioned": "hardhat ignition deploy ignition/modules/SedaCorePermissioned.ts --parameters ignition/modules/parameters.json", - "deploy:core:v1": "hardhat ignition deploy ignition/modules/SedaCoreV1.ts --parameters ignition/modules/parameters.json", - "format:sol": "prettier --write \"**/*.sol\"", - "format:sol:check": "prettier --check \"**/*.sol\"", + "format:sol": "prettier --check \"**/*.sol\"", + "format:sol:fix": "prettier --write \"**/*.sol\"", "gen:testvectors": "bun scripts/generateTestVectors.ts", "lint": "bun run lint:ts && bun run lint:sol", "lint:sol": "solhint 'contracts/**/*.sol' --ignore-path .solhintignore", From 30021caf542339a3db7ea0ca135d4ecb46dc804f Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Fri, 6 Dec 2024 11:23:14 +0000 Subject: [PATCH 15/22] chore(tasks): add deploy tasks --- deployments/parameters.json | 14 +++++ hardhat.config.ts | 6 +- tasks/common/addresses.ts | 51 +++++++++++++++++ tasks/common/config.ts | 28 +++++++++ tasks/common/deployment.ts | 56 ++++++++++++++++++ tasks/common/io.ts | 60 +++++++++++++++++++ tasks/common/logger.ts | 50 ++++++++++++++++ tasks/common/params.ts | 37 ++++++++++++ tasks/common/proxy.ts | 23 ++++++++ tasks/deployAll.ts | 26 +++++++++ tasks/deployCore.ts | 111 ++++++++++++++++++++++++++++++++++++ tasks/deployProver.ts | 84 +++++++++++++++++++++++++++ tasks/index.ts | 35 ++++++++++++ 13 files changed, 579 insertions(+), 2 deletions(-) create mode 100644 deployments/parameters.json create mode 100644 tasks/common/addresses.ts create mode 100644 tasks/common/config.ts create mode 100644 tasks/common/deployment.ts create mode 100644 tasks/common/io.ts create mode 100644 tasks/common/logger.ts create mode 100644 tasks/common/params.ts create mode 100644 tasks/common/proxy.ts create mode 100644 tasks/deployAll.ts create mode 100644 tasks/deployCore.ts create mode 100644 tasks/deployProver.ts create mode 100644 tasks/index.ts diff --git a/deployments/parameters.json b/deployments/parameters.json new file mode 100644 index 0000000..2033034 --- /dev/null +++ b/deployments/parameters.json @@ -0,0 +1,14 @@ +{ + "SedaCoreV1": { + "sedaProverAddress": "0xe2E938Ec34C2f03C6D2Aaa851861eE72891177F0" + }, + "Secp256k1ProverV1": { + "initialBatch": { + "batchHeight": 3, + "blockHeight": 31, + "validatorsRoot": "0x6b8a7c6cd54c814f4e30b89b5f2e91b9d96860e24eb39366f4c260400fcb47db", + "resultsRoot": "0x56c4f39b7564ea6a32877fc98743652998753c4cd8a4b455c26dcb3b92774b73", + "provingMetadata": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 421ca5c..8420fcc 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,7 +1,9 @@ -import type { HardhatUserConfig } from 'hardhat/config'; import '@nomicfoundation/hardhat-toolbox'; -import { getEtherscanConfig, getNetworksConfig } from './config'; import '@openzeppelin/hardhat-upgrades'; +import type { HardhatUserConfig } from 'hardhat/config'; +import { getEtherscanConfig, getNetworksConfig } from './config'; + +import './tasks'; const gasReporterConfig = { currency: 'USD', diff --git a/tasks/common/addresses.ts b/tasks/common/addresses.ts new file mode 100644 index 0000000..48d16d3 --- /dev/null +++ b/tasks/common/addresses.ts @@ -0,0 +1,51 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { CONFIG } from './config'; +import { readFile, writeFile } from './io'; + +const DEPLOYMENTS_FOLDER = CONFIG.DEPLOYMENTS.FOLDER; +const ADDRESSES_FILE = CONFIG.DEPLOYMENTS.FILES.ADDRESSES; + +// Define the type for the addresses object +type Addresses = { + [networkName: string]: { + [contractName: string]: { + proxy: string; + implementation: string; + gitCommitHash: string; + }; + }; +}; + +export async function updateAddresses( + hre: HardhatRuntimeEnvironment, + contractName: string, + proxyAddress: string, + implAddress: string, +) { + const addressesPath = path.join(DEPLOYMENTS_FOLDER, ADDRESSES_FILE); + let addresses: Addresses = {}; + + if (fs.existsSync(addressesPath)) { + const content = await readFile(addressesPath); + if (content.trim()) { + addresses = JSON.parse(content) as Addresses; + } + } + + const networkName = `${hre.network.name}-${(await hre.ethers.provider.getNetwork()).chainId.toString()}`; + if (!addresses[networkName]) { + addresses[networkName] = {}; + } + + const gitCommitHash = require('node:child_process').execSync('git rev-parse HEAD').toString().trim(); + + addresses[networkName][contractName] = { + proxy: proxyAddress, + implementation: implAddress, + gitCommitHash, + }; + + await writeFile(addressesPath, addresses); +} diff --git a/tasks/common/config.ts b/tasks/common/config.ts new file mode 100644 index 0000000..ec22493 --- /dev/null +++ b/tasks/common/config.ts @@ -0,0 +1,28 @@ +export const CONFIG = { + DEPLOYMENTS: { + FOLDER: 'deployments', + FILES: { + ADDRESSES: 'addresses.json', + ARTIFACTS: 'artifacts', + }, + }, + LOGGER: { + ICONS: { + info: '•', + success: '✓', + error: '✗', + warn: '⚠️', + }, + SECTION_ICONS: { + config: '🔧', + deploy: '🚀', + files: '📝', + test: '🧪', + verify: '🔍', + params: '📜', + default: '🔹', + meta: '🌟', + }, + META_BORDER: '━', + }, +} as const; diff --git a/tasks/common/deployment.ts b/tasks/common/deployment.ts new file mode 100644 index 0000000..11fee44 --- /dev/null +++ b/tasks/common/deployment.ts @@ -0,0 +1,56 @@ +import type { Artifact, BuildInfo } from 'hardhat/types'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { CONFIG } from './config'; +import { path, ensureDirectoryExists, writeFile } from './io'; + +export async function updateDeployment(hre: HardhatRuntimeEnvironment, contractName: string) { + const deploymentsDir = path.join(process.cwd(), CONFIG.DEPLOYMENTS.FOLDER); + await ensureDirectoryExists(deploymentsDir); + + const networkId = `${hre.network.name}-${(await hre.ethers.provider.getNetwork()).chainId.toString()}`; + const networkDeployDir = path.join(deploymentsDir, networkId); + const artifactsDir = path.join(networkDeployDir, CONFIG.DEPLOYMENTS.FILES.ARTIFACTS); + + await Promise.all([ensureDirectoryExists(networkDeployDir), ensureDirectoryExists(artifactsDir)]); + + await Promise.all([ + writeBuildInfoToFile(hre, contractName, networkDeployDir), + writeArtifactToFile(hre, contractName, artifactsDir), + ]); +} + +async function writeArtifactToFile(hre: HardhatRuntimeEnvironment, contractName: string, artifactDir: string) { + try { + ensureDirectoryExists(artifactDir); + const artifact: Artifact = await hre.artifacts.readArtifact(contractName); + const artifactPath = path.join(artifactDir, `${contractName}.json`); + await writeFile(artifactPath, artifact); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Artifact generation failed: ${errorMessage}`); + } +} + +async function writeBuildInfoToFile(hre: HardhatRuntimeEnvironment, contractName: string, folderPath: string) { + ensureDirectoryExists(folderPath); + + const buildInfo: BuildInfo | undefined = await hre.artifacts.getBuildInfo(await findBuildInfoPath(hre, contractName)); + + if (!buildInfo) { + throw new Error(`Build info not found for ${contractName}`); + } + + const buildInfoPath = path.join(folderPath, `${contractName}.buildinfo`); + await writeFile(buildInfoPath, buildInfo); +} + +async function findBuildInfoPath(hre: HardhatRuntimeEnvironment, contractName: string): Promise { + const fullNames = await hre.artifacts.getAllFullyQualifiedNames(); + const contractPath = fullNames.find((name) => name.endsWith(`${contractName}.sol:${contractName}`)); + + if (!contractPath) { + throw new Error(`Contract ${contractName} not found in artifacts`); + } + + return contractPath; +} diff --git a/tasks/common/io.ts b/tasks/common/io.ts new file mode 100644 index 0000000..63111e4 --- /dev/null +++ b/tasks/common/io.ts @@ -0,0 +1,60 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import * as readline from 'node:readline'; +import { logger } from './logger'; + +// Add this helper function at the top level +export async function prompt(question: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer); + }); + }); +} + +export async function readFile(filePath: string): Promise { + try { + return await fs.readFile(filePath, 'utf8'); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`Failed to read file ${filePath}: ${errorMessage}`); + throw new Error(`File read failed: ${errorMessage}`); + } +} + +export async function writeFile(filePath: string, data: object): Promise { + try { + const relativePath = path.relative(process.cwd(), filePath); + await fs.writeFile(filePath, JSON.stringify(data, null, 2)); + logger.success(`Updated ${relativePath}`); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to write ${path.basename(filePath)}: ${errorMessage}`); + } +} + +export async function ensureDirectoryExists(dirPath: string): Promise { + try { + await fs.access(dirPath); + } catch { + logger.info(`Creating directory: ${dirPath}`); + await fs.mkdir(dirPath, { recursive: true }); + } +} + +export async function directoryExists(dirPath: string): Promise { + try { + await fs.access(dirPath); + return true; + } catch { + return false; + } +} + +export { path }; diff --git a/tasks/common/logger.ts b/tasks/common/logger.ts new file mode 100644 index 0000000..f0f6e3c --- /dev/null +++ b/tasks/common/logger.ts @@ -0,0 +1,50 @@ +import { CONFIG } from './config'; + +type LogLevel = 'info' | 'success' | 'error' | 'warn'; +type SectionType = 'config' | 'deploy' | 'files' | 'test' | 'verify' | 'default' | 'params' | 'meta'; + +class Logger { + private prefix?: string; + + constructor(prefix?: string) { + this.prefix = prefix; + } + + private log(level: LogLevel, message: string) { + const icon = CONFIG.LOGGER.ICONS[level]; + const prefix = this.prefix ? `[${this.prefix}] ` : ''; + console.log(`${icon} ${prefix}${message}`); + } + + section(message: string, type: SectionType = 'default'): void { + const icon = CONFIG.LOGGER.SECTION_ICONS[type]; + if (type === 'meta') { + const border = CONFIG.LOGGER.META_BORDER.repeat(40); + console.log(`\n${border}\n${icon} ${message.toUpperCase()}\n${border}`); + } else { + console.log(`\n${icon} ${message.toUpperCase()}\n${'-'.repeat(60)}`); + } + } + + info(message: string): void { + this.log('info', message); + } + + success(message: string): void { + this.log('success', message); + } + + error(message: string): void { + this.log('error', message); + } + + warn(message: string): void { + this.log('warn', message); + } + + withPrefix(prefix: string): Logger { + return new Logger(prefix); + } +} + +export const logger = new Logger(); diff --git a/tasks/common/params.ts b/tasks/common/params.ts new file mode 100644 index 0000000..662502c --- /dev/null +++ b/tasks/common/params.ts @@ -0,0 +1,37 @@ +import { readFile } from './io'; + +export async function readParams( + filePath: string, + requiredFields: string[], + objectPath: string[], +): Promise { + // Read and parse JSON file + let params: T; + try { + const fileContent = await readFile(filePath); + const parsedJson = JSON.parse(fileContent); + + // Navigate through nested object structure with better error handling + params = objectPath.reduce((obj, key) => { + if (obj === undefined || obj === null) { + throw new Error(`Invalid path: '${objectPath.join('.')}' - '${key}' not found`); + } + return obj[key]; + }, parsedJson); + + if (!params) { + throw new Error(`No data found at path '${objectPath.join('.')}'`); + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to read or parse params file: ${errorMessage}`); + } + + // Validate JSON structure + const missingFields = requiredFields.filter((field) => !(field in params)); + if (missingFields.length > 0) { + throw new Error(`Invalid params configuration: missing required fields: ${missingFields.join(', ')}`); + } + + return params as T; +} diff --git a/tasks/common/proxy.ts b/tasks/common/proxy.ts new file mode 100644 index 0000000..c1f8663 --- /dev/null +++ b/tasks/common/proxy.ts @@ -0,0 +1,23 @@ +import type { Signer } from 'ethers'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; + +export async function deployProxyContract( + hre: HardhatRuntimeEnvironment, + contractName: string, + constructorArgs: unknown[], + signer: Signer, +) { + const ContractFactory = await hre.ethers.getContractFactory(contractName, signer); + const contract = await hre.upgrades.deployProxy(ContractFactory, constructorArgs, { + initializer: 'initialize', + kind: 'uups', + }); + await contract.waitForDeployment(); + + const contractImplAddress = await hre.upgrades.erc1967.getImplementationAddress(await contract.getAddress()); + + return { + contract, + contractImplAddress, + }; +} diff --git a/tasks/deployAll.ts b/tasks/deployAll.ts new file mode 100644 index 0000000..310e25b --- /dev/null +++ b/tasks/deployAll.ts @@ -0,0 +1,26 @@ +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { logger } from './common/logger'; +import { deploySedaCore } from './deployCore'; +import { deploySecp256k1Prover } from './deployProver'; + +export async function deployAll( + hre: HardhatRuntimeEnvironment, + options: { + proverParams: string; + verify?: boolean; + }, +) { + // 1. Deploy Secp256k1Prover + logger.section('1. Deploy Secp256k1Prover contracts', 'meta'); + const { contractAddress } = await deploySecp256k1Prover(hre, { + params: options.proverParams, + verify: options.verify, + }); + + // 2. Deploy SedaCore using the prover address + logger.section('2. Deploy SedaCoreV1 contracts', 'meta'); + await deploySedaCore(hre, { + proverAddress: contractAddress, + verify: options.verify, + }); +} diff --git a/tasks/deployCore.ts b/tasks/deployCore.ts new file mode 100644 index 0000000..e76ae73 --- /dev/null +++ b/tasks/deployCore.ts @@ -0,0 +1,111 @@ +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { updateAddresses } from './common/addresses'; +import { CONFIG } from './common/config'; +import { updateDeployment } from './common/deployment'; +import { directoryExists } from './common/io'; +import { prompt } from './common/io'; +import { logger } from './common/logger'; +import { readParams } from './common/params'; +import { deployProxyContract } from './common/proxy'; + +interface SedaCoreV1Params { + sedaProverAddress: string; +} + +export async function deploySedaCore( + hre: HardhatRuntimeEnvironment, + options: { + params?: string; + proverAddress?: string; + verify?: boolean; + }, +) { + const { params, proverAddress, verify } = options; + const contractName = 'SedaCoreV1'; + + // Add Contract Parameters section + logger.section('Contract Parameters', 'params'); + // Check for conflicting parameters + if (params && proverAddress) { + throw new Error('Both params file and proverAddress cannot be provided simultaneously.'); + } + + // Validate parameters + let sedaProverAddress: string; + if (params) { + const sedaProverParams = await readParams(params, ['sedaProverAddress'], ['SedaCoreV1']); + sedaProverAddress = sedaProverParams.sedaProverAddress; + logger.info(`Using parameters file: ${params}`); + logger.info(`File content: \n ${JSON.stringify(sedaProverParams, null, 2).replace(/\n/g, '\n ')}`); + } else if (proverAddress) { + // Use the directly provided prover address + sedaProverAddress = proverAddress; + logger.info(`Using provided prover address parameter: ${sedaProverAddress}`); + } else { + // Try to read from deployments/addresses.json + try { + const networkKey = `${hre.network.name}-${hre.network.config.chainId}`; + const addressesPath = `../../${CONFIG.DEPLOYMENTS.FOLDER}/${CONFIG.DEPLOYMENTS.FILES.ADDRESSES}`; + const addresses = require(addressesPath); + const proverDeployment = addresses[networkKey]?.Secp256k1ProverV1; + + if (!proverDeployment?.proxy) { + throw new Error(`No Secp256k1ProverV1 proxy address found in ${CONFIG.DEPLOYMENTS.FILES.ADDRESSES}`); + } + + sedaProverAddress = proverDeployment.proxy; + logger.info(`Using prover address from deployments/addresses.json: ${sedaProverAddress}`); + } catch { + throw new Error('Either params file or proverAddress must be provided, or Secp256k1ProverV1 must be deployed'); + } + } + + // Configuration + logger.section('Deployment Configuration', 'config'); + logger.info(`Contract: ${contractName}`); + logger.info(`Network: ${hre.network.name}`); + logger.info(`Chain ID: ${hre.network.config.chainId}`); + const [owner] = await hre.ethers.getSigners(); + const balance = hre.ethers.formatEther(await owner.provider.getBalance(owner.address)); + logger.info(`Deployer: ${owner.address} (${balance} ETH)`); + + // Deploy + logger.section('Deploying Contracts', 'deploy'); + if (await directoryExists(CONFIG.DEPLOYMENTS.FOLDER)) { + const confirmation = await prompt('Deployments folder already exists. Type "yes" to continue: '); + if (confirmation !== 'yes') { + logger.error('Deployment aborted.'); + return; + } + } + + const { contract, contractImplAddress } = await deployProxyContract(hre, contractName, [sedaProverAddress], owner); + const contractAddress = await contract.getAddress(); + logger.success(`Proxy address: ${contractAddress}`); + logger.success(`Impl. address: ${contractImplAddress}`); + + // Update deployment files + logger.section('Updating Deployment Files', 'files'); + await updateDeployment(hre, contractName); + await updateAddresses(hre, contractName, contractAddress, contractImplAddress); + + if (verify) { + logger.section('Verifying Contracts', 'verify'); + try { + await hre.run('verify:verify', { + address: contractAddress, + }); + logger.success('Contract verified successfully'); + } catch (error) { + // Check if the error is "Already Verified" + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes('Already Verified')) { + logger.success('Contract is already verified on block explorer'); + } else { + logger.warn(`Verification failed: ${error}`); + } + } + } + + return { contract, contractAddress, contractImplAddress }; +} diff --git a/tasks/deployProver.ts b/tasks/deployProver.ts new file mode 100644 index 0000000..92c7f8d --- /dev/null +++ b/tasks/deployProver.ts @@ -0,0 +1,84 @@ +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { SedaDataTypes } from '../typechain-types/contracts/libraries/SedaDataTypes'; +import { updateAddresses } from './common/addresses'; +import { CONFIG } from './common/config'; +import { updateDeployment } from './common/deployment'; +import { directoryExists } from './common/io'; +import { prompt } from './common/io'; +import { logger } from './common/logger'; +import { readParams } from './common/params'; +import { deployProxyContract } from './common/proxy'; + +interface Secp256k1ProverV1Params { + initialBatch: SedaDataTypes.BatchStruct; +} + +export async function deploySecp256k1Prover( + hre: HardhatRuntimeEnvironment, + options: { + params: string; + verify?: boolean; + }, +): Promise<{ contractAddress: string; contractImplAddress: string }> { + const { params, verify } = options; + const contractName = 'Secp256k1ProverV1'; + + // Contract Parameters + logger.section('Contract Parameters', 'params'); + const proverParams = await readParams( + params, + ['batchHeight', 'blockHeight', 'validatorsRoot', 'resultsRoot', 'provingMetadata'], + ['Secp256k1ProverV1', 'initialBatch'], + ); + logger.info(`Using parameters file: ${params}`); + logger.info(`File Content: \n ${JSON.stringify(proverParams, null, 2).replace(/\n/g, '\n ')}`); + + // Configuration + logger.section('Deployment Configuration', 'config'); + logger.info(`Contract: ${contractName}`); + logger.info(`Network: ${hre.network.name}`); + logger.info(`Chain ID: ${hre.network.config.chainId}`); + const [owner] = await hre.ethers.getSigners(); + const balance = hre.ethers.formatEther(await owner.provider.getBalance(owner.address)); + logger.info(`Deployer: ${owner.address} (${balance} ETH)`); + + // Deploy + logger.section('Deploying Contracts', 'deploy'); + // If deployments folder exists, ask user to confirm by typing 'yes' + if (await directoryExists(CONFIG.DEPLOYMENTS.FOLDER)) { + const confirmation = await prompt('Deployments folder already exists. Type "yes" to continue: '); + if (confirmation !== 'yes') { + logger.error('Deployment aborted.'); + throw new Error('Deployment aborted: User cancelled the operation'); + } + } + const { contract, contractImplAddress } = await deployProxyContract(hre, contractName, [proverParams], owner); + const contractAddress = await contract.getAddress(); + logger.success(`Proxy address: ${contractAddress}`); + logger.success(`Impl. address: ${contractImplAddress}`); + + // Update deployment files + logger.section('Updating Deployment Files', 'files'); + await updateDeployment(hre, contractName); + await updateAddresses(hre, contractName, contractAddress, contractImplAddress); + + if (verify) { + logger.section('Verifying Contracts', 'verify'); + try { + await hre.run('verify:verify', { + address: contractAddress, + }); + logger.success('Contract verified successfully'); + } catch (error) { + // Check if the error is "Already Verified" + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes('Already Verified')) { + logger.info('Contract is already verified on block explorer'); + } else { + logger.warn(`Verification failed: ${error}`); + } + } + } + + return { contractAddress, contractImplAddress }; +} diff --git a/tasks/index.ts b/tasks/index.ts new file mode 100644 index 0000000..6499e6b --- /dev/null +++ b/tasks/index.ts @@ -0,0 +1,35 @@ +import { scope, types } from 'hardhat/config'; + +/** + * Defines the scope for SEDA-related tasks. + */ +export const sedaScope = scope('seda', 'Deploy and interact with SEDA contracts'); + +import { deployAll } from './deployAll'; +import { deploySedaCore } from './deployCore'; +import { deploySecp256k1Prover } from './deployProver'; + +sedaScope + .task('deploy:core', 'Deploy the SedaCoreV1 contract') + .addOptionalParam('params', 'The parameters file to use', undefined, types.string) + .addOptionalParam('proverAddress', 'Direct SedaProver contract address', undefined, types.string) + .addFlag('verify', 'Verify the contract on etherscan') + .setAction(async (taskArgs, hre) => { + await deploySedaCore(hre, taskArgs); + }); + +sedaScope + .task('deploy:prover', 'Deploy the Secp256k1ProverV1 contract') + .addParam('params', 'The parameters file to use', undefined, types.string) + .addFlag('verify', 'Verify the contract on etherscan') + .setAction(async (taskArgs, hre) => { + await deploySecp256k1Prover(hre, taskArgs); + }); + +sedaScope + .task('deploy:all', 'Deploy the Secp256k1ProverV1 and SedaCoreV1 contracts') + .addParam('params', 'The parameters file to use', undefined, types.string) + .addFlag('verify', 'Verify the contract on etherscan') + .setAction(async (taskArgs, hre) => { + await deployAll(hre, { proverParams: taskArgs.params, verify: taskArgs.verify }); + }); From c5efef1094b36ee724d42cdb134597ded23ce2a3 Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Fri, 6 Dec 2024 12:22:17 +0000 Subject: [PATCH 16/22] refactor(contracts): add ISedaCore interface --- contracts/core/SedaCoreV1.sol | 13 ++++++++----- contracts/core/abstract/ResultHandlerBase.sol | 7 ++++++- contracts/interfaces/IProver.sol | 4 ++++ contracts/interfaces/IRequestHandler.sol | 10 +++++----- contracts/interfaces/IResultHandler.sol | 11 ++++++----- contracts/interfaces/ISedaCore.sol | 16 ++++++++++++++++ contracts/provers/Secp256k1ProverV1.sol | 2 +- contracts/provers/abstract/ProverBase.sol | 3 +++ 8 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 contracts/interfaces/ISedaCore.sol diff --git a/contracts/core/SedaCoreV1.sol b/contracts/core/SedaCoreV1.sol index 81e84a3..0e92781 100644 --- a/contracts/core/SedaCoreV1.sol +++ b/contracts/core/SedaCoreV1.sol @@ -5,6 +5,9 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IRequestHandler} from "../interfaces/IRequestHandler.sol"; +import {IResultHandler} from "../interfaces/IResultHandler.sol"; +import {ISedaCore} from "../interfaces/ISedaCore.sol"; import {RequestHandlerBase} from "./abstract/RequestHandlerBase.sol"; import {ResultHandlerBase} from "./abstract/ResultHandlerBase.sol"; import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; @@ -12,13 +15,13 @@ import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; /// @title SedaCoreV1 /// @notice Core contract for the Seda protocol, managing requests and results /// @dev Implements ResultHandler and RequestHandler functionalities, and manages active requests -contract SedaCoreV1 is RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, OwnableUpgradeable { +contract SedaCoreV1 is ISedaCore, RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, OwnableUpgradeable { using EnumerableSet for EnumerableSet.Bytes32Set; // ============ Constants ============ // Constant storage slot for the state following the ERC-7201 standard - bytes32 private constant SEDA_CORE_V1_STORAGE_SLOT = + bytes32 private constant CORE_V1_STORAGE_SLOT = keccak256(abi.encode(uint256(keccak256("sedacore.storage.v1")) - 1)) & ~bytes32(uint256(0xff)); // ============ Storage ============ @@ -55,7 +58,7 @@ contract SedaCoreV1 is RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, O /// @dev Overrides the base implementation to also add the request ID to the pendingRequests array function postRequest( SedaDataTypes.RequestInputs calldata inputs - ) public override(RequestHandlerBase) returns (bytes32) { + ) public override(RequestHandlerBase, IRequestHandler) returns (bytes32) { bytes32 requestId = super.postRequest(inputs); _addRequest(requestId); @@ -68,7 +71,7 @@ contract SedaCoreV1 is RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, O SedaDataTypes.Result calldata result, uint64 batchHeight, bytes32[] calldata proof - ) public override(ResultHandlerBase) returns (bytes32) { + ) public override(ResultHandlerBase, IResultHandler) returns (bytes32) { bytes32 resultId = super.postResult(result, batchHeight, proof); _removeRequest(result.drId); @@ -105,7 +108,7 @@ contract SedaCoreV1 is RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, O /// @dev Uses ERC-7201 storage pattern to access the storage struct at a specific slot /// @return s The storage struct containing the contract's state variables function _storageV1() internal pure returns (SedaCoreStorage storage s) { - bytes32 slot = SEDA_CORE_V1_STORAGE_SLOT; + bytes32 slot = CORE_V1_STORAGE_SLOT; // solhint-disable-next-line no-inline-assembly assembly { s.slot := slot diff --git a/contracts/core/abstract/ResultHandlerBase.sol b/contracts/core/abstract/ResultHandlerBase.sol index ca01f13..e394d07 100644 --- a/contracts/core/abstract/ResultHandlerBase.sol +++ b/contracts/core/abstract/ResultHandlerBase.sol @@ -73,10 +73,15 @@ abstract contract ResultHandlerBase is IResultHandler, Initializable { if (bytes(result.version).length == 0) { revert ResultNotFound(requestId); } - return _resultHandlerStorage().results[requestId]; } + /// @notice Returns the address of the Seda prover contract + /// @return The address of the Seda prover contract + function getSedaProver() public view returns (address) { + return address(_resultHandlerStorage().sedaProver); + } + /// @notice Verifies the result without storing it /// @param result The result to verify /// @param batchHeight The height of the batch the result belongs to diff --git a/contracts/interfaces/IProver.sol b/contracts/interfaces/IProver.sol index 202d5b1..6adb8e7 100644 --- a/contracts/interfaces/IProver.sol +++ b/contracts/interfaces/IProver.sol @@ -8,6 +8,10 @@ import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; interface IProver { event BatchPosted(uint256 indexed batchHeight, bytes32 batchHash); + /// @notice Gets the height of the most recently posted batch + /// @return uint64 The height of the last batch, 0 if no batches exist + function getLastBatchHeight() external view returns (uint64); + /// @notice Posts a new batch with new data and validator proofs /// @param newBatch The new batch data to be posted /// @param signatures Array of signatures validating the new batch diff --git a/contracts/interfaces/IRequestHandler.sol b/contracts/interfaces/IRequestHandler.sol index 1aa28ee..f75afff 100644 --- a/contracts/interfaces/IRequestHandler.sol +++ b/contracts/interfaces/IRequestHandler.sol @@ -12,13 +12,13 @@ interface IRequestHandler { event RequestPosted(bytes32 indexed requestId); - /// @notice Allows users to post a new data request. - /// @param inputs The input parameters for the data request. - /// @return requestId The unique identifier for the posted request. - function postRequest(SedaDataTypes.RequestInputs calldata inputs) external returns (bytes32); - /// @notice Retrieves a stored data request by its unique identifier. /// @param id The unique identifier of the request to retrieve. /// @return request The details of the requested data. function getRequest(bytes32 id) external view returns (SedaDataTypes.Request memory); + + /// @notice Allows users to post a new data request. + /// @param inputs The input parameters for the data request. + /// @return requestId The unique identifier for the posted request. + function postRequest(SedaDataTypes.RequestInputs calldata inputs) external returns (bytes32); } diff --git a/contracts/interfaces/IResultHandler.sol b/contracts/interfaces/IResultHandler.sol index 74d7ae7..0bb7a7f 100644 --- a/contracts/interfaces/IResultHandler.sol +++ b/contracts/interfaces/IResultHandler.sol @@ -12,18 +12,19 @@ interface IResultHandler { event ResultPosted(bytes32 indexed resultId); + /// @notice Retrieves a result by its ID + /// @param requestId The unique identifier of the request + /// @return The result data associated with the given ID + function getResult(bytes32 requestId) external view returns (SedaDataTypes.Result memory); + /// @notice Posts a new result with a proof /// @param inputs The result data to be posted /// @param batchHeight The height of the batch the result belongs to /// @param proof The proof associated with the result + /// @return resultId The unique identifier of the posted result function postResult( SedaDataTypes.Result calldata inputs, uint64 batchHeight, bytes32[] memory proof ) external returns (bytes32); - - /// @notice Retrieves a result by its ID - /// @param requestId The unique identifier of the request - /// @return The result data associated with the given ID - function getResult(bytes32 requestId) external view returns (SedaDataTypes.Result memory); } diff --git a/contracts/interfaces/ISedaCore.sol b/contracts/interfaces/ISedaCore.sol new file mode 100644 index 0000000..f6c1c18 --- /dev/null +++ b/contracts/interfaces/ISedaCore.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IResultHandler} from "./IResultHandler.sol"; +import {IRequestHandler} from "./IRequestHandler.sol"; +import {SedaDataTypes} from "../libraries/SedaDataTypes.sol"; + +/// @title ISedaCoreV1 +/// @notice Interface for the main Seda protocol contract that handles both requests and results +interface ISedaCore is IResultHandler, IRequestHandler { + /// @notice Retrieves a paginated list of pending requests + /// @param offset The starting position in the list + /// @param limit The maximum number of requests to return + /// @return An array of Request structs + function getPendingRequests(uint256 offset, uint256 limit) external view returns (SedaDataTypes.Request[] memory); +} diff --git a/contracts/provers/Secp256k1ProverV1.sol b/contracts/provers/Secp256k1ProverV1.sol index ea4af57..7c9a307 100644 --- a/contracts/provers/Secp256k1ProverV1.sol +++ b/contracts/provers/Secp256k1ProverV1.sol @@ -137,7 +137,7 @@ contract Secp256k1ProverV1 is ProverBase, Initializable, UUPSUpgradeable, Ownabl /// @notice Returns the last processed batch height /// @return The height of the last batch - function getLastBatchHeight() public view returns (uint64) { + function getLastBatchHeight() public view override returns (uint64) { return _storageV1().lastBatchHeight; } diff --git a/contracts/provers/abstract/ProverBase.sol b/contracts/provers/abstract/ProverBase.sol index da677a3..133fa0c 100644 --- a/contracts/provers/abstract/ProverBase.sol +++ b/contracts/provers/abstract/ProverBase.sol @@ -29,4 +29,7 @@ abstract contract ProverBase is IProver { uint64 batchHeight, bytes32[] calldata merkleProof ) public view virtual override(IProver) returns (bool); + + /// @inheritdoc IProver + function getLastBatchHeight() external view virtual override(IProver) returns (uint64); } From 242dd3b96822b25b4a99b342688faa00a651f10b Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Fri, 6 Dec 2024 12:24:05 +0000 Subject: [PATCH 17/22] test(proxy): add seda core proxy tests --- contracts/mocks/MockSedaCoreV2.sol | 54 ++++++++++++ test/core/SedaCore.proxy.ts | 82 +++++++++++++++++++ test/prover/Secp256k1Prover.proxy.ts | 1 - ...over.test.ts => Secp256k1ProverV1.test.ts} | 2 +- 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 contracts/mocks/MockSedaCoreV2.sol create mode 100644 test/core/SedaCore.proxy.ts rename test/prover/{Secp256k1Prover.test.ts => Secp256k1ProverV1.test.ts} (99%) diff --git a/contracts/mocks/MockSedaCoreV2.sol b/contracts/mocks/MockSedaCoreV2.sol new file mode 100644 index 0000000..8c64e83 --- /dev/null +++ b/contracts/mocks/MockSedaCoreV2.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {SedaCoreV1} from "../core/SedaCoreV1.sol"; + +/// @title MockSedaCoreV2 +/// @notice Mock version of SedaCore for testing purposes +/// @dev This contract is a mock and should not be used in production +contract MockSedaCoreV2 is SedaCoreV1 { + // ============ Errors ============ + error ContractNotUpgradeable(); + + // ============ Constants ============ + bytes32 private constant CORE_V2_STORAGE_SLOT = + keccak256(abi.encode(uint256(keccak256("sedacore.storage.v2")) - 1)) & ~bytes32(uint256(0xff)); + + // ============ Storage ============ + /// @custom:storage-location sedacore.storage.v2 + struct V2Storage { + string version; + } + + // ============ Constructor & Initializer ============ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize() external reinitializer(2) onlyOwner { + V2Storage storage s = _storageV2(); + s.version = "2.0.0"; + } + + // ============ External Functions ============ + /// @notice Returns the version string from V2 storage + /// @return version The version string + function getVersion() external view returns (string memory) { + return _storageV2().version; + } + + // ============ Internal Functions ============ + function _storageV2() internal pure returns (V2Storage storage s) { + bytes32 slot = CORE_V2_STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + s.slot := slot + } + } + + // /// @dev Override the _authorizeUpgrade function + // function _authorizeUpgrade(address) internal virtual override onlyOwner { + // revert ContractNotUpgradeable(); + // } +} diff --git a/test/core/SedaCore.proxy.ts b/test/core/SedaCore.proxy.ts new file mode 100644 index 0000000..9fffce5 --- /dev/null +++ b/test/core/SedaCore.proxy.ts @@ -0,0 +1,82 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers'; +import { expect } from 'chai'; +import { ethers, upgrades } from 'hardhat'; + +describe('Proxy: SedaCore', () => { + async function deployProxyFixture() { + const [owner] = await ethers.getSigners(); + + const initialBatch = { + batchHeight: 0, + blockHeight: 0, + validatorsRoot: ethers.ZeroHash, + resultsRoot: ethers.ZeroHash, + provingMetadata: ethers.ZeroHash, + }; + + // Deploy prover + const ProverFactory = await ethers.getContractFactory('Secp256k1ProverV1'); + const prover = await upgrades.deployProxy(ProverFactory, [initialBatch], { initializer: 'initialize' }); + await prover.waitForDeployment(); + + // Deploy V1 through proxy + const CoreV1Factory = await ethers.getContractFactory('SedaCoreV1', owner); + const core = await upgrades.deployProxy(CoreV1Factory, [await prover.getAddress()], { initializer: 'initialize' }); + await core.waitForDeployment(); + + // Get V2 factory + const CoreV2Factory = await ethers.getContractFactory('MockSedaCoreV2', owner); + + return { prover, core, CoreV2Factory }; + } + + describe('upgrade', () => { + it('should maintain state after upgrade', async () => { + const { prover, core, CoreV2Factory } = await loadFixture(deployProxyFixture); + + // Check initial state (using a relevant state variable from your SedaCore) + const stateBeforeUpgrade = await core.getSedaProver(); + expect(stateBeforeUpgrade).to.equal(await prover.getAddress()); + + // Upgrade to V2 + const proxyV2 = await upgrades.upgradeProxy(await core.getAddress(), CoreV2Factory); + + // Check state is maintained + const stateAfterUpgrade = await proxyV2.getSedaProver(); + expect(stateAfterUpgrade).to.equal(stateBeforeUpgrade); + }); + + it('should maintain owner after upgrade', async () => { + const { core: proxy, CoreV2Factory } = await loadFixture(deployProxyFixture); + const [owner] = await ethers.getSigners(); + + // Check owner before upgrade + const ownerBeforeUpgrade = await proxy.owner(); + expect(ownerBeforeUpgrade).to.equal(owner.address); + + // Upgrade to V2 + const proxyV2 = await upgrades.upgradeProxy(await proxy.getAddress(), CoreV2Factory); + + // Check owner is maintained after upgrade + const ownerAfterUpgrade = await proxyV2.owner(); + expect(ownerAfterUpgrade).to.equal(owner.address); + }); + + it('should have new functionality after upgrade', async () => { + const { core: proxy, CoreV2Factory } = await loadFixture(deployProxyFixture); + + // Verify V1 doesn't have getVersion() + const V1Contract = proxy.connect(await ethers.provider.getSigner()); + // @ts-expect-error - getVersion shouldn't exist on V1 + expect(V1Contract.getVersion).to.be.undefined; + + // Upgrade to V2 + const proxyV2 = await upgrades.upgradeProxy(await proxy.getAddress(), CoreV2Factory); + await proxyV2.initialize(); + + // Check new V2 functionality + const version = await proxyV2.getVersion(); + expect(version).to.equal('2.0.0'); + }); + }); +}); diff --git a/test/prover/Secp256k1Prover.proxy.ts b/test/prover/Secp256k1Prover.proxy.ts index 48da4c0..e3835cc 100644 --- a/test/prover/Secp256k1Prover.proxy.ts +++ b/test/prover/Secp256k1Prover.proxy.ts @@ -1,7 +1,6 @@ import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers'; import { expect } from 'chai'; import { ethers, upgrades } from 'hardhat'; -import { generateDataFixtures } from '../utils'; describe('Proxy: Secp256k1Prover', () => { async function deployProxyFixture() { diff --git a/test/prover/Secp256k1Prover.test.ts b/test/prover/Secp256k1ProverV1.test.ts similarity index 99% rename from test/prover/Secp256k1Prover.test.ts rename to test/prover/Secp256k1ProverV1.test.ts index bd0b0fb..bb1217b 100644 --- a/test/prover/Secp256k1Prover.test.ts +++ b/test/prover/Secp256k1ProverV1.test.ts @@ -13,7 +13,7 @@ import { generateNewBatchWithId, } from '../utils'; -describe('Secp256k1Prover', () => { +describe('Secp256k1ProverV1', () => { async function deployProverFixture(length = 4) { const wallets = Array.from({ length }, (_, i) => { const seed = ethers.id(`validator${i}`); From ca64aa51e56f162644b989dee88a50a563251c41 Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Fri, 6 Dec 2024 12:59:26 +0000 Subject: [PATCH 18/22] chore(tasks): fix minor bug and restructure --- tasks/common/addresses.ts | 51 ---------------------- tasks/common/io.ts | 4 +- tasks/common/{deployment.ts => reports.ts} | 48 +++++++++++++++++++- tasks/common/{proxy.ts => uupsProxy.ts} | 0 tasks/deployCore.ts | 14 +++--- tasks/deployProver.ts | 13 +++--- tasks/index.ts | 2 + tasks/postRequest.ts | 37 ++++++++++++++++ 8 files changed, 101 insertions(+), 68 deletions(-) delete mode 100644 tasks/common/addresses.ts rename tasks/common/{deployment.ts => reports.ts} (63%) rename tasks/common/{proxy.ts => uupsProxy.ts} (100%) create mode 100644 tasks/postRequest.ts diff --git a/tasks/common/addresses.ts b/tasks/common/addresses.ts deleted file mode 100644 index 48d16d3..0000000 --- a/tasks/common/addresses.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import type { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { CONFIG } from './config'; -import { readFile, writeFile } from './io'; - -const DEPLOYMENTS_FOLDER = CONFIG.DEPLOYMENTS.FOLDER; -const ADDRESSES_FILE = CONFIG.DEPLOYMENTS.FILES.ADDRESSES; - -// Define the type for the addresses object -type Addresses = { - [networkName: string]: { - [contractName: string]: { - proxy: string; - implementation: string; - gitCommitHash: string; - }; - }; -}; - -export async function updateAddresses( - hre: HardhatRuntimeEnvironment, - contractName: string, - proxyAddress: string, - implAddress: string, -) { - const addressesPath = path.join(DEPLOYMENTS_FOLDER, ADDRESSES_FILE); - let addresses: Addresses = {}; - - if (fs.existsSync(addressesPath)) { - const content = await readFile(addressesPath); - if (content.trim()) { - addresses = JSON.parse(content) as Addresses; - } - } - - const networkName = `${hre.network.name}-${(await hre.ethers.provider.getNetwork()).chainId.toString()}`; - if (!addresses[networkName]) { - addresses[networkName] = {}; - } - - const gitCommitHash = require('node:child_process').execSync('git rev-parse HEAD').toString().trim(); - - addresses[networkName][contractName] = { - proxy: proxyAddress, - implementation: implAddress, - gitCommitHash, - }; - - await writeFile(addressesPath, addresses); -} diff --git a/tasks/common/io.ts b/tasks/common/io.ts index 63111e4..773b507 100644 --- a/tasks/common/io.ts +++ b/tasks/common/io.ts @@ -48,9 +48,9 @@ export async function ensureDirectoryExists(dirPath: string): Promise { } } -export async function directoryExists(dirPath: string): Promise { +export async function pathExists(path: string): Promise { try { - await fs.access(dirPath); + await fs.access(path); return true; } catch { return false; diff --git a/tasks/common/deployment.ts b/tasks/common/reports.ts similarity index 63% rename from tasks/common/deployment.ts rename to tasks/common/reports.ts index 11fee44..1a02290 100644 --- a/tasks/common/deployment.ts +++ b/tasks/common/reports.ts @@ -1,7 +1,53 @@ import type { Artifact, BuildInfo } from 'hardhat/types'; import type { HardhatRuntimeEnvironment } from 'hardhat/types'; import { CONFIG } from './config'; -import { path, ensureDirectoryExists, writeFile } from './io'; +import { path, ensureDirectoryExists, pathExists, readFile, writeFile } from './io'; + +const DEPLOYMENTS_FOLDER = CONFIG.DEPLOYMENTS.FOLDER; +const ADDRESSES_FILE = CONFIG.DEPLOYMENTS.FILES.ADDRESSES; + +// Define the type for the addresses object +type Addresses = { + [networkName: string]: { + [contractName: string]: { + proxy: string; + implementation: string; + gitCommitHash: string; + }; + }; +}; + +export async function updateAddressesFile( + hre: HardhatRuntimeEnvironment, + contractName: string, + proxyAddress: string, + implAddress: string, +) { + const addressesPath = path.join(DEPLOYMENTS_FOLDER, ADDRESSES_FILE); + let addresses: Addresses = {}; + + if (await pathExists(addressesPath)) { + const content = await readFile(addressesPath); + if (content.trim()) { + addresses = JSON.parse(content) as Addresses; + } + } + + const networkName = `${hre.network.name}-${(await hre.ethers.provider.getNetwork()).chainId.toString()}`; + if (!addresses[networkName]) { + addresses[networkName] = {}; + } + + const gitCommitHash = require('node:child_process').execSync('git rev-parse HEAD').toString().trim(); + + addresses[networkName][contractName] = { + proxy: proxyAddress, + implementation: implAddress, + gitCommitHash, + }; + + await writeFile(addressesPath, addresses); +} export async function updateDeployment(hre: HardhatRuntimeEnvironment, contractName: string) { const deploymentsDir = path.join(process.cwd(), CONFIG.DEPLOYMENTS.FOLDER); diff --git a/tasks/common/proxy.ts b/tasks/common/uupsProxy.ts similarity index 100% rename from tasks/common/proxy.ts rename to tasks/common/uupsProxy.ts diff --git a/tasks/deployCore.ts b/tasks/deployCore.ts index e76ae73..4fbf952 100644 --- a/tasks/deployCore.ts +++ b/tasks/deployCore.ts @@ -1,12 +1,11 @@ import type { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { updateAddresses } from './common/addresses'; import { CONFIG } from './common/config'; -import { updateDeployment } from './common/deployment'; -import { directoryExists } from './common/io'; +import { pathExists } from './common/io'; import { prompt } from './common/io'; import { logger } from './common/logger'; import { readParams } from './common/params'; -import { deployProxyContract } from './common/proxy'; +import { updateAddressesFile, updateDeployment } from './common/reports'; +import { deployProxyContract } from './common/uupsProxy'; interface SedaCoreV1Params { sedaProverAddress: string; @@ -71,8 +70,9 @@ export async function deploySedaCore( // Deploy logger.section('Deploying Contracts', 'deploy'); - if (await directoryExists(CONFIG.DEPLOYMENTS.FOLDER)) { - const confirmation = await prompt('Deployments folder already exists. Type "yes" to continue: '); + const networkKey = `${hre.network.name}-${hre.network.config.chainId}`; + if (await pathExists(`${CONFIG.DEPLOYMENTS.FOLDER}/${networkKey}`)) { + const confirmation = await prompt(`Deployments folder for ${networkKey} already exists. Type "yes" to continue: `); if (confirmation !== 'yes') { logger.error('Deployment aborted.'); return; @@ -87,7 +87,7 @@ export async function deploySedaCore( // Update deployment files logger.section('Updating Deployment Files', 'files'); await updateDeployment(hre, contractName); - await updateAddresses(hre, contractName, contractAddress, contractImplAddress); + await updateAddressesFile(hre, contractName, contractAddress, contractImplAddress); if (verify) { logger.section('Verifying Contracts', 'verify'); diff --git a/tasks/deployProver.ts b/tasks/deployProver.ts index 92c7f8d..01fe282 100644 --- a/tasks/deployProver.ts +++ b/tasks/deployProver.ts @@ -1,13 +1,12 @@ import type { HardhatRuntimeEnvironment } from 'hardhat/types'; import type { SedaDataTypes } from '../typechain-types/contracts/libraries/SedaDataTypes'; -import { updateAddresses } from './common/addresses'; import { CONFIG } from './common/config'; -import { updateDeployment } from './common/deployment'; -import { directoryExists } from './common/io'; +import { pathExists } from './common/io'; import { prompt } from './common/io'; import { logger } from './common/logger'; import { readParams } from './common/params'; -import { deployProxyContract } from './common/proxy'; +import { updateAddressesFile, updateDeployment } from './common/reports'; +import { deployProxyContract } from './common/uupsProxy'; interface Secp256k1ProverV1Params { initialBatch: SedaDataTypes.BatchStruct; @@ -44,8 +43,8 @@ export async function deploySecp256k1Prover( // Deploy logger.section('Deploying Contracts', 'deploy'); - // If deployments folder exists, ask user to confirm by typing 'yes' - if (await directoryExists(CONFIG.DEPLOYMENTS.FOLDER)) { + const networkKey = `${hre.network.name}-${hre.network.config.chainId}`; + if (await pathExists(`${CONFIG.DEPLOYMENTS.FOLDER}/${networkKey}`)) { const confirmation = await prompt('Deployments folder already exists. Type "yes" to continue: '); if (confirmation !== 'yes') { logger.error('Deployment aborted.'); @@ -60,7 +59,7 @@ export async function deploySecp256k1Prover( // Update deployment files logger.section('Updating Deployment Files', 'files'); await updateDeployment(hre, contractName); - await updateAddresses(hre, contractName, contractAddress, contractImplAddress); + await updateAddressesFile(hre, contractName, contractAddress, contractImplAddress); if (verify) { logger.section('Verifying Contracts', 'verify'); diff --git a/tasks/index.ts b/tasks/index.ts index 6499e6b..e7e1296 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -5,6 +5,8 @@ import { scope, types } from 'hardhat/config'; */ export const sedaScope = scope('seda', 'Deploy and interact with SEDA contracts'); +import './postRequest'; + import { deployAll } from './deployAll'; import { deploySedaCore } from './deployCore'; import { deploySecp256k1Prover } from './deployProver'; diff --git a/tasks/postRequest.ts b/tasks/postRequest.ts new file mode 100644 index 0000000..504e771 --- /dev/null +++ b/tasks/postRequest.ts @@ -0,0 +1,37 @@ +import { sedaScope } from '.'; +import { logger } from './common/logger'; + +sedaScope + .task('post-request', 'Posts a data request to the SedaProver contract') + .addParam('core', 'The address of the SedaCore contract') + .setAction(async (taskArgs, hre) => { + logger.section('Post Data Request', 'deploy'); + + const core = await hre.ethers.getContractAt('ISedaCore', taskArgs.core); + logger.info(`SedaCore address: ${taskArgs.core}`); + + const timestamp = Math.floor(Date.now() / 1000).toString(16); + const request = { + execProgramId: '0x541d1faf3b6e167ea5369928a24a0019f4167ca430da20a271c5a7bc5fa2657a', + execInputs: '0x1234', + execGasLimit: 100000n, + tallyProgramId: '0x541d1faf3b6e167ea5369928a24a0019f4167ca430da20a271c5a7bc5fa2657a', + tallyInputs: '0x5678', + tallyGasLimit: 100000n, + replicationFactor: 1, + consensusFilter: '0x00', + gasPrice: 1000n, + memo: `0x${timestamp}`, + }; + + logger.info(`Posting DR with memo: ${request.memo}`); + const tx = await core.postRequest(request); + const receipt = await tx.wait(); + logger.success('Data request posted successfully!'); + + // Get requestId from event logs + const logs = await core.queryFilter(core.filters.RequestPosted(), receipt?.blockNumber, receipt?.blockNumber); + const requestId = logs[0].args[0]; + + logger.info(`Data Request ID: ${requestId}`); + }); From b3d515ac9a34915a568f7897c1ff95435ff5a168 Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Fri, 6 Dec 2024 13:08:29 +0000 Subject: [PATCH 19/22] chore(tasks): add reset flag to replace existing deployments --- tasks/deployAll.ts | 7 +++++-- tasks/deployCore.ts | 3 ++- tasks/deployProver.ts | 3 ++- tasks/index.ts | 5 ++++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tasks/deployAll.ts b/tasks/deployAll.ts index 310e25b..1413d63 100644 --- a/tasks/deployAll.ts +++ b/tasks/deployAll.ts @@ -6,15 +6,17 @@ import { deploySecp256k1Prover } from './deployProver'; export async function deployAll( hre: HardhatRuntimeEnvironment, options: { - proverParams: string; + params: string; + reset?: boolean; verify?: boolean; }, ) { // 1. Deploy Secp256k1Prover logger.section('1. Deploy Secp256k1Prover contracts', 'meta'); const { contractAddress } = await deploySecp256k1Prover(hre, { - params: options.proverParams, + params: options.params, verify: options.verify, + reset: options.reset, }); // 2. Deploy SedaCore using the prover address @@ -22,5 +24,6 @@ export async function deployAll( await deploySedaCore(hre, { proverAddress: contractAddress, verify: options.verify, + reset: options.reset, }); } diff --git a/tasks/deployCore.ts b/tasks/deployCore.ts index 4fbf952..7a7e05e 100644 --- a/tasks/deployCore.ts +++ b/tasks/deployCore.ts @@ -16,6 +16,7 @@ export async function deploySedaCore( options: { params?: string; proverAddress?: string; + reset?: boolean; verify?: boolean; }, ) { @@ -71,7 +72,7 @@ export async function deploySedaCore( // Deploy logger.section('Deploying Contracts', 'deploy'); const networkKey = `${hre.network.name}-${hre.network.config.chainId}`; - if (await pathExists(`${CONFIG.DEPLOYMENTS.FOLDER}/${networkKey}`)) { + if (!options.reset && (await pathExists(`${CONFIG.DEPLOYMENTS.FOLDER}/${networkKey}`))) { const confirmation = await prompt(`Deployments folder for ${networkKey} already exists. Type "yes" to continue: `); if (confirmation !== 'yes') { logger.error('Deployment aborted.'); diff --git a/tasks/deployProver.ts b/tasks/deployProver.ts index 01fe282..9009a1c 100644 --- a/tasks/deployProver.ts +++ b/tasks/deployProver.ts @@ -16,6 +16,7 @@ export async function deploySecp256k1Prover( hre: HardhatRuntimeEnvironment, options: { params: string; + reset?: boolean; verify?: boolean; }, ): Promise<{ contractAddress: string; contractImplAddress: string }> { @@ -44,7 +45,7 @@ export async function deploySecp256k1Prover( // Deploy logger.section('Deploying Contracts', 'deploy'); const networkKey = `${hre.network.name}-${hre.network.config.chainId}`; - if (await pathExists(`${CONFIG.DEPLOYMENTS.FOLDER}/${networkKey}`)) { + if (!options.reset && (await pathExists(`${CONFIG.DEPLOYMENTS.FOLDER}/${networkKey}`))) { const confirmation = await prompt('Deployments folder already exists. Type "yes" to continue: '); if (confirmation !== 'yes') { logger.error('Deployment aborted.'); diff --git a/tasks/index.ts b/tasks/index.ts index e7e1296..cd37897 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -16,6 +16,7 @@ sedaScope .addOptionalParam('params', 'The parameters file to use', undefined, types.string) .addOptionalParam('proverAddress', 'Direct SedaProver contract address', undefined, types.string) .addFlag('verify', 'Verify the contract on etherscan') + .addFlag('reset', 'Replace existing deployment files') .setAction(async (taskArgs, hre) => { await deploySedaCore(hre, taskArgs); }); @@ -24,6 +25,7 @@ sedaScope .task('deploy:prover', 'Deploy the Secp256k1ProverV1 contract') .addParam('params', 'The parameters file to use', undefined, types.string) .addFlag('verify', 'Verify the contract on etherscan') + .addFlag('reset', 'Replace existing deployment files') .setAction(async (taskArgs, hre) => { await deploySecp256k1Prover(hre, taskArgs); }); @@ -32,6 +34,7 @@ sedaScope .task('deploy:all', 'Deploy the Secp256k1ProverV1 and SedaCoreV1 contracts') .addParam('params', 'The parameters file to use', undefined, types.string) .addFlag('verify', 'Verify the contract on etherscan') + .addFlag('reset', 'Replace existing deployment files') .setAction(async (taskArgs, hre) => { - await deployAll(hre, { proverParams: taskArgs.params, verify: taskArgs.verify }); + await deployAll(hre, taskArgs); }); From 11734008623db777e5a8172f08efe6e2d6d5bca5 Mon Sep 17 00:00:00 2001 From: Thomas van Dam Date: Fri, 6 Dec 2024 16:31:05 +0100 Subject: [PATCH 20/22] chore(tasks): simplify parameter file validation Also make the deployProxyContract task typed, so it's more explicit about the contracts we're supporting and what constructor arguments they need. --- bun.lockb | Bin 280060 -> 280427 bytes package.json | 3 +- tasks/common/params.ts | 50 +++++++++++++------------- tasks/common/uupsProxy.ts | 16 +++++++-- tasks/deployCore.ts | 8 ++--- tasks/deployProver.ts | 22 +++++------- test/helpers.ts | 11 +++--- test/prover/Secp256k1ProverV1.test.ts | 10 ++++-- test/utils.ts | 17 +++++---- ts-types/index.ts | 5 +++ 10 files changed, 75 insertions(+), 67 deletions(-) create mode 100644 ts-types/index.ts diff --git a/bun.lockb b/bun.lockb index f48a9295a4b5b9d270e4e0706d6e7525f25b179e..b2e84428bc58a81c118051f19b7f4e7f798915fa 100755 GIT binary patch delta 52852 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 n(YE}%Sn+^P9FVDSvjD61z@kh=_a`Xuv=hp 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 IbaD6p0KRcM+5i9m delta 52439 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 diff --git a/package.json b/package.json index ce3f960..649f198 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "prettier": "^3.4.2", "prettier-plugin-solidity": "^1.4.1", "rimraf": "^6.0.1", - "solhint": "^5.0.3" + "solhint": "^5.0.3", + "valibot": "^0.42.1" }, "dependencies": { "@openzeppelin/contracts": "5.1.0", diff --git a/tasks/common/params.ts b/tasks/common/params.ts index 662502c..80a6eb1 100644 --- a/tasks/common/params.ts +++ b/tasks/common/params.ts @@ -1,37 +1,35 @@ +import * as v from 'valibot'; import { readFile } from './io'; -export async function readParams( - filePath: string, - requiredFields: string[], - objectPath: string[], -): Promise { - // Read and parse JSON file - let params: T; +const HexString = v.pipe(v.string(), v.regex(/^0x[0-9a-fA-F]*$/, 'Invalid hex string')); + +const ParamsSchema = v.object({ + SedaCoreV1: v.object({ + sedaProverAddress: HexString, + }), + Secp256k1ProverV1: v.object({ + initialBatch: v.object({ + batchHeight: v.number(), + blockHeight: v.number(), + validatorsRoot: HexString, + resultsRoot: HexString, + provingMetadata: HexString, + }), + }), +}); + +export async function readParams(filePath: string): Promise> { try { const fileContent = await readFile(filePath); const parsedJson = JSON.parse(fileContent); - // Navigate through nested object structure with better error handling - params = objectPath.reduce((obj, key) => { - if (obj === undefined || obj === null) { - throw new Error(`Invalid path: '${objectPath.join('.')}' - '${key}' not found`); - } - return obj[key]; - }, parsedJson); - - if (!params) { - throw new Error(`No data found at path '${objectPath.join('.')}'`); - } + return v.parse(ParamsSchema, parsedJson); } catch (error: unknown) { + if (error instanceof v.ValiError) { + throw new Error(`Failed to read or parse params file: ${v.flatten(error.issues)}`); + } + const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to read or parse params file: ${errorMessage}`); } - - // Validate JSON structure - const missingFields = requiredFields.filter((field) => !(field in params)); - if (missingFields.length > 0) { - throw new Error(`Invalid params configuration: missing required fields: ${missingFields.join(', ')}`); - } - - return params as T; } diff --git a/tasks/common/uupsProxy.ts b/tasks/common/uupsProxy.ts index c1f8663..e0b4302 100644 --- a/tasks/common/uupsProxy.ts +++ b/tasks/common/uupsProxy.ts @@ -1,10 +1,20 @@ import type { Signer } from 'ethers'; import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { ProverDataTypes } from '../../ts-types'; -export async function deployProxyContract( +type Contracts = { + Secp256k1ProverV1: { + constructorArgs: [ProverDataTypes.BatchStruct]; + }; + SedaCoreV1: { + constructorArgs: [string]; + }; +}; + +export async function deployProxyContract( hre: HardhatRuntimeEnvironment, - contractName: string, - constructorArgs: unknown[], + contractName: T, + constructorArgs: Contracts[T]['constructorArgs'], signer: Signer, ) { const ContractFactory = await hre.ethers.getContractFactory(contractName, signer); diff --git a/tasks/deployCore.ts b/tasks/deployCore.ts index 7a7e05e..be40568 100644 --- a/tasks/deployCore.ts +++ b/tasks/deployCore.ts @@ -7,10 +7,6 @@ import { readParams } from './common/params'; import { updateAddressesFile, updateDeployment } from './common/reports'; import { deployProxyContract } from './common/uupsProxy'; -interface SedaCoreV1Params { - sedaProverAddress: string; -} - export async function deploySedaCore( hre: HardhatRuntimeEnvironment, options: { @@ -33,9 +29,9 @@ export async function deploySedaCore( // Validate parameters let sedaProverAddress: string; if (params) { - const sedaProverParams = await readParams(params, ['sedaProverAddress'], ['SedaCoreV1']); - sedaProverAddress = sedaProverParams.sedaProverAddress; logger.info(`Using parameters file: ${params}`); + const sedaProverParams = await readParams(params); + sedaProverAddress = sedaProverParams.SedaCoreV1.sedaProverAddress; logger.info(`File content: \n ${JSON.stringify(sedaProverParams, null, 2).replace(/\n/g, '\n ')}`); } else if (proverAddress) { // Use the directly provided prover address diff --git a/tasks/deployProver.ts b/tasks/deployProver.ts index 9009a1c..2cd39cf 100644 --- a/tasks/deployProver.ts +++ b/tasks/deployProver.ts @@ -1,5 +1,4 @@ import type { HardhatRuntimeEnvironment } from 'hardhat/types'; -import type { SedaDataTypes } from '../typechain-types/contracts/libraries/SedaDataTypes'; import { CONFIG } from './common/config'; import { pathExists } from './common/io'; import { prompt } from './common/io'; @@ -8,10 +7,6 @@ import { readParams } from './common/params'; import { updateAddressesFile, updateDeployment } from './common/reports'; import { deployProxyContract } from './common/uupsProxy'; -interface Secp256k1ProverV1Params { - initialBatch: SedaDataTypes.BatchStruct; -} - export async function deploySecp256k1Prover( hre: HardhatRuntimeEnvironment, options: { @@ -20,17 +15,13 @@ export async function deploySecp256k1Prover( verify?: boolean; }, ): Promise<{ contractAddress: string; contractImplAddress: string }> { - const { params, verify } = options; + const { params: paramsFilePath, verify } = options; const contractName = 'Secp256k1ProverV1'; // Contract Parameters logger.section('Contract Parameters', 'params'); - const proverParams = await readParams( - params, - ['batchHeight', 'blockHeight', 'validatorsRoot', 'resultsRoot', 'provingMetadata'], - ['Secp256k1ProverV1', 'initialBatch'], - ); - logger.info(`Using parameters file: ${params}`); + logger.info(`Using parameters file: ${paramsFilePath}`); + const proverParams = await readParams(paramsFilePath); logger.info(`File Content: \n ${JSON.stringify(proverParams, null, 2).replace(/\n/g, '\n ')}`); // Configuration @@ -52,7 +43,12 @@ export async function deploySecp256k1Prover( throw new Error('Deployment aborted: User cancelled the operation'); } } - const { contract, contractImplAddress } = await deployProxyContract(hre, contractName, [proverParams], owner); + const { contract, contractImplAddress } = await deployProxyContract( + hre, + contractName, + [proverParams.Secp256k1ProverV1.initialBatch], + owner, + ); const contractAddress = await contract.getAddress(); logger.success(`Proxy address: ${contractAddress}`); logger.success(`Impl. address: ${contractImplAddress}`); diff --git a/test/helpers.ts b/test/helpers.ts index 1870bda..db56584 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,12 +1,11 @@ import { expect } from 'chai'; - -import type { SedaDataTypes } from '../typechain-types/contracts/libraries/SedaDataTypes'; +import type { CoreRequestTypes, CoreResultTypes, ProverDataTypes } from '../ts-types'; // Function to convert an unformatted tuple result to a formatted struct export function convertToRequestInputs( // biome-ignore lint/suspicious/noExplicitAny: Explicit any type is necessary to handle the unformatted tuple result request: any, -): SedaDataTypes.RequestInputsStruct { +): CoreRequestTypes.RequestInputsStruct { return { //version: unformatted[0], execProgramId: request[1], @@ -24,8 +23,8 @@ export function convertToRequestInputs( // Helper function to compare two requests export const compareRequests = ( - actual: SedaDataTypes.RequestInputsStruct, - expected: SedaDataTypes.RequestInputsStruct, + actual: CoreRequestTypes.RequestInputsStruct, + expected: CoreRequestTypes.RequestInputsStruct, ) => { expect(actual.execProgramId).to.equal(expected.execProgramId); expect(actual.execInputs).to.equal(expected.execInputs); @@ -40,7 +39,7 @@ export const compareRequests = ( }; // Helper function to compare two results -export const compareResults = (actual: SedaDataTypes.ResultStruct, expected: SedaDataTypes.ResultStruct) => { +export const compareResults = (actual: CoreResultTypes.ResultStruct, expected: CoreResultTypes.ResultStruct) => { expect(actual.version).to.equal(expected.version); expect(actual.drId).to.equal(expected.drId); expect(actual.consensus).to.equal(expected.consensus); diff --git a/test/prover/Secp256k1ProverV1.test.ts b/test/prover/Secp256k1ProverV1.test.ts index bb1217b..ef6a60f 100644 --- a/test/prover/Secp256k1ProverV1.test.ts +++ b/test/prover/Secp256k1ProverV1.test.ts @@ -3,7 +3,7 @@ import { SimpleMerkleTree } from '@openzeppelin/merkle-tree'; import { expect } from 'chai'; import type { Wallet } from 'ethers'; import { ethers, upgrades } from 'hardhat'; -import type { SedaDataTypes } from '../../typechain-types/contracts/libraries'; +import type { ProverDataTypes } from '../../ts-types'; import { computeResultLeafHash, computeValidatorLeafHash, @@ -81,7 +81,11 @@ describe('Secp256k1ProverV1', () => { } // Add a helper function to generate and sign a new batch - async function generateAndSignBatch(wallets: Wallet[], initialBatch: SedaDataTypes.BatchStruct, signerIndices = [0]) { + async function generateAndSignBatch( + wallets: Wallet[], + initialBatch: ProverDataTypes.BatchStruct, + signerIndices = [0], + ) { const { newBatchId, newBatch } = generateNewBatchWithId(initialBatch); const signatures = await Promise.all(signerIndices.map((i) => wallets[i].signingKey.sign(newBatchId).serialized)); return { newBatchId, newBatch, signatures }; @@ -347,7 +351,7 @@ describe('Secp256k1ProverV1', () => { describe('batch id', () => { it('should generate the correct batch id for test vectors', async () => { - const testBatch: SedaDataTypes.BatchStruct = { + const testBatch: ProverDataTypes.BatchStruct = { batchHeight: 4, blockHeight: 134, resultsRoot: '0x49918c4e986fff80aeb3532466132920d2ffd8db2a9615e8d02dd0f02e19503a', diff --git a/test/utils.ts b/test/utils.ts index 16d6bae..3c79a2f 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,6 +1,5 @@ import { ethers } from 'hardhat'; - -import type { SedaDataTypes } from '../typechain-types/contracts/libraries/SedaDataTypes'; +import type { CoreRequestTypes, CoreResultTypes, ProverDataTypes } from '../ts-types'; export const SEDA_DATA_TYPES_VERSION = '0.0.1'; @@ -11,8 +10,8 @@ function padBigIntToBytes(value: bigint, byteLength: number): string { return ethers.zeroPadValue(ethers.toBeArray(value), byteLength); } -export function generateNewBatchWithId(initialBatch: SedaDataTypes.BatchStruct) { - const newBatch: SedaDataTypes.BatchStruct = { +export function generateNewBatchWithId(initialBatch: ProverDataTypes.BatchStruct) { + const newBatch: ProverDataTypes.BatchStruct = { ...initialBatch, batchHeight: BigInt(initialBatch.batchHeight) + BigInt(1), blockHeight: BigInt(initialBatch.blockHeight) + BigInt(1), @@ -22,7 +21,7 @@ export function generateNewBatchWithId(initialBatch: SedaDataTypes.BatchStruct) return { newBatchId, newBatch }; } -export function deriveBatchId(batch: SedaDataTypes.BatchStruct): string { +export function deriveBatchId(batch: ProverDataTypes.BatchStruct): string { return ethers.keccak256( ethers.concat([ padBigIntToBytes(BigInt(batch.batchHeight), 8), @@ -34,7 +33,7 @@ export function deriveBatchId(batch: SedaDataTypes.BatchStruct): string { ); } -export function deriveRequestId(request: SedaDataTypes.RequestInputsStruct): string { +export function deriveRequestId(request: CoreRequestTypes.RequestInputsStruct): string { return ethers.keccak256( ethers.concat([ ethers.keccak256(ethers.toUtf8Bytes(SEDA_DATA_TYPES_VERSION)), @@ -52,7 +51,7 @@ export function deriveRequestId(request: SedaDataTypes.RequestInputsStruct): str ); } -export function deriveDataResultId(dataResult: SedaDataTypes.ResultStruct): string { +export function deriveDataResultId(dataResult: CoreResultTypes.ResultStruct): string { return ethers.keccak256( ethers.concat([ ethers.keccak256(ethers.toUtf8Bytes(SEDA_DATA_TYPES_VERSION)), @@ -81,8 +80,8 @@ export function computeValidatorLeafHash(validator: string, votingPower: number) } export function generateDataFixtures(length: number): { - requests: SedaDataTypes.RequestInputsStruct[]; - results: SedaDataTypes.ResultStruct[]; + requests: CoreRequestTypes.RequestInputsStruct[]; + results: CoreResultTypes.ResultStruct[]; } { const requests = Array.from({ length }, (_, i) => ({ execProgramId: ethers.ZeroHash, diff --git a/ts-types/index.ts b/ts-types/index.ts new file mode 100644 index 0000000..3b5584f --- /dev/null +++ b/ts-types/index.ts @@ -0,0 +1,5 @@ +import type { SedaDataTypes as CoreRequestTypes } from '../typechain-types/contracts/core/abstract/RequestHandlerBase'; +import type { SedaDataTypes as CoreResultTypes } from '../typechain-types/contracts/core/abstract/ResultHandlerBase'; +import type { SedaDataTypes as ProverDataTypes } from '../typechain-types/contracts/provers/abstract/ProverBase'; + +export type { ProverDataTypes, CoreRequestTypes, CoreResultTypes }; From a50aedaf6ce8f3327733679e49691d12c9b4d4e3 Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Fri, 6 Dec 2024 22:35:11 +0000 Subject: [PATCH 21/22] refactor(permissioned): use getRequest from RequestHandlerBase --- contracts/core/SedaCorePermissioned.sol | 42 ++++--------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/contracts/core/SedaCorePermissioned.sol b/contracts/core/SedaCorePermissioned.sol index d5f5034..df24297 100644 --- a/contracts/core/SedaCorePermissioned.sol +++ b/contracts/core/SedaCorePermissioned.sol @@ -70,20 +70,6 @@ contract SedaCorePermissioned is RequestHandlerBase, IResultHandler, AccessContr return resultId; } - /// @notice Retrieves a stored request - /// @param requestId The ID of the request to retrieve - /// @return The requested data - function getRequest( - bytes32 requestId - ) external view override(RequestHandlerBase) returns (SedaDataTypes.Request memory) { - SedaDataTypes.Request memory request = requests[requestId]; - if (bytes(request.version).length == 0) { - revert RequestNotFound(requestId); - } - - return requests[requestId]; - } - /// @notice Retrieves a result by its ID /// @param requestId The unique identifier of the result /// @return The result data associated with the given ID @@ -103,33 +89,17 @@ contract SedaCorePermissioned is RequestHandlerBase, IResultHandler, AccessContr function postRequest( SedaDataTypes.RequestInputs calldata inputs ) public override(RequestHandlerBase) whenNotPaused returns (bytes32) { - uint16 replicationFactor = inputs.replicationFactor; - if (replicationFactor > maxReplicationFactor || replicationFactor == 0) { + // Check max replication factor first + if (inputs.replicationFactor > maxReplicationFactor) { revert InvalidReplicationFactor(); } - bytes32 requestId = SedaDataTypes.deriveRequestId(inputs); - if (bytes(requests[requestId].version).length != 0) { - revert RequestAlreadyExists(requestId); - } - - requests[requestId] = SedaDataTypes.Request({ - version: SedaDataTypes.VERSION, - execProgramId: inputs.execProgramId, - execInputs: inputs.execInputs, - execGasLimit: inputs.execGasLimit, - tallyProgramId: inputs.tallyProgramId, - tallyInputs: inputs.tallyInputs, - tallyGasLimit: inputs.tallyGasLimit, - replicationFactor: inputs.replicationFactor, - consensusFilter: inputs.consensusFilter, - gasPrice: inputs.gasPrice, - memo: inputs.memo - }); + // Call parent implementation which handles the rest + bytes32 requestId = super.postRequest(inputs); + // Add to pending requests (unique to this implementation) _addPendingRequest(requestId); - emit RequestPosted(requestId); return requestId; } @@ -147,7 +117,7 @@ contract SedaCorePermissioned is RequestHandlerBase, IResultHandler, AccessContr SedaDataTypes.Request[] memory queriedPendingRequests = new SedaDataTypes.Request[](actualLimit); for (uint256 i = 0; i < actualLimit; i++) { bytes32 requestId = pendingRequests.at(offset + i); - queriedPendingRequests[i] = requests[requestId]; + queriedPendingRequests[i] = getRequest(requestId); } return queriedPendingRequests; From f816df59be50973503fac81271b2794bf6078b1a Mon Sep 17 00:00:00 2001 From: Mario Cao Date: Fri, 6 Dec 2024 22:38:49 +0000 Subject: [PATCH 22/22] feat(core): add storage slot to RequestHandlerBase --- contracts/core/SedaCoreV1.sol | 2 +- .../core/abstract/RequestHandlerBase.sol | 40 +++++++++++++++---- contracts/core/abstract/ResultHandlerBase.sol | 3 -- test/core/SedaCoreV1.test.ts | 3 +- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/contracts/core/SedaCoreV1.sol b/contracts/core/SedaCoreV1.sol index 0e92781..f2739dc 100644 --- a/contracts/core/SedaCoreV1.sol +++ b/contracts/core/SedaCoreV1.sol @@ -96,7 +96,7 @@ contract SedaCoreV1 is ISedaCore, RequestHandlerBase, ResultHandlerBase, UUPSUpg SedaDataTypes.Request[] memory queriedPendingRequests = new SedaDataTypes.Request[](actualLimit); for (uint256 i = 0; i < actualLimit; i++) { bytes32 requestId = _storageV1().pendingRequests.at(offset + i); - queriedPendingRequests[i] = requests[requestId]; + queriedPendingRequests[i] = getRequest(requestId); } return queriedPendingRequests; diff --git a/contracts/core/abstract/RequestHandlerBase.sol b/contracts/core/abstract/RequestHandlerBase.sol index 5300f1b..5bd1adc 100644 --- a/contracts/core/abstract/RequestHandlerBase.sol +++ b/contracts/core/abstract/RequestHandlerBase.sol @@ -7,8 +7,21 @@ import {IRequestHandler} from "../../interfaces/IRequestHandler.sol"; /// @title RequestHandler /// @notice Implements the RequestHandlerBase for managing Seda protocol requests abstract contract RequestHandlerBase is IRequestHandler { - // Mapping of request IDs to Request structs - mapping(bytes32 => SedaDataTypes.Request) public requests; + // ============ Constants ============ + + // Define a unique storage slot for RequestHandlerBase + bytes32 private constant REQUEST_HANDLER_STORAGE_SLOT = + keccak256(abi.encode(uint256(keccak256("seda.requesthandler.storage")) - 1)) & ~bytes32(uint256(0xff)); + + // ============ Storage ============ + + /// @custom:storage-location erc7201:seda.requesthandler.storage + struct RequestHandlerStorage { + // Mapping of request IDs to Request structs + mapping(bytes32 => SedaDataTypes.Request) requests; + } + + // ============ External Functions ============ /// @inheritdoc IRequestHandler function postRequest( @@ -19,11 +32,11 @@ abstract contract RequestHandlerBase is IRequestHandler { } bytes32 requestId = SedaDataTypes.deriveRequestId(inputs); - if (bytes(requests[requestId].version).length != 0) { + if (bytes(_requestHandlerStorage().requests[requestId].version).length != 0) { revert RequestAlreadyExists(requestId); } - requests[requestId] = SedaDataTypes.Request({ + _requestHandlerStorage().requests[requestId] = SedaDataTypes.Request({ version: SedaDataTypes.VERSION, execProgramId: inputs.execProgramId, execInputs: inputs.execInputs, @@ -44,14 +57,14 @@ abstract contract RequestHandlerBase is IRequestHandler { /// @inheritdoc IRequestHandler function getRequest( bytes32 requestId - ) external view virtual override(IRequestHandler) returns (SedaDataTypes.Request memory) { - SedaDataTypes.Request memory request = requests[requestId]; + ) public view virtual override(IRequestHandler) returns (SedaDataTypes.Request memory) { + SedaDataTypes.Request memory request = _requestHandlerStorage().requests[requestId]; // Version field is always set if (bytes(request.version).length == 0) { revert RequestNotFound(requestId); } - return requests[requestId]; + return _requestHandlerStorage().requests[requestId]; } /// @notice Derives a request ID from the given inputs @@ -60,4 +73,17 @@ abstract contract RequestHandlerBase is IRequestHandler { function deriveRequestId(SedaDataTypes.RequestInputs calldata inputs) public pure returns (bytes32) { return SedaDataTypes.deriveRequestId(inputs); } + + // ============ Internal Functions ============ + + /// @notice Returns the storage struct for the contract + /// @dev Uses ERC-7201 storage pattern to access the storage struct at a specific slot + /// @return s The storage struct containing the contract's state variables + function _requestHandlerStorage() internal pure returns (RequestHandlerStorage storage s) { + bytes32 slot = REQUEST_HANDLER_STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + s.slot := slot + } + } } diff --git a/contracts/core/abstract/ResultHandlerBase.sol b/contracts/core/abstract/ResultHandlerBase.sol index e394d07..f9e9c5e 100644 --- a/contracts/core/abstract/ResultHandlerBase.sol +++ b/contracts/core/abstract/ResultHandlerBase.sol @@ -10,9 +10,6 @@ import {IResultHandler} from "../../interfaces/IResultHandler.sol"; /// @title ResultHandler /// @notice Implements the ResultHandlerBase for managing Seda protocol results abstract contract ResultHandlerBase is IResultHandler, Initializable { - // ============ Errors ============ - // Note: Errors are defined in IResultHandler interface - // ============ Constants ============ // Define a unique storage slot for ResultHandlerBase diff --git a/test/core/SedaCoreV1.test.ts b/test/core/SedaCoreV1.test.ts index 8e48018..8442eee 100644 --- a/test/core/SedaCoreV1.test.ts +++ b/test/core/SedaCoreV1.test.ts @@ -80,6 +80,7 @@ describe('SedaCoreV1', () => { const requests = await core.getPendingRequests(0, 1); expect(requests.length).to.equal(0); }); + it('should post a request and then post its result', async () => { const { core, data } = await loadFixture(deployCoreFixture); @@ -178,7 +179,7 @@ describe('SedaCoreV1', () => { const gasUsed = await core.postResult.estimateGas(data.results[2], 0, data.proofs[2]); // This is rough esimate - expect(gasUsed).to.be.lessThan(250000); + expect(gasUsed).to.be.lessThan(300000); }); it('should maintain pending requests (with removals)', async () => {