|
| 1 | +/* |
| 2 | + * SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network> |
| 3 | + * |
| 4 | + * SPDX-License-Identifier: Apache-2.0 |
| 5 | + */ |
| 6 | + |
| 7 | +pragma solidity ^0.8.24; |
| 8 | + |
| 9 | +import "forge-std/Test.sol"; |
| 10 | +import "openzeppelin-foundry-upgrades/Upgrades.sol"; |
| 11 | +import "../contracts/DstackKms.sol"; |
| 12 | +import "../contracts/DstackApp.sol"; |
| 13 | + |
| 14 | +contract EventAuditTest is Test { |
| 15 | + bytes32 private constant AUDIT_TOPIC = keccak256("PolicyChanged(address,bytes32,bytes32,bool)"); |
| 16 | + |
| 17 | + address private owner; |
| 18 | + address private outsider; |
| 19 | + DstackKms private kms; |
| 20 | + DstackApp private app; |
| 21 | + |
| 22 | + function setUp() public { |
| 23 | + owner = makeAddr("audit-owner"); |
| 24 | + outsider = makeAddr("audit-outsider"); |
| 25 | + vm.startPrank(owner); |
| 26 | + DstackApp appImplementation = new DstackApp(); |
| 27 | + kms = DstackKms( |
| 28 | + Upgrades.deployUUPSProxy( |
| 29 | + "DstackKms.sol", abi.encodeCall(DstackKms.initialize, (owner, address(appImplementation))) |
| 30 | + ) |
| 31 | + ); |
| 32 | + app = DstackApp( |
| 33 | + Upgrades.deployUUPSProxy( |
| 34 | + "DstackApp.sol", |
| 35 | + abi.encodeWithSignature( |
| 36 | + "initialize(address,bool,bool,bool,bytes32,bytes32)", |
| 37 | + owner, |
| 38 | + false, |
| 39 | + false, |
| 40 | + false, |
| 41 | + bytes32(0), |
| 42 | + bytes32(0) |
| 43 | + ) |
| 44 | + ) |
| 45 | + ); |
| 46 | + vm.stopPrank(); |
| 47 | + } |
| 48 | + |
| 49 | + function test_KmsPolicyEventsReconstructQueriedState() public { |
| 50 | + bytes32 mr = keccak256("audit-mr"); |
| 51 | + bytes32 device = keccak256("audit-device"); |
| 52 | + bytes32 image = keccak256("audit-image"); |
| 53 | + string memory gateway = "audit-gateway"; |
| 54 | + |
| 55 | + vm.recordLogs(); |
| 56 | + vm.startPrank(owner); |
| 57 | + kms.addKmsAggregatedMr(mr); |
| 58 | + kms.addKmsDevice(device); |
| 59 | + kms.addOsImageHash(image); |
| 60 | + kms.setGatewayAppId(gateway); |
| 61 | + kms.registerApp(address(app)); |
| 62 | + kms.removeKmsAggregatedMr(mr); |
| 63 | + vm.stopPrank(); |
| 64 | + Vm.Log[] memory logs = vm.getRecordedLogs(); |
| 65 | + |
| 66 | + _assertAudit(logs, owner, "kms-aggregated-mr", mr, true); |
| 67 | + _assertAudit(logs, owner, "kms-device", device, true); |
| 68 | + _assertAudit(logs, owner, "os-image", image, true); |
| 69 | + _assertAudit(logs, owner, "gateway-app-id", keccak256(bytes(gateway)), true); |
| 70 | + _assertAudit(logs, owner, "registered-app", bytes32(uint256(uint160(address(app)))), true); |
| 71 | + _assertAudit(logs, owner, "kms-aggregated-mr", mr, false); |
| 72 | + |
| 73 | + assertFalse(kms.kmsAllowedAggregatedMrs(mr)); |
| 74 | + assertTrue(kms.kmsAllowedDeviceIds(device)); |
| 75 | + assertTrue(kms.allowedOsImages(image)); |
| 76 | + assertEq(kms.gatewayAppId(), gateway); |
| 77 | + assertTrue(kms.registeredApps(address(app))); |
| 78 | + } |
| 79 | + |
| 80 | + function test_AppPolicyEventsReconstructQueriedState() public { |
| 81 | + bytes32 composeHash = keccak256("audit-compose"); |
| 82 | + bytes32 device = keccak256("audit-app-device"); |
| 83 | + |
| 84 | + vm.recordLogs(); |
| 85 | + vm.startPrank(owner); |
| 86 | + app.addComposeHash(composeHash); |
| 87 | + app.addDevice(device); |
| 88 | + app.setAllowAnyDevice(true); |
| 89 | + app.setRequireTcbUpToDate(true); |
| 90 | + app.removeDevice(device); |
| 91 | + app.disableUpgrades(); |
| 92 | + vm.stopPrank(); |
| 93 | + Vm.Log[] memory logs = vm.getRecordedLogs(); |
| 94 | + |
| 95 | + _assertAudit(logs, owner, "compose-hash", composeHash, true); |
| 96 | + _assertAudit(logs, owner, "device", device, true); |
| 97 | + _assertAudit(logs, owner, "allow-any-device", bytes32(0), true); |
| 98 | + _assertAudit(logs, owner, "require-tcb-up-to-date", bytes32(0), true); |
| 99 | + _assertAudit(logs, owner, "device", device, false); |
| 100 | + _assertAudit(logs, owner, "upgrades-disabled", bytes32(0), true); |
| 101 | + |
| 102 | + assertTrue(app.allowedComposeHashes(composeHash)); |
| 103 | + assertFalse(app.allowedDeviceIds(device)); |
| 104 | + assertTrue(app.allowAnyDevice()); |
| 105 | + assertTrue(app.requireTcbUpToDate()); |
| 106 | + } |
| 107 | + |
| 108 | + function test_InvalidMutationEmitsNoAuditAndLeavesNoPartialState() public { |
| 109 | + bytes32 image = keccak256("unauthorized-image"); |
| 110 | + vm.recordLogs(); |
| 111 | + vm.prank(outsider); |
| 112 | + vm.expectRevert(); |
| 113 | + kms.addOsImageHash(image); |
| 114 | + Vm.Log[] memory logs = vm.getRecordedLogs(); |
| 115 | + assertEq(_auditCount(logs), 0); |
| 116 | + assertFalse(kms.allowedOsImages(image)); |
| 117 | + } |
| 118 | + |
| 119 | + function test_ReorgDropsOrphanEventAndCanonicalEventRebuildsState() public { |
| 120 | + bytes32 orphaned = keccak256("orphaned-compose"); |
| 121 | + bytes32 canonical = keccak256("canonical-compose"); |
| 122 | + uint256 snapshot = vm.snapshotState(); |
| 123 | + |
| 124 | + vm.recordLogs(); |
| 125 | + vm.prank(owner); |
| 126 | + app.addComposeHash(orphaned); |
| 127 | + Vm.Log[] memory orphanLogs = vm.getRecordedLogs(); |
| 128 | + _assertAudit(orphanLogs, owner, "compose-hash", orphaned, true); |
| 129 | + assertTrue(app.allowedComposeHashes(orphaned)); |
| 130 | + |
| 131 | + assertTrue(vm.revertToState(snapshot)); |
| 132 | + assertFalse(app.allowedComposeHashes(orphaned)); |
| 133 | + |
| 134 | + vm.recordLogs(); |
| 135 | + vm.prank(owner); |
| 136 | + app.addComposeHash(canonical); |
| 137 | + Vm.Log[] memory canonicalLogs = vm.getRecordedLogs(); |
| 138 | + _assertAudit(canonicalLogs, owner, "compose-hash", canonical, true); |
| 139 | + assertEq(_auditCount(canonicalLogs), 1); |
| 140 | + assertTrue(app.allowedComposeHashes(canonical)); |
| 141 | + assertFalse(app.allowedComposeHashes(orphaned)); |
| 142 | + } |
| 143 | + |
| 144 | + function test_UpgradeAuditIncludesActorAndImplementation() public { |
| 145 | + vm.startPrank(owner); |
| 146 | + DstackApp replacement = new DstackApp(); |
| 147 | + vm.recordLogs(); |
| 148 | + app.upgradeToAndCall(address(replacement), ""); |
| 149 | + Vm.Log[] memory logs = vm.getRecordedLogs(); |
| 150 | + vm.stopPrank(); |
| 151 | + |
| 152 | + _assertAudit(logs, owner, "implementation-upgrade", bytes32(uint256(uint160(address(replacement)))), true); |
| 153 | + assertEq(Upgrades.getImplementationAddress(address(app)), address(replacement)); |
| 154 | + } |
| 155 | + |
| 156 | + function _assertAudit( |
| 157 | + Vm.Log[] memory logs, |
| 158 | + address actor, |
| 159 | + string memory policy, |
| 160 | + bytes32 value, |
| 161 | + bool enabled |
| 162 | + ) |
| 163 | + private |
| 164 | + pure |
| 165 | + { |
| 166 | + bytes32 actorTopic = bytes32(uint256(uint160(actor))); |
| 167 | + bytes32 policyTopic = keccak256(bytes(policy)); |
| 168 | + for (uint256 i = 0; i < logs.length; ++i) { |
| 169 | + Vm.Log memory entry = logs[i]; |
| 170 | + if ( |
| 171 | + entry.topics.length == 4 && entry.topics[0] == AUDIT_TOPIC && entry.topics[1] == actorTopic |
| 172 | + && entry.topics[2] == policyTopic && entry.topics[3] == value |
| 173 | + && abi.decode(entry.data, (bool)) == enabled |
| 174 | + ) { |
| 175 | + return; |
| 176 | + } |
| 177 | + } |
| 178 | + revert("expected policy audit event not found"); |
| 179 | + } |
| 180 | + |
| 181 | + function _auditCount(Vm.Log[] memory logs) private pure returns (uint256 count) { |
| 182 | + for (uint256 i = 0; i < logs.length; ++i) { |
| 183 | + if (logs[i].topics.length == 4 && logs[i].topics[0] == AUDIT_TOPIC) ++count; |
| 184 | + } |
| 185 | + } |
| 186 | +} |
0 commit comments