diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..337b9c9c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crates/enclave-contract/lib/forge-std"] + path = crates/enclave-contract/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/crates/enclave-contract/contracts/MultisigUpgradeOperator.sol b/crates/enclave-contract/contracts/MultisigUpgradeOperator.sol index dba1f0d3..cd0f36bf 100644 --- a/crates/enclave-contract/contracts/MultisigUpgradeOperator.sol +++ b/crates/enclave-contract/contracts/MultisigUpgradeOperator.sol @@ -9,181 +9,330 @@ import "./UpgradeOperator.sol"; * Uses the ANVIL test keys as the three signers */ contract MultisigUpgradeOperator { - // The three signers (ANVIL keys) - address public constant signer1 = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; // Alice (0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80) - address public constant signer2 = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; // Bob (0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d) - address public constant signer3 = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; // Charlie (0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a) - + address public constant signer1 = + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; // Alice (0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80) + address public constant signer2 = + 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; // Bob (0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d) + address public constant signer3 = + 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; // Charlie (0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a) + // The UpgradeOperator contract being controlled - UpgradeOperator constant public upgradeOperator = UpgradeOperator(0x1000000000000000000000000000000000000001); // Set in seismic-reth genesis - + UpgradeOperator public constant upgradeOperator = + UpgradeOperator(0x1000000000000000000000000000000000000001); // Set in seismic-reth genesis + // Nonce counter for proposal uniqueness uint256 public proposalNonce; - - // Mapping to track votes for each proposal - mapping(bytes32 => mapping(address => bool)) public votes; - - // Mapping to track proposal execution status - mapping(bytes32 => bool) public executed; - - // Event emitted when a proposal is created (version 1) - event ProposalCreatedV1(bytes32 indexed proposalId, uint256 nonce, bytes mrtd, bytes mrseam, bytes pcr4, bool status); - - // Event emitted when a vote is cast - event VoteCast(bytes32 indexed proposalId, address indexed voter, bool approved); - - // Event emitted when a proposal is executed - event ProposalExecuted(bytes32 indexed proposalId); - - // Event emitted when upgrade operator is set - event UpgradeOperatorSet(address indexed upgradeOperator); - + + // Enum for proposal types + enum ProposalType { + ADD_MEASUREMENTS, + DEPRECATE_MEASUREMENTS, + REINSTATE_MEASUREMENTS + } + + // Proposal struct + struct Proposal { + ProposalType proposalType; + bytes32 tagHash; + UpgradeOperator.Measurements measurements; + bool executed; + uint256 voteCount; + mapping(address => bool) hasVoted; + } + + // Mapping to track proposals + mapping(bytes32 => Proposal) public proposals; + + // Events + event ProposalCreated( + bytes32 indexed proposalId, + ProposalType indexed proposalType, + string tag, + uint256 nonce + ); + + event VoteCast(bytes32 indexed proposalId, address indexed voter); + + event ProposalExecuted( + bytes32 indexed proposalId, + ProposalType indexed proposalType + ); + + modifier onlySigner() { + require( + msg.sender == signer1 || + msg.sender == signer2 || + msg.sender == signer3, + "Not authorized" + ); + _; + } + + /** + * @dev Creates a proposal to add new measurements + * @param measurements The measurements to add + * @return proposalId The unique identifier for this proposal + */ + function proposeAddMeasurements( + UpgradeOperator.Measurements calldata measurements + ) external onlySigner returns (bytes32 proposalId) { + // Validate inputs + require(bytes(measurements.tag).length > 0, "Tag cannot be empty"); + require(measurements.mrtd.length > 0, "MRTD cannot be empty"); + require(measurements.mrseam.length > 0, "MRSEAM cannot be empty"); + require( + measurements.registrar_slots.length == + measurements.registrar_values.length, + "Registrar arrays length mismatch" + ); + + proposalNonce++; + proposalId = keccak256( + abi.encodePacked( + ProposalType.ADD_MEASUREMENTS, + measurements.tag, + measurements.mrtd, + measurements.mrseam, + proposalNonce + ) + ); + + Proposal storage proposal = proposals[proposalId]; + require(!proposal.executed, "Proposal already exists"); + + proposal.proposalType = ProposalType.ADD_MEASUREMENTS; + proposal.tagHash = keccak256(abi.encodePacked(measurements.tag)); + proposal.measurements = measurements; + + emit ProposalCreated( + proposalId, + ProposalType.ADD_MEASUREMENTS, + measurements.tag, + proposalNonce + ); + + // Auto-vote for proposer + _vote(proposalId); + + return proposalId; + } + + /** + * @dev Creates a proposal to deprecate measurements + * @param measurements The measurements to deprecate + * @return proposalId The unique identifier for this proposal + */ + function proposeDeprecateMeasurements( + UpgradeOperator.Measurements calldata measurements + ) external onlySigner returns (bytes32 proposalId) { + require(bytes(measurements.tag).length > 0, "Tag cannot be empty"); + + proposalNonce++; + proposalId = keccak256( + abi.encodePacked( + ProposalType.DEPRECATE_MEASUREMENTS, + measurements.tag, + proposalNonce + ) + ); + + Proposal storage proposal = proposals[proposalId]; + require(!proposal.executed, "Proposal already exists"); + + proposal.proposalType = ProposalType.DEPRECATE_MEASUREMENTS; + proposal.tagHash = keccak256(abi.encodePacked(measurements.tag)); + // Store tag in measurements.tag for execution + proposal.measurements = measurements; + + emit ProposalCreated( + proposalId, + ProposalType.DEPRECATE_MEASUREMENTS, + measurements.tag, + proposalNonce + ); + + // Auto-vote for proposer + _vote(proposalId); + + return proposalId; + } + /** - * @dev Creates a proposal to set defining attributes (version 1) in the UpgradeOperator - * @param mrtd The MRTD value (48 bytes) - * @param mrseam The MRSEAM value (48 bytes) - * @param pcr4 The PCR4 value (32 bytes) - * @param status The status to set + * @dev Creates a proposal to reinstate measurements + * @param measurements The measurements to reinstate * @return proposalId The unique identifier for this proposal */ - function createProposalV1( - bytes memory mrtd, - bytes memory mrseam, - bytes memory pcr4, - bool status - ) public returns (bytes32 proposalId) { - require(mrtd.length == 48, "Invalid mrtd length"); - require(mrseam.length == 48, "Invalid mrseam length"); - require(pcr4.length == 32, "Invalid pcr4 length"); - - // Increment nonce and use it in proposal ID calculation + function proposeReinstateMeasurements( + UpgradeOperator.Measurements calldata measurements + ) external onlySigner returns (bytes32 proposalId) { + require(bytes(measurements.tag).length > 0, "Tag cannot be empty"); + proposalNonce++; - proposalId = computeProposalIdV1(mrtd, mrseam, pcr4, status, proposalNonce); + proposalId = keccak256( + abi.encodePacked( + ProposalType.REINSTATE_MEASUREMENTS, + measurements.tag, + proposalNonce + ) + ); + + Proposal storage proposal = proposals[proposalId]; + require(!proposal.executed, "Proposal already exists"); + + proposal.proposalType = ProposalType.REINSTATE_MEASUREMENTS; + proposal.tagHash = keccak256(abi.encodePacked(measurements.tag)); + // Store tag in measurements.tag for execution + proposal.measurements = measurements; + + emit ProposalCreated( + proposalId, + ProposalType.REINSTATE_MEASUREMENTS, + measurements.tag, + proposalNonce + ); + + // Auto-vote for proposer + _vote(proposalId); - require(!executed[proposalId], "Proposal already executed"); - - emit ProposalCreatedV1(proposalId, proposalNonce, mrtd, mrseam, pcr4, status); - return proposalId; } - + /** - * @dev Casts a vote on a proposal + * @dev Vote on a proposal * @param proposalId The proposal to vote on - * @param approved Whether to approve the proposal */ - function vote(bytes32 proposalId, bool approved) public { - require(msg.sender == signer1 || msg.sender == signer2 || msg.sender == signer3, "Not authorized to vote"); - require(!executed[proposalId], "Proposal already executed"); - require(!votes[proposalId][msg.sender], "Already voted"); - - votes[proposalId][msg.sender] = approved; - - emit VoteCast(proposalId, msg.sender, approved); + function vote(bytes32 proposalId) external onlySigner { + _vote(proposalId); } - + /** - * @dev Executes a proposal if it has enough votes (version 1) - * @param mrtd The MRTD value (48 bytes) - * @param mrseam The MRSEAM value (48 bytes) - * @param pcr4 The PCR4 value (32 bytes) - * @param status The status to set - * @param nonce The nonce used when creating the proposal + * @dev Internal vote logic */ - function executeProposalV1( - bytes memory mrtd, - bytes memory mrseam, - bytes memory pcr4, - bool status, - uint256 nonce - ) public { - bytes32 proposalId = computeProposalIdV1(mrtd, mrseam, pcr4, status, nonce); - - require(!executed[proposalId], "Proposal already executed"); - - uint256 approvalCount = 0; - if (votes[proposalId][signer1]) approvalCount++; - if (votes[proposalId][signer2]) approvalCount++; - if (votes[proposalId][signer3]) approvalCount++; - - require(approvalCount >= 2, "Insufficient votes"); - - executed[proposalId] = true; - - // Execute the actual set_id_status_v1 call on the UpgradeOperator - upgradeOperator.set_id_status_v1(mrtd, mrseam, pcr4, status); - - emit ProposalExecuted(proposalId); + function _vote(bytes32 proposalId) internal { + Proposal storage proposal = proposals[proposalId]; + + require( + bytes(proposal.measurements.tag).length > 0, + "proposal does not exist" + ); + require(!proposal.executed, "Proposal already executed"); + require(!proposal.hasVoted[msg.sender], "Already voted"); + + proposal.hasVoted[msg.sender] = true; + proposal.voteCount++; + + emit VoteCast(proposalId, msg.sender); } - + /** - * @dev Gets the vote count for a proposal - * @param proposalId The proposal to check - * @return approvalCount Number of approvals - * @return totalVotes Total number of votes cast + * @dev Execute a proposal that has enough votes + * @param proposalId The proposal to execute */ - function getVoteCount(bytes32 proposalId) public view returns (uint256 approvalCount, uint256 totalVotes) { - if (votes[proposalId][signer1]) { - approvalCount++; - totalVotes++; - } - if (votes[proposalId][signer2]) { - approvalCount++; - totalVotes++; - } - if (votes[proposalId][signer3]) { - approvalCount++; - totalVotes++; + function executeProposal(bytes32 proposalId) external { + Proposal storage proposal = proposals[proposalId]; + + require(!proposal.executed, "Proposal already executed"); + require(proposal.voteCount >= 2, "Insufficient votes"); + _executeProposal(proposalId); + } + + /** + * @dev Internal execution logic + */ + function _executeProposal(bytes32 proposalId) internal { + Proposal storage proposal = proposals[proposalId]; + + proposal.executed = true; + + if (proposal.proposalType == ProposalType.ADD_MEASUREMENTS) { + upgradeOperator.addAcceptedMeasurements(proposal.measurements); + } else if ( + proposal.proposalType == ProposalType.DEPRECATE_MEASUREMENTS + ) { + upgradeOperator.deprecateMeasurements(proposal.measurements); + } else if ( + proposal.proposalType == ProposalType.REINSTATE_MEASUREMENTS + ) { + upgradeOperator.reinstateMeasurement(proposal.measurements); } - - return (approvalCount, totalVotes); + + emit ProposalExecuted(proposalId, proposal.proposalType); } - + /** - * @dev Checks if a proposal can be executed + * @dev Get vote status for a proposal * @param proposalId The proposal to check - * @return True if the proposal has enough votes to be executed + * @return voteCount Number of votes + * @return hasVoted1 Whether signer1 voted + * @return hasVoted2 Whether signer2 voted + * @return hasVoted3 Whether signer3 voted + * @return canExecute Whether proposal can be executed */ - function canExecute(bytes32 proposalId) public view returns (bool) { - if (executed[proposalId]) return false; - - uint256 approvalCount = 0; - if (votes[proposalId][signer1]) approvalCount++; - if (votes[proposalId][signer2]) approvalCount++; - if (votes[proposalId][signer3]) approvalCount++; - - return approvalCount >= 2; + function getVoteStatus( + bytes32 proposalId + ) + external + view + returns ( + uint256 voteCount, + bool hasVoted1, + bool hasVoted2, + bool hasVoted3, + bool canExecute + ) + { + Proposal storage proposal = proposals[proposalId]; + + voteCount = proposal.voteCount; + hasVoted1 = proposal.hasVoted[signer1]; + hasVoted2 = proposal.hasVoted[signer2]; + hasVoted3 = proposal.hasVoted[signer3]; + canExecute = !proposal.executed && proposal.voteCount >= 2; } - + /** - * @dev Computes the proposal ID for given parameters and nonce (version 1) - * Uses the UpgradeOperator's computeIdV1 method for the base ID calculation - * @param mrtd The MRTD value (48 bytes) - * @param mrseam The MRSEAM value (48 bytes) - * @param pcr4 The PCR4 value (32 bytes) - * @param status The status to set - * @param nonce The nonce to use - * @return The computed proposal ID + * @dev Get proposal details + * @param proposalId The proposal to query + * @return proposalType The type of proposal + * @return tag The measurement tag + * @return executed Whether the proposal has been executed + * @return voteCount Number of votes */ - function computeProposalIdV1( - bytes memory mrtd, - bytes memory mrseam, - bytes memory pcr4, - bool status, - uint256 nonce - ) public pure returns (bytes32) { - // Create the DefiningAttributesV1 struct and use the UpgradeOperator's computeIdV1 method - UpgradeOperator.DefiningAttributesV1 memory attrs = UpgradeOperator.DefiningAttributesV1(mrtd, mrseam, pcr4); - - bytes32 baseId; - try upgradeOperator.computeIdV1(attrs) returns (bytes32 result) { - baseId = result; - } catch { - revert("upgradeOperator.computeIdV1 failed"); - } + function getProposalInfo( + bytes32 proposalId + ) + external + view + returns ( + ProposalType proposalType, + string memory tag, + bool executed, + uint256 voteCount + ) + { + Proposal storage proposal = proposals[proposalId]; - // Combine with status and nonce for proposal uniqueness - return keccak256(abi.encodePacked(baseId, status, nonce)); + return ( + proposal.proposalType, + proposal.measurements.tag, + proposal.executed, + proposal.voteCount + ); + } + + /** + * @dev Get full measurement details for an add proposal + * @param proposalId The proposal to query + * @return measurements The full measurements struct + */ + function getProposalMeasurements( + bytes32 proposalId + ) external view returns (UpgradeOperator.Measurements memory) { + require( + proposals[proposalId].proposalType == ProposalType.ADD_MEASUREMENTS, + "Not an add measurements proposal" + ); + return proposals[proposalId].measurements; } -} \ No newline at end of file +} diff --git a/crates/enclave-contract/contracts/UpgradeOperator.sol b/crates/enclave-contract/contracts/UpgradeOperator.sol index 8ffcfa56..85593dd2 100644 --- a/crates/enclave-contract/contracts/UpgradeOperator.sol +++ b/crates/enclave-contract/contracts/UpgradeOperator.sol @@ -1,70 +1,199 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -/* - The Upgrade Operator is responsible for defining the - configuration to upgrade. -*/ contract UpgradeOperator { - - struct DefiningAttributesV1 { + struct Measurements { + string tag; bytes mrtd; bytes mrseam; - bytes pcr4; + uint8[] registrar_slots; + bytes[] registrar_values; } - struct DefiningAttributesV2 { - bytes mrtd; - bytes mrseam; - bytes pcr4; - bytes pcr7; + mapping(bytes32 => Measurements) public acceptedMeasurements; + mapping(bytes32 => Measurements) public deprecatedMeasurements; + + // Keep track of all tags for enumeration if needed + bytes32[] public acceptedTags; + bytes32[] public deprecatedTags; + + // Track if a tag exists to prevent duplicates + mapping(bytes32 => bool) public tagExists; + + address public constant OWNER = 0x1000000000000000000000000000000000000002; + + event MeasurementAdded(string indexed tag, bytes32 indexed tagHash); + event MeasurementDeprecated(string indexed tag, bytes32 indexed tagHash); + + modifier onlyNetworkMultisig() virtual { + require(msg.sender == OWNER, "Ownable: caller is not the owner"); + _; } - address constant public owner = 0x1000000000000000000000000000000000000002; // Set in seismic-reth genesis - mapping(bytes32 => bool) public attributes; + /** + * @dev Add a set of measurements the network allows + * @param measurements The measurements to add + */ + function addAcceptedMeasurements( + Measurements calldata measurements + ) external onlyNetworkMultisig { + bytes32 tagHash = keccak256(abi.encodePacked(measurements.tag)); + + // Check uniqueness + require(!tagExists[tagHash], "Measurement tag already exists"); - event SetDefiningAttributesV1(bytes mrtd, bytes mrseam, bytes pcr4, bool status); - event SetDefiningAttributesV2(bytes mrtd, bytes mrseam, bytes pcr4, bytes pcr7, bool status); + // Validate inputs TODO: assert actual length these measurements should be + require(bytes(measurements.tag).length > 0, "Tag cannot be empty"); + require(measurements.mrtd.length > 0, "MRTD cannot be empty"); + require(measurements.mrseam.length > 0, "MRSEAM cannot be empty"); + require( + measurements.registrar_slots.length == + measurements.registrar_values.length, + "Registrar arrays length mismatch" + ); + bytes32 measurementHash = _getMeasurementHash(measurements); + + // Store the measurement + acceptedMeasurements[measurementHash] = measurements; + acceptedTags.push(measurementHash); + tagExists[tagHash] = true; + + emit MeasurementAdded(measurements.tag, measurementHash); + } /** - * @dev Sets the status for a set of defining attributes (version 1) + * @dev accept a currently deprecated set of measurements + * @param m the measurement to reinstate */ - function set_id_status_v1(bytes memory mrtd, bytes memory mrseam, bytes memory pcr4, bool status) public { - require(msg.sender == owner, "Only owner can set status"); - require(mrtd.length == 48, "Invalid mrtd length"); - require(mrseam.length == 48, "Invalid mrseam length"); - require(pcr4.length == 32, "Invalid pcr4 length"); - - DefiningAttributesV1 memory attrs = DefiningAttributesV1(mrtd, mrseam, pcr4); - bytes32 id = computeIdV1(attrs); - attributes[id] = status; - emit SetDefiningAttributesV1(mrtd, mrseam, pcr4, status); + function reinstateMeasurement( + Measurements calldata m + ) external onlyNetworkMultisig { + bytes32 tagHash = keccak256(abi.encodePacked(m.tag)); + + bytes32 measurementHash = _getMeasurementHash(m); + + // Check if measurement exists in deprecated + require( + keccak256( + abi.encodePacked(deprecatedMeasurements[measurementHash].tag) + ) == tagHash, + "No deprecated measurement with that tag or hash" + ); + + // Move from deprecated to accepted + Measurements memory measurement = deprecatedMeasurements[ + measurementHash + ]; + acceptedMeasurements[measurementHash] = measurement; + acceptedTags.push(measurementHash); + + // Remove from deprecated + delete deprecatedMeasurements[measurementHash]; + _removeFromArray(deprecatedTags, measurementHash); + + emit MeasurementAdded(m.tag, measurementHash); } /** - * @dev Gets the status of a set of defining attributes (version 1) + * @dev Deprecate a currently allowed set of measurements + * @param m the measurement to deprecate */ - function get_id_status_v1(bytes memory mrtd, bytes memory mrseam, bytes memory pcr4) public view returns (bool) { - require(mrtd.length == 48, "Invalid mrtd length"); - require(mrseam.length == 48, "Invalid mrseam length"); - require(pcr4.length == 32, "Invalid pcr4 length"); - - DefiningAttributesV1 memory attrs = DefiningAttributesV1(mrtd, mrseam, pcr4); - bytes32 id = computeIdV1(attrs); - return attributes[id]; + function deprecateMeasurements( + Measurements calldata m + ) external onlyNetworkMultisig { + bytes32 tagHash = keccak256(abi.encodePacked(m.tag)); + bytes32 measurementHash = _getMeasurementHash(m); + // Check if measurement exists in accepted + require( + keccak256( + abi.encodePacked(acceptedMeasurements[measurementHash].tag) + ) == tagHash, + "No deprecated measurement with that tag or hash" + ); + + // Move from accepted to deprecated + Measurements memory measurement = acceptedMeasurements[measurementHash]; + deprecatedMeasurements[measurementHash] = measurement; + deprecatedTags.push(measurementHash); + + // Remove from accepted + delete acceptedMeasurements[measurementHash]; + _removeFromArray(acceptedTags, measurementHash); + + emit MeasurementDeprecated(m.tag, measurementHash); + } + + /** + * @dev Check if measurements are accepted + * @param measurementHash Hash of the measurements to check + */ + function isAccepted(bytes32 measurementHash) external view returns (bool) { + return bytes(acceptedMeasurements[measurementHash].tag).length > 0; + } + + /** + * @dev Check if measurements are accepted + * @param measurementHash Hash of the measurements to check + */ + function isDeprecated( + bytes32 measurementHash + ) external view returns (bool) { + return bytes(deprecatedMeasurements[measurementHash].tag).length > 0; } /** - * @dev Computes the ID for a set of defining attributes (version 1) + * @dev Get accepted measurement by tag */ - function computeIdV1(DefiningAttributesV1 memory attrs) public pure returns (bytes32) { - return keccak256(abi.encode(attrs)); + function getAcceptedMeasurement( + bytes32 measurementHash + ) external view returns (Measurements memory) { + require( + bytes(acceptedMeasurements[measurementHash].tag).length > 0, + "Measurement not found" + ); + return acceptedMeasurements[measurementHash]; } /** - * @dev Computes the ID for a set of defining attributes (version 2) + * @dev Get count of accepted measurements */ - function computeIdV2(DefiningAttributesV2 memory attrs) public pure returns (bytes32) { - return keccak256(abi.encode(attrs)); + function getAcceptedCount() external view returns (uint256) { + return acceptedTags.length; + } + + function getMeasurementHash( + Measurements calldata measurements + ) external pure returns (bytes32) { + return _getMeasurementHash(measurements); + } + + /** + * @dev Helper to remove element from array + */ + function _removeFromArray( + bytes32[] storage array, + bytes32 element + ) private { + for (uint256 i = 0; i < array.length; i++) { + if (array[i] == element) { + array[i] = array[array.length - 1]; + array.pop(); + break; + } + } + } + + function _getMeasurementHash( + Measurements memory m + ) private pure returns (bytes32) { + return + keccak256( + abi.encode( + m.mrtd, + m.mrseam, + m.registrar_slots, + m.registrar_values + ) + ); } -} \ No newline at end of file +} diff --git a/crates/enclave-contract/contracts/foundry.toml b/crates/enclave-contract/contracts/foundry.toml deleted file mode 100644 index 82d497fc..00000000 --- a/crates/enclave-contract/contracts/foundry.toml +++ /dev/null @@ -1,4 +0,0 @@ -[profile.default] -src = "." -out = "out" -libs = [] \ No newline at end of file diff --git a/crates/enclave-contract/foundry.toml b/crates/enclave-contract/foundry.toml new file mode 100644 index 00000000..ecb7d231 --- /dev/null +++ b/crates/enclave-contract/foundry.toml @@ -0,0 +1,5 @@ +[profile.default] +src = "contracts" +out = "out" +libs = ["lib"] +test = "tests" \ No newline at end of file diff --git a/crates/enclave-contract/lib/forge-std b/crates/enclave-contract/lib/forge-std new file mode 160000 index 00000000..8e40513d --- /dev/null +++ b/crates/enclave-contract/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8e40513d678f392f398620b3ef2b418648b33e89 diff --git a/crates/enclave-contract/src/contract_interface.rs b/crates/enclave-contract/src/contract_interface.rs index b57d3503..5eccd1bf 100644 --- a/crates/enclave-contract/src/contract_interface.rs +++ b/crates/enclave-contract/src/contract_interface.rs @@ -2,82 +2,56 @@ use alloy::{ network::EthereumWallet, - primitives::{bytes, Bytes, U256}, + primitives::{FixedBytes, Log}, providers::ProviderBuilder, signers::local::PrivateKeySigner, sol, }; -// Generate contract bindings for the factory -sol! { - #[sol(rpc)] - interface UpgradeOperatorFactory { - function deployUpgradeOperator(bytes32 salt) external returns (address); - function deployUpgradeOperatorWithOwner(bytes32 salt, address owner) external returns (address); - function deployMultisigUpgradeOperator(bytes32 salt, address upgradeOperator) external returns (address); - function deployUpgradeOperatorWithMultisig(bytes32 upgradeOperatorSalt, bytes32 multisigSalt) external returns (address, address); - function computeUpgradeOperatorAddress(bytes32 salt) external view returns (address); - function computeUpgradeOperatorAddressWithOwner(bytes32 salt, address owner) external view returns (address); - function computeMultisigUpgradeOperatorAddress(bytes32 salt, address upgradeOperator) external view returns (address); - function isDeployed(address contractAddress) external view returns (bool); - } -} +use crate::{Measurements, MultisigUpgradeOperator::ProposalCreated}; // Generate contract bindings for the upgrade operator sol! { #[sol(rpc)] interface UpgradeOperator { - function set_id_status_v1(bytes mrtd, bytes mrseam, bytes pcr4, bool status) external; - function get_id_status_v1(bytes mrtd, bytes mrseam, bytes pcr4) external view returns (bool); - function computeIdV1(bytes mrtd, bytes mrseam, bytes pcr4) external pure returns (bytes32); - function owner() external view returns (address); + struct Measurements { + string tag; + bytes mrtd; + bytes mrseam; + uint8[] registrar_slots; + bytes[] registrar_values; + } + function acceptedMeasurments(bytes32) public returns(Measurements); + function deprecatedMeasurments(bytes32) public returns(Measurements); + function acceptedTags() public returns(bytes32[]); + function deprecatedTags() public returns(bytes32[]); + + function addAcceptedMeasurements(Measurements measurements) external; + function reinstateMeasurement(Measurements measurements) external; + function deprecateMeasurements(Measurements measurements) external; + function isAccepted(bytes32 measurementHash) external view returns(bool); + function isDeprecated(bytes32 measurementHash) external view returns(bool); + function getAcceptedMeasurement(bytes32 measurementHash) external view returns(Measurements); + function getAcceptedCount() external view returns (uint256); + function OWNER() external view returns (address); + function getMeasurementHash(Measurements measurements) external pure returns(bytes32); } -} - // Generate contract bindings for the multisig contract -sol! { #[sol(rpc)] interface MultisigUpgradeOperator { - function createProposalV1(bytes mrtd, bytes mrseam, bytes pcr4, bool status) external returns (bytes32); - function vote(bytes32 proposalId, bool approved) external; - function executeProposalV1(bytes mrtd, bytes mrseam, bytes pcr4, bool status, uint256 nonce) external; - function getVoteCount(bytes32 proposalId) external view returns (uint256 approvalCount, uint256 totalVotes); - function canExecute(bytes32 proposalId) external view returns (bool); - function computeProposalIdV1(bytes mrtd, bytes mrseam, bytes pcr4, bool status, uint256 nonce) external view returns (bytes32); + event ProposalCreated(bytes32 indexed proposalId,uint8 indexed proposalType,string tag,uint256 nonce); + + function proposeAddMeasurements(UpgradeOperator.Measurements measurements) external returns(bytes32 proposalId); + function proposeDeprecateMeasurements(UpgradeOperator.Measurements measurements) external returns(bytes32 proposalId); + function proposeReinstateMeasurements(UpgradeOperator.Measurements measurements) external returns(bytes32 proposalId); + function vote(bytes32 proposalId) external; + function executeProposal(bytes32 proposalId) external; + function getVoteStatus(bytes32 proposalId) external view returns (uint256 voteCount, bool hasVoted1, bool hasVoted2, bool hasVoted3, bool canExecute); function proposalNonce() external view returns (uint256); function signer1() external view returns (address); function signer2() external view returns (address); function signer3() external view returns (address); function upgradeOperator() external view returns (address); - function setUpgradeOperator(address _upgradeOperator) external; - function factory() external view returns (address); - } -} - -/// Represents the proposal parameters for upgrade validation -/// This struct makes it easy to change the parameters in the future -/// To change parameters, just modify this struct and update the contract interfaces -#[derive(Debug, Clone)] -pub struct ProposalParamsV1 { - pub mrtd: Bytes, // 48 bytes - pub mrseam: Bytes, // 48 bytes - pub pcr4: Bytes, // 32 bytes -} - -impl ProposalParamsV1 { - /// Creates a new ProposalParams instance - pub fn new(mrtd: Bytes, mrseam: Bytes, pcr4: Bytes) -> Self { - Self { mrtd, mrseam, pcr4 } - } - - /// Creates test proposal parameters - /// Based off the devbox values - pub fn test_params() -> Self { - Self { - mrtd: bytes!("cbd40696f617d42254fc7037469cbcf1414fe173678798cfa1070b7d40e26fa8175b99d0cd245994278f980dec73146a"), - mrseam: bytes!("9790d89a10210ec6968a773cee2ca05b5aa97309f36727a968527be4606fc19e6f73acce350946c9d46a9bf7a63f8430"), - pcr4: bytes!("6f2f7d9a42b35a2f8f9d7bf366ca3e369a45d004f3ac49b0a93785fe817c82b5"), - } } } @@ -98,12 +72,11 @@ pub async fn create_multisig_proposal( multisig_address: alloy::primitives::Address, sk: &str, rpc: &str, - params: &ProposalParamsV1, - status: bool, -) -> Result<([u8; 32], u64), anyhow::Error> { + params: Measurements, +) -> Result, anyhow::Error> { // Set up signer with the provided sk let signer: PrivateKeySigner = sk.parse().unwrap(); - let wallet = EthereumWallet::from(signer); + let wallet = EthereumWallet::from(signer.clone()); let rpc_url = reqwest::Url::parse(rpc).unwrap(); let provider = ProviderBuilder::new().wallet(wallet).connect_http(rpc_url); @@ -111,20 +84,8 @@ pub async fn create_multisig_proposal( let multisig_contract = MultisigUpgradeOperator::new(multisig_address, std::sync::Arc::new(provider.clone())); - // Get current nonce before creating proposal (for debugging/logging if needed) - let _current_nonce = multisig_contract - .proposalNonce() - .call() - .await - .map_err(|e| anyhow::anyhow!("Failed to get current nonce: {:?}", e))?; - // Create proposal - let create_tx = multisig_contract.createProposalV1( - params.mrtd.clone(), - params.mrseam.clone(), - params.pcr4.clone(), - status, - ); + let create_tx = multisig_contract.proposeAddMeasurements(params); let create_pending = create_tx.send().await.map_err(|e| { anyhow::anyhow!( "create_multisig_proposal create proposal tx failed: {:?}", @@ -132,37 +93,17 @@ pub async fn create_multisig_proposal( ) })?; - let _create_receipt = create_pending - .watch() + // wait for it to be included + let receipt = create_pending + .get_receipt() .await .map_err(|e| anyhow::anyhow!("Failed to get proposal creation receipt: {:?}", e))?; - // Get the new nonce after proposal creation - let new_nonce = multisig_contract - .proposalNonce() - .call() - .await - .map_err(|e| anyhow::anyhow!("Failed to get new nonce: {:?}", e))?; - - // Compute the proposal ID using the new nonce - let proposal_id = multisig_contract - .computeProposalIdV1( - params.mrtd.clone(), - params.mrseam.clone(), - params.pcr4.clone(), - status, - new_nonce, - ) - .call() - .await - .map_err(|e| anyhow::anyhow!("Failed to compute proposal ID: {:?}", e))?; + let event: Log = receipt.decoded_log().unwrap(); - println!( - "Proposal created with ID: {:?}, nonce: {}", - proposal_id, new_nonce - ); + let proposal_id = event.proposalId; - Ok((proposal_id.into(), new_nonce.try_into().unwrap())) + Ok(proposal_id) } /// Votes on a proposal in the MultisigUpgradeOperator contract. @@ -182,8 +123,7 @@ pub async fn vote_on_multisig_proposal( multisig_address: alloy::primitives::Address, sk: &str, rpc: &str, - proposal_id: [u8; 32], - approved: bool, + proposal_id: FixedBytes<32>, ) -> Result<(), anyhow::Error> { // Set up signer with the provided sk let signer: PrivateKeySigner = sk.parse().unwrap(); @@ -196,7 +136,7 @@ pub async fn vote_on_multisig_proposal( MultisigUpgradeOperator::new(multisig_address, std::sync::Arc::new(provider)); // Vote on proposal - let vote_tx = multisig_contract.vote(proposal_id.into(), approved); + let vote_tx = multisig_contract.vote(proposal_id); let vote_pending = vote_tx .send() .await @@ -207,7 +147,7 @@ pub async fn vote_on_multisig_proposal( .await .map_err(|e| anyhow::anyhow!("Failed to get vote receipt: {:?}", e))?; - println!("Voted {} on proposal: {:?}", approved, proposal_id); + println!("Voted on proposal:{:?}", proposal_id); Ok(()) } @@ -230,9 +170,7 @@ pub async fn execute_multisig_proposal( multisig_address: alloy::primitives::Address, sk: &str, rpc: &str, - params: &ProposalParamsV1, - status: bool, - nonce: u64, + params: FixedBytes<32>, ) -> Result<(), anyhow::Error> { // Set up signer with the provided sk let signer: PrivateKeySigner = sk.parse().unwrap(); @@ -245,13 +183,7 @@ pub async fn execute_multisig_proposal( MultisigUpgradeOperator::new(multisig_address, std::sync::Arc::new(provider)); // Execute proposal - let execute_tx = multisig_contract.executeProposalV1( - params.mrtd.clone(), - params.mrseam.clone(), - params.pcr4.clone(), - status, - U256::from(nonce), - ); + let execute_tx = multisig_contract.executeProposal(params); let execute_pending = execute_tx .send() .await @@ -281,7 +213,7 @@ pub async fn execute_multisig_proposal( pub async fn can_execute_multisig_proposal( multisig_address: alloy::primitives::Address, rpc: &str, - proposal_id: [u8; 32], + proposal_id: FixedBytes<32>, ) -> Result { let rpc_url = reqwest::Url::parse(rpc).unwrap(); let provider = ProviderBuilder::new().connect_http(rpc_url); @@ -291,13 +223,13 @@ pub async fn can_execute_multisig_proposal( MultisigUpgradeOperator::new(multisig_address, std::sync::Arc::new(provider)); // Check if proposal can be executed - let can_execute = multisig_contract - .canExecute(proposal_id.into()) + let res = multisig_contract + .getVoteStatus(proposal_id) .call() .await .map_err(|e| anyhow::anyhow!("Failed to check if proposal can be executed: {:?}", e))?; - Ok(can_execute) + Ok(res.canExecute) } /// Gets the vote count for a proposal in the MultisigUpgradeOperator contract. @@ -314,8 +246,8 @@ pub async fn can_execute_multisig_proposal( pub async fn get_multisig_vote_count( multisig_address: alloy::primitives::Address, rpc: &str, - proposal_id: [u8; 32], -) -> Result<(u64, u64), anyhow::Error> { + proposal_id: FixedBytes<32>, +) -> Result { let rpc_url = reqwest::Url::parse(rpc).unwrap(); let provider = ProviderBuilder::new().connect_http(rpc_url); @@ -324,16 +256,13 @@ pub async fn get_multisig_vote_count( MultisigUpgradeOperator::new(multisig_address, std::sync::Arc::new(provider)); // Get vote count - let result = multisig_contract - .getVoteCount(proposal_id.into()) + let res = multisig_contract + .getVoteStatus(proposal_id) .call() .await .map_err(|e| anyhow::anyhow!("Failed to get vote count: {:?}", e))?; - Ok(( - result.approvalCount.try_into().unwrap(), - result.totalVotes.try_into().unwrap(), - )) + Ok(res.voteCount.try_into().unwrap()) } /// Checks if a proposal configuration is approved in the UpgradeOperator contract. @@ -347,10 +276,10 @@ pub async fn get_multisig_vote_count( /// # Returns /// /// * `Result` - Returns true if the proposal is approved, or an `anyhow::Error` if an error occurs. -pub async fn check_proposal_status_v1( +pub async fn check_proposal_status( upgrade_operator_address: alloy::primitives::Address, rpc: &str, - params: &ProposalParamsV1, + params: Measurements, ) -> Result { let rpc_url = reqwest::Url::parse(rpc).unwrap(); let provider = ProviderBuilder::new().connect_http(rpc_url); @@ -359,47 +288,18 @@ pub async fn check_proposal_status_v1( let upgrade_operator_contract = UpgradeOperator::new(upgrade_operator_address, std::sync::Arc::new(provider)); + let measurement_hash = upgrade_operator_contract + .getMeasurementHash(params) + .call() + .await + .map_err(|e| anyhow::anyhow!("get_measurement_hash failed: {:?}", e))?; + // Check proposal status let status = upgrade_operator_contract - .get_id_status_v1( - params.mrtd.clone(), - params.mrseam.clone(), - params.pcr4.clone(), - ) + .isAccepted(measurement_hash) .call() .await .map_err(|e| anyhow::anyhow!("check_proposal_status_v1 failed: {:?}", e))?; Ok(status) } - -/// Computes the CREATE2 address for a contract without deploying it. -/// -/// # Arguments -/// -/// * `factory_address` - The address of the factory contract. -/// * `rpc` - A string slice representing the RPC URL of the Ethereum node. -/// * `salt` - A 32-byte salt value for CREATE2 deployment. -/// -/// # Returns -/// -/// * `Result` - Returns the computed CREATE2 address if successful, or an `anyhow::Error` if an error occurs. -pub async fn compute_create2_address( - factory_address: alloy::primitives::Address, - rpc: &str, - salt: [u8; 32], -) -> Result { - let rpc_url = reqwest::Url::parse(rpc).unwrap(); - let provider = ProviderBuilder::new().connect_http(rpc_url); - - let factory_contract = - UpgradeOperatorFactory::new(factory_address, std::sync::Arc::new(provider)); - - let expected_address = factory_contract - .computeUpgradeOperatorAddress(salt.into()) - .call() - .await - .map_err(|e| anyhow::anyhow!("Failed to compute CREATE2 address: {:?}", e))?; - - Ok(expected_address) -} diff --git a/crates/enclave-contract/src/lib.rs b/crates/enclave-contract/src/lib.rs index 49fcb1cb..456295a1 100644 --- a/crates/enclave-contract/src/lib.rs +++ b/crates/enclave-contract/src/lib.rs @@ -7,6 +7,7 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] pub mod contract_interface; +pub use contract_interface::UpgradeOperator::Measurements; pub use contract_interface::*; /// Anvil's first secret key that they publically expose and fund for testing diff --git a/crates/enclave-contract/tests/MultisigUpgradeOperator.t.sol b/crates/enclave-contract/tests/MultisigUpgradeOperator.t.sol new file mode 100644 index 00000000..d46ab672 --- /dev/null +++ b/crates/enclave-contract/tests/MultisigUpgradeOperator.t.sol @@ -0,0 +1,398 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "../contracts/UpgradeOperator.sol"; +import "../contracts/MultisigUpgradeOperator.sol"; + +contract MultisigUpgradeOperatorTest is Test { + UpgradeOperatorMock public upgradeOperator; + MultisigUpgradeOperator public multisig; + + // ANVIL test accounts + address public signer1 = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + address public signer2 = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + address public signer3 = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; + address public nonSigner = address(0x4); + + // Test data + UpgradeOperator.Measurements public testMeasurement1; + UpgradeOperator.Measurements public testMeasurement2; + bytes32 public measurement1Hash; + bytes32 public measurement2Hash; + + event ProposalCreated( + bytes32 indexed proposalId, + MultisigUpgradeOperator.ProposalType indexed proposalType, + string tag, + uint256 nonce + ); + + event VoteCast(bytes32 indexed proposalId, address indexed voter); + + event ProposalExecuted( + bytes32 indexed proposalId, + MultisigUpgradeOperator.ProposalType indexed proposalType + ); + + function setUp() public { + // Deploy MultisigUpgradeOperator + multisig = new MultisigUpgradeOperator(); + + // Since we can't actually transfer ownership in this test setup, + // we'll use vm.etch to deploy the UpgradeOperator at the expected address + // and modify it to accept the multisig as owner + + // For testing purposes, we'll deploy a modified UpgradeOperator + // that accepts our multisig as the owner + UpgradeOperatorMock mockUpgradeOperator = new UpgradeOperatorMock( + address(multisig) + ); + vm.etch( + address(0x1000000000000000000000000000000000000001), + address(mockUpgradeOperator).code + ); + + upgradeOperator = UpgradeOperatorMock( + 0x1000000000000000000000000000000000000001 + ); + + // Setup test measurements + testMeasurement1.tag = "AzureV1"; + testMeasurement1 + .mrtd = hex"cbd40696f617d42254fc7037469cbcf1414fe173678798cfa1070b7d40e26fa8175b99d0cd245994278f980dec73146a"; + testMeasurement1 + .mrseam = hex"9790d89a10210ec6968a773cee2ca05b5aa97309f36727a968527be4606fc19e6f73acce350946c9d46a9bf7a63f8430"; + testMeasurement1.registrar_slots = new uint8[](1); + testMeasurement1.registrar_slots[0] = 4; + testMeasurement1.registrar_values = new bytes[](1); + testMeasurement1.registrar_values[ + 0 + ] = hex"6f2f7d9a42b35a2f8f9d7bf366ca3e369a45d004f3ac49b0a93785fe817c82b5"; + + testMeasurement2.tag = "AWSV1"; + testMeasurement2 + .mrtd = hex"555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555"; + testMeasurement2 + .mrseam = hex"666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666"; + testMeasurement2.registrar_slots = new uint8[](1); + testMeasurement2.registrar_slots[0] = 3; + testMeasurement2.registrar_values = new bytes[](1); + testMeasurement2.registrar_values[ + 0 + ] = hex"77777777777777777777777777777777777777777777777777777777"; + + measurement1Hash = upgradeOperator.getMeasurementHash(testMeasurement1); + measurement2Hash = upgradeOperator.getMeasurementHash(testMeasurement2); + } + + // Test proposal creation for adding measurements + function testProposeAddMeasurements() public { + vm.prank(signer1); + + bytes32 proposalId = multisig.proposeAddMeasurements(testMeasurement1); + + // Check proposal was created + ( + MultisigUpgradeOperator.ProposalType proposalType, + string memory tag, + bool executed, + uint256 voteCount + ) = multisig.getProposalInfo(proposalId); + + assertEq( + uint(proposalType), + uint(MultisigUpgradeOperator.ProposalType.ADD_MEASUREMENTS) + ); + assertEq(tag, "AzureV1"); + assertFalse(executed); + assertEq(voteCount, 1); // Auto-voted by proposer + } + + // Test non-signer cannot create proposals + function testNonSignerCannotPropose() public { + vm.prank(nonSigner); + vm.expectRevert("Not authorized"); + multisig.proposeAddMeasurements(testMeasurement1); + } + + // Test voting on proposal + function testVoting() public { + vm.prank(signer1); + bytes32 proposalId = multisig.proposeAddMeasurements(testMeasurement1); + + // Check initial vote count + (uint256 voteCount, , , , ) = multisig.getVoteStatus(proposalId); + assertEq(voteCount, 1); + + // Second signer votes + vm.prank(signer2); + vm.expectEmit(true, true, false, true); + emit VoteCast(proposalId, signer2); + multisig.vote(proposalId); + + // Check vote count increased + (voteCount, , , , ) = multisig.getVoteStatus(proposalId); + assertEq(voteCount, 2); + } + + // Test double voting fails + function testDoubleVoteFails() public { + vm.prank(signer1); + bytes32 proposalId = multisig.proposeAddMeasurements(testMeasurement1); + + vm.prank(signer1); + vm.expectRevert("Already voted"); + multisig.vote(proposalId); + } + + // Test manual execution + function testManualExecution() public { + vm.prank(signer1); + bytes32 proposalId = multisig.proposeAddMeasurements(testMeasurement1); + + vm.prank(signer2); + multisig.vote(proposalId); + + // Anyone can execute once threshold is met + vm.prank(nonSigner); + multisig.executeProposal(proposalId); + + // Check proposal is executed + (, , bool executed, ) = multisig.getProposalInfo(proposalId); + assertTrue(executed); + } + + // Test cannot execute without enough votes + function testCannotExecuteWithoutVotes() public { + vm.prank(signer1); + bytes32 proposalId = multisig.proposeAddMeasurements(testMeasurement1); + + vm.expectRevert("Insufficient votes"); + multisig.executeProposal(proposalId); + } + + // Test deprecate measurements proposal + function testProposeDeprecateMeasurements() public { + // First add a measurement via multisig + vm.prank(signer1); + bytes32 addProposalId = multisig.proposeAddMeasurements( + testMeasurement1 + ); + vm.prank(signer2); + multisig.vote(addProposalId); + multisig.executeProposal(addProposalId); + + assertTrue(upgradeOperator.isAccepted(measurement1Hash)); + + // Now propose to deprecate it + vm.prank(signer1); + bytes32 deprecateProposalId = multisig.proposeDeprecateMeasurements( + testMeasurement1 + ); + + // Vote to execute + vm.prank(signer3); + multisig.vote(deprecateProposalId); + multisig.executeProposal(deprecateProposalId); + + // Check it's deprecated + assertFalse(upgradeOperator.isAccepted(measurement1Hash)); + assertTrue(upgradeOperator.isDeprecated(measurement1Hash)); + } + + // Test reinstate measurements proposal + function testProposeReinstateMeasurements() public { + // Add, then deprecate a measurement + vm.prank(signer1); + bytes32 addProposalId = multisig.proposeAddMeasurements( + testMeasurement1 + ); + vm.prank(signer2); + multisig.vote(addProposalId); + multisig.executeProposal(addProposalId); + + vm.prank(signer1); + bytes32 deprecateProposalId = multisig.proposeDeprecateMeasurements( + testMeasurement1 + ); + vm.prank(signer2); + multisig.vote(deprecateProposalId); + multisig.executeProposal(deprecateProposalId); + assertFalse(upgradeOperator.isAccepted(measurement1Hash)); + assertTrue(upgradeOperator.isDeprecated(measurement1Hash)); + + // Now propose to reinstate + vm.prank(signer1); + bytes32 reinstateProposalId = multisig.proposeReinstateMeasurements( + testMeasurement1 + ); + vm.prank(signer3); + multisig.vote(reinstateProposalId); + multisig.executeProposal(reinstateProposalId); + + // Check it's reinstated + assertTrue(upgradeOperator.isAccepted(measurement1Hash)); + assertFalse(upgradeOperator.isDeprecated(measurement1Hash)); + } + + // Test get vote status + function testGetVoteStatus() public { + vm.prank(signer1); + bytes32 proposalId = multisig.proposeAddMeasurements(testMeasurement1); + + ( + uint256 voteCount, + bool hasVoted1, + bool hasVoted2, + bool hasVoted3, + bool canExecute + ) = multisig.getVoteStatus(proposalId); + + assertEq(voteCount, 1); + assertTrue(hasVoted1); + assertFalse(hasVoted2); + assertFalse(hasVoted3); + assertFalse(canExecute); + + vm.prank(signer2); + multisig.vote(proposalId); + + (voteCount, hasVoted1, hasVoted2, hasVoted3, canExecute) = multisig + .getVoteStatus(proposalId); + + assertEq(voteCount, 2); + assertTrue(hasVoted1); + assertTrue(hasVoted2); + assertFalse(hasVoted3); + assertTrue(canExecute); + } + + // Test complete multisig workflow + function testCompleteMultisigWorkflow() public { + // Signer1 proposes to add Azure measurements + vm.prank(signer1); + bytes32 proposal1 = multisig.proposeAddMeasurements(testMeasurement1); + // Signer2 votes, triggering execution + vm.prank(signer2); + multisig.vote(proposal1); + multisig.executeProposal(proposal1); + + assertTrue(upgradeOperator.isAccepted(measurement1Hash)); + + // Signer2 proposes to add AWS measurements + vm.prank(signer2); + bytes32 proposal2 = multisig.proposeAddMeasurements(testMeasurement2); + + // Signer3 votes allowing enough votes for execution + vm.prank(signer3); + multisig.vote(proposal2); + + // execute + multisig.executeProposal(proposal2); + + assertTrue(upgradeOperator.isAccepted(measurement2Hash)); + + assertEq(upgradeOperator.getAcceptedCount(), 2); + + // Signer1 proposes to deprecate Azure + vm.prank(signer1); + bytes32 proposal3 = multisig.proposeDeprecateMeasurements( + testMeasurement1 + ); + + // Signer2 votes + vm.prank(signer2); + multisig.vote(proposal3); + multisig.executeProposal(proposal3); + + assertFalse(upgradeOperator.isAccepted(measurement1Hash)); + assertTrue(upgradeOperator.isDeprecated(measurement1Hash)); + assertEq(upgradeOperator.getAcceptedCount(), 1); + + // Signer3 proposes to reinstate Azure + vm.prank(signer3); + bytes32 proposal4 = multisig.proposeReinstateMeasurements( + testMeasurement1 + ); + + // Signer1 votes + vm.prank(signer1); + multisig.vote(proposal4); + multisig.executeProposal(proposal4); + + assertTrue(upgradeOperator.isAccepted(measurement1Hash)); + assertFalse(upgradeOperator.isDeprecated(measurement1Hash)); + assertEq(upgradeOperator.getAcceptedCount(), 2); + } + + // Test proposal with invalid measurements fails + function testInvalidMeasurementsFails() public { + UpgradeOperator.Measurements memory invalidMeasurement; + + // Empty tag + invalidMeasurement = testMeasurement1; + invalidMeasurement.tag = ""; + vm.prank(signer1); + vm.expectRevert("Tag cannot be empty"); + multisig.proposeAddMeasurements(invalidMeasurement); + + // Empty MRTD + invalidMeasurement = testMeasurement1; + invalidMeasurement.mrtd = ""; + vm.prank(signer1); + vm.expectRevert("MRTD cannot be empty"); + multisig.proposeAddMeasurements(invalidMeasurement); + + // Mismatched arrays + invalidMeasurement = testMeasurement1; + uint8[] memory slots = new uint8[](3); + slots[0] = 1; + slots[1] = 2; + slots[2] = 3; + invalidMeasurement.registrar_slots = slots; + vm.prank(signer1); + vm.expectRevert("Registrar arrays length mismatch"); + multisig.proposeAddMeasurements(invalidMeasurement); + } + + // Test get proposal measurements + function testGetProposalMeasurements() public { + vm.prank(signer1); + bytes32 proposalId = multisig.proposeAddMeasurements(testMeasurement1); + + UpgradeOperator.Measurements memory retrieved = multisig + .getProposalMeasurements(proposalId); + + assertEq(retrieved.tag, testMeasurement1.tag); + assertEq(retrieved.mrtd, testMeasurement1.mrtd); + assertEq(retrieved.mrseam, testMeasurement1.mrseam); + assertEq(retrieved.registrar_slots.length, 1); + assertEq(retrieved.registrar_values.length, 1); + } + + // Test cannot get measurements for non-add proposals + function testCannotGetMeasurementsForDeprecateProposal() public { + vm.prank(signer1); + bytes32 proposalId = multisig.proposeDeprecateMeasurements( + testMeasurement1 + ); + + vm.expectRevert("Not an add measurements proposal"); + multisig.getProposalMeasurements(proposalId); + } +} + +// Mock UpgradeOperator for testing that accepts a different owner +contract UpgradeOperatorMock is UpgradeOperator { + address public immutable testOwner; + + constructor(address _testOwner) { + testOwner = _testOwner; + } + + modifier onlyNetworkMultisig() override { + require(msg.sender == testOwner, "Ownable-- caller is not the owner"); + _; + } +} diff --git a/crates/enclave-contract/tests/multisig_test.rs b/crates/enclave-contract/tests/multisig_test.rs index b0b4d1fb..3e9ea1e0 100644 --- a/crates/enclave-contract/tests/multisig_test.rs +++ b/crates/enclave-contract/tests/multisig_test.rs @@ -1,13 +1,17 @@ +use enclave_contract::can_execute_multisig_proposal; +use enclave_contract::check_proposal_status; +use enclave_contract::create_multisig_proposal; +use enclave_contract::execute_multisig_proposal; +use enclave_contract::get_multisig_vote_count; +use enclave_contract::vote_on_multisig_proposal; +use enclave_contract::Measurements; use enclave_contract::UPGRADE_MULTISIG_ADDRESS; use enclave_contract::UPGRADE_OPERATOR_ADDRESS; -use enclave_contract::{ - can_execute_multisig_proposal, check_proposal_status_v1, create_multisig_proposal, - execute_multisig_proposal, get_multisig_vote_count, vote_on_multisig_proposal, - ProposalParamsV1, ANVIL_ALICE_SK, ANVIL_BOB_SK, -}; + +use alloy::primitives::bytes; +use enclave_contract::{ANVIL_ALICE_SK, ANVIL_BOB_SK}; use std::thread::sleep; use std::time::Duration; - /// Prints a string to standard output and immediately flushes the output buffer. /// Useful to see prints immediately during long-running Cargo tests. pub fn print_flush>(s: S) { @@ -36,37 +40,37 @@ pub async fn test_multisig_upgrade_operator_workflow() -> Result<(), anyhow::Err sleep(Duration::from_secs(2)); // Test data for proposal - let params = ProposalParamsV1::test_params(); - let status = true; + let params = Measurements { + tag: "AlloyV1".to_string(), + mrtd: bytes!("cbd40696f617d42254fc7037469cbcf1414fe173678798cfa1070b7d40e26fa8175b99d0cd245994278f980dec73146a"), + mrseam: bytes!("9790d89a10210ec6968a773cee2ca05b5aa97309f36727a968527be4606fc19e6f73acce350946c9d46a9bf7a63f8430"), + registrar_slots: vec![4], + registrar_values: vec![bytes!("6f2f7d9a42b35a2f8f9d7bf366ca3e369a45d004f3ac49b0a93785fe817c82b5")], + }; print_flush("Creating multisig proposal...\n"); // Create a proposal to set MRTD - let (proposal_id, nonce) = - create_multisig_proposal(multisig_address, ANVIL_ALICE_SK, reth_rpc, ¶ms, status) + let proposal_id = + create_multisig_proposal(multisig_address, ANVIL_ALICE_SK, reth_rpc, params.clone()) .await .map_err(|e| anyhow::anyhow!("multisig workflow failed to create proposal: {:?}", e))?; - print_flush(format!( - "Proposal created with ID: {:?}, nonce: {}\n", - proposal_id, nonce - )); + print_flush(format!("Proposal created with ID: {:?}\n", proposal_id)); // Wait a bit for the transaction to be processed sleep(Duration::from_secs(2)); // Check initial vote count - let (approval_count, total_votes) = - get_multisig_vote_count(multisig_address, reth_rpc, proposal_id) - .await - .map_err(|e| anyhow::anyhow!("failed to get vote count: {:?}", e))?; + let total_votes = get_multisig_vote_count(multisig_address, reth_rpc, proposal_id) + .await + .map_err(|e| anyhow::anyhow!("failed to get vote count: {:?}", e))?; print_flush(format!( - "Initial vote count - Approvals: {}, Total votes: {}\n", - approval_count, total_votes + "Initial vote count - Total votes: {}\n", + total_votes )); - assert_eq!(approval_count, 0, "Initial approval count should be 0"); - assert_eq!(total_votes, 0, "Initial total votes should be 0"); + assert_eq!(total_votes, 1, "Initial total votes should be 1"); // Check if proposal can be executed (should be false initially) let can_execute = can_execute_multisig_proposal(multisig_address, reth_rpc, proposal_id) @@ -76,56 +80,10 @@ pub async fn test_multisig_upgrade_operator_workflow() -> Result<(), anyhow::Err print_flush(format!("Can execute proposal: {}\n", can_execute)); assert!(!can_execute, "Proposal should not be executable initially"); - print_flush("Alice voting yes on proposal...\n"); - - // Alice votes yes - vote_on_multisig_proposal( - multisig_address, - ANVIL_ALICE_SK, - reth_rpc, - proposal_id, - true, - ) - .await - .map_err(|e| anyhow::anyhow!("failed to vote with Alice: {:?}", e))?; - - // Wait a bit for the transaction to be processed - sleep(Duration::from_secs(2)); - - // Check vote count after Alice's vote - let (approval_count, total_votes) = - get_multisig_vote_count(multisig_address, reth_rpc, proposal_id) - .await - .map_err(|e| anyhow::anyhow!("failed to get vote count: {:?}", e))?; - - print_flush(format!( - "Vote count after Alice - Approvals: {}, Total votes: {}\n", - approval_count, total_votes - )); - assert_eq!( - approval_count, 1, - "Approval count should be 1 after Alice's vote" - ); - assert_eq!(total_votes, 1, "Total votes should be 1 after Alice's vote"); - - // Check if proposal can be executed (should still be false with only 1 vote) - let can_execute = can_execute_multisig_proposal(multisig_address, reth_rpc, proposal_id) - .await - .map_err(|e| anyhow::anyhow!("failed to check if proposal can be executed: {:?}", e))?; - - print_flush(format!( - "Can execute proposal after Alice: {}\n", - can_execute - )); - assert!( - !can_execute, - "Proposal should not be executable with only 1 vote" - ); - print_flush("Bob voting yes on proposal...\n"); // Bob votes yes - vote_on_multisig_proposal(multisig_address, ANVIL_BOB_SK, reth_rpc, proposal_id, true) + vote_on_multisig_proposal(multisig_address, ANVIL_BOB_SK, reth_rpc, proposal_id) .await .map_err(|e| anyhow::anyhow!("failed to vote with Bob: {:?}", e))?; @@ -133,19 +91,15 @@ pub async fn test_multisig_upgrade_operator_workflow() -> Result<(), anyhow::Err sleep(Duration::from_secs(2)); // Check vote count after Bob's vote - let (approval_count, total_votes) = - get_multisig_vote_count(multisig_address, reth_rpc, proposal_id) - .await - .map_err(|e| anyhow::anyhow!("failed to get vote count: {:?}", e))?; + let total_votes = get_multisig_vote_count(multisig_address, reth_rpc, proposal_id) + .await + .map_err(|e| anyhow::anyhow!("failed to get vote count: {:?}", e))?; print_flush(format!( - "Vote count after Bob - Approvals: {}, Total votes: {}\n", - approval_count, total_votes + "Vote count after Bob - Total votes: {}\n", + total_votes )); - assert_eq!( - approval_count, 2, - "Approval count should be 2 after Bob's vote" - ); + assert_eq!(total_votes, 2, "Total votes should be 2 after Bob's vote"); // Check if proposal can be executed (should be true with 2 votes) @@ -158,7 +112,7 @@ pub async fn test_multisig_upgrade_operator_workflow() -> Result<(), anyhow::Err // Check initial proposal status (should be false) let initial_proposal_status = - check_proposal_status_v1(upgrade_operator_address, reth_rpc, ¶ms) + check_proposal_status(upgrade_operator_address, reth_rpc, params.clone()) .await .map_err(|e| anyhow::anyhow!("failed to check initial proposal status: {:?}", e))?; @@ -174,16 +128,9 @@ pub async fn test_multisig_upgrade_operator_workflow() -> Result<(), anyhow::Err print_flush("Executing proposal...\n"); // Execute the proposal - execute_multisig_proposal( - multisig_address, - ANVIL_ALICE_SK, - reth_rpc, - ¶ms, - status, - nonce, - ) - .await - .map_err(|e| anyhow::anyhow!("failed to execute proposal: {:?}", e))?; + execute_multisig_proposal(multisig_address, ANVIL_ALICE_SK, reth_rpc, proposal_id) + .await + .map_err(|e| anyhow::anyhow!("failed to execute proposal: {:?}", e))?; // Wait a bit for the transaction to be processed sleep(Duration::from_secs(2)); @@ -191,10 +138,9 @@ pub async fn test_multisig_upgrade_operator_workflow() -> Result<(), anyhow::Err print_flush("Checking final proposal status...\n"); // Check final proposal status (should be true) - let final_proposal_status = - check_proposal_status_v1(upgrade_operator_address, reth_rpc, ¶ms) - .await - .map_err(|e| anyhow::anyhow!("failed to check final proposal status: {:?}", e))?; + let final_proposal_status = check_proposal_status(upgrade_operator_address, reth_rpc, params) + .await + .map_err(|e| anyhow::anyhow!("failed to check final proposal status: {:?}", e))?; print_flush(format!( "Final proposal status: {}\n", diff --git a/crates/enclave-server/src/server/boot.rs b/crates/enclave-server/src/server/boot.rs index 32245d29..1c736a6f 100644 --- a/crates/enclave-server/src/server/boot.rs +++ b/crates/enclave-server/src/server/boot.rs @@ -234,11 +234,13 @@ impl Booter { } // Create ProposalParamsV1 struct - let params = enclave_contract::ProposalParamsV1::new( - alloy::primitives::Bytes::from(mr_td_bytes), - alloy::primitives::Bytes::from(mr_seam_bytes), - alloy::primitives::Bytes::from(pcr4_bytes), - ); + let params = enclave_contract::Measurements { + tag: "AzureV1".to_string(), + mrtd: alloy::primitives::Bytes::from(mr_td_bytes), + mrseam: alloy::primitives::Bytes::from(mr_seam_bytes), + registrar_slots: vec![4], + registrar_values: vec![alloy::primitives::Bytes::from(pcr4_bytes)], + }; // Get contract address and RPC URL from environment variables let upgrade_operator_address = enclave_contract::UPGRADE_OPERATOR_ADDRESS @@ -248,7 +250,7 @@ impl Booter { // Check the proposal status against the onchain contract let status = - enclave_contract::check_proposal_status_v1(upgrade_operator_address, &rpc_url, ¶ms) + enclave_contract::check_proposal_status(upgrade_operator_address, &rpc_url, params) .await .map_err(|e| anyhow::anyhow!("Booter failed to check proposal status: {e}"))?; diff --git a/crates/enclave-server/tests/integration/snapshot.rs b/crates/enclave-server/tests/integration/snapshot.rs index 43ac9ed1..168f2c38 100644 --- a/crates/enclave-server/tests/integration/snapshot.rs +++ b/crates/enclave-server/tests/integration/snapshot.rs @@ -144,6 +144,8 @@ pub async fn test_snapshot_integration_handlers() -> Result<(), anyhow::Error> { "restore_from_encrypted_snapshot failed: {}", restore_resp.error ); + // give reth some time to start back up + sleep(Duration::from_secs(15)); assert!(Path::new(format!("{}/db/mdbx.dat", RETH_DATA_DIR).as_str()).exists()); assert!(reth_is_running()); diff --git a/scripts/run_integration_tests.sh b/scripts/run_integration_tests.sh index 1c21fa5f..c31d0b49 100755 --- a/scripts/run_integration_tests.sh +++ b/scripts/run_integration_tests.sh @@ -17,6 +17,7 @@ echo "๐Ÿš€ Starting integration tests..." # Function to cleanup processes cleanup() { echo "๐Ÿงน Cleaning up processes..." + echo $(sudo supervisorctl status) sudo supervisorctl stop all || true # Logs are stored elsewhere: # ~/.reth-logs and /var/log/reth.{out,err}.log @@ -26,16 +27,19 @@ cleanup() { # # Set up trap to cleanup on exit trap cleanup EXIT - -# Make sure enclave is NOT running so we can access TPM in test -sudo supervisorctl stop seismic-enclave-server || true -sleep 2 +# Make sure enclave is running so we can start reth +sudo supervisorctl start enclave-server || true +sleep 10 # Start services via supervisor echo "๐Ÿ”ง Starting supervisor services..." sudo supervisorctl start reth || true sleep 10 +# Make sure enclave is NOT running so we can access TPM in test +sudo supervisorctl stop enclave-server || true +sleep 2 + # Check if reth is running echo "๐Ÿ” Checking if reth is running..." if ! sudo supervisorctl status reth | grep -q "RUNNING"; then @@ -44,6 +48,7 @@ if ! sudo supervisorctl status reth | grep -q "RUNNING"; then fi echo "โœ… reth service is running" + # Test 1: Run multisig upgrade operator workflow test echo "๐Ÿงช Running test_multisig_upgrade_operator_workflow..." cd crates/enclave-contract @@ -79,6 +84,7 @@ fi echo "Found binaries: ${binaries[*]}" # Run the first binary with the specific test sleep 2 + echo "๐Ÿš€ Executing: sudo ${binaries[0]} test_boot_share_root_key" if ! sudo "${binaries[0]}" test_boot_share_root_key; then echo "โŒ test_boot_share_root_key failed with exit code $?" @@ -90,8 +96,9 @@ echo "โœ… test_boot_share_root_key passed" # Test 3: Run snapshot integration handlers test echo "๐Ÿงช Running test_snapshot_integration_handlers..." -sudo supervisorctl start enclave-server -sleep 2 +sudo supervisorctl start enclave-server +sleep 10 + if ! sudo "${binaries[0]}" test_snapshot_integration_handlers; then echo "โŒ test_snapshot_integration_handlers failed" exit 1