Skip to content

Commit f311d74

Browse files
committed
move
1 parent 98ed3ac commit f311d74

File tree

7 files changed

+608
-0
lines changed

7 files changed

+608
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Contracts
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
jobs:
9+
contracts:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
with:
14+
submodules: recursive
15+
fetch-depth: 0
16+
- name: Install Foundry
17+
uses: foundry-rs/foundry-toolchain@v1
18+
- name: Run contract tests
19+
run: forge test -vvv

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Compiler files
2+
cache/
3+
out/
4+
5+
# Ignores development broadcast logs
6+
!/broadcast
7+
/broadcast/*/31337/
8+
/broadcast/**/dry-run/
9+
10+
# Docs
11+
docs/
12+
13+
# Dotenv file
14+
.env

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "lib/openzeppelin-contracts-upgradeable"]
2+
path = lib/openzeppelin-contracts-upgradeable
3+
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git

foundry.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[profile.default]
2+
src = "src"
3+
test = "test"
4+
script = "script"
5+
out = "out"
6+
libs = ["lib"]
7+
cache_path = "cache"
8+
broadcast = "broadcast"
9+
via_ir = true
10+
11+
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

src/KeyManager.sol

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
5+
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
6+
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
7+
8+
contract KeyManager is Initializable, OwnableUpgradeable, UUPSUpgradeable {
9+
struct CommitteeMember {
10+
/// @notice public key for consensus votes, also used as the primary label for a node
11+
bytes sigKey;
12+
/// @notice DH public key used for authenticated network messages
13+
bytes dhKey;
14+
/// @notice public key for encrypting DKG-specific payloads
15+
bytes dkgKey;
16+
/// @notice a network address: `ip:port` or `hostname:port`
17+
string networkAddress;
18+
}
19+
20+
/// @notice The consensus committee rotates with each epoch, registered by contract `manager`.
21+
/// @notice Timeboost makes the simplifying decision that this committee is exactly the keyset
22+
struct Committee {
23+
/// @notice unique identifier for the committee, assigned by this contract
24+
uint64 id;
25+
/// @notice wall clock time since unix epoch for this committee to be active
26+
uint64 effectiveTimestamp;
27+
/// @notice constituting members and their key materials
28+
CommitteeMember[] members;
29+
}
30+
31+
/// @notice Emitted when a committee is created.
32+
/// @param id The id of the committee.
33+
event CommitteeCreated(uint64 indexed id);
34+
35+
/// @notice Emitted when the threshold encryption key is set.
36+
/// @param thresholdEncryptionKey The threshold encryption key.
37+
event ThresholdEncryptionKeyUpdated(bytes thresholdEncryptionKey);
38+
39+
/// @notice Emitted when the manager is changed.
40+
/// @param oldManager The old manager.
41+
/// @param newManager The new manager.
42+
event ManagerChanged(address indexed oldManager, address indexed newManager);
43+
44+
/// @notice Emitted when a committee is removed.
45+
/// @param fromId The id of the first committee to prune.
46+
/// @param toId The id of the last committee to prune.
47+
event CommitteesPruned(uint64 indexed fromId, uint64 indexed toId);
48+
49+
/// @notice Thrown when the caller is not the manager.
50+
/// @param caller The address that called the function.
51+
error NotManager(address caller);
52+
53+
/// @notice Thrown when the address is invalid.
54+
error InvalidAddress();
55+
56+
/// @notice Thrown when the threshold encryption key is already set.
57+
error ThresholdEncryptionKeyAlreadySet();
58+
59+
/// @notice Thrown when the committee id does not exist.
60+
/// @param committeeId The id of the committee.
61+
error CommitteeIdDoesNotExist(uint64 committeeId);
62+
/// @notice Thrown when the committee is empty.
63+
error EmptyCommitteeMembers();
64+
/// @notice Thrown when the effective timestamp is invalid.
65+
error InvalidEffectiveTimestamp(uint64 effectiveTimestamp, uint64 lastEffectiveTimestamp);
66+
/// @notice Thrown when there is no committee scheduled.
67+
error NoCommitteeScheduled();
68+
/// @notice Thrown when the committee id overflows.
69+
error CommitteeIdOverflow();
70+
/// @notice Thrown when the committee is too recent to remove.
71+
error CannotRemoveRecentCommittees();
72+
/// @notice Thrown when pruning with invalid range.
73+
error InvalidPruneRange(uint64 upToCommitteeId, uint64 oldestStored, uint64 nextCommitteeId);
74+
75+
/// @notice The threshold encryption key for the committee.
76+
bytes public thresholdEncryptionKey;
77+
/// @notice The mapping of committee ids to committees.
78+
mapping(uint64 => Committee) public committees;
79+
/// @notice The manager of the contract.
80+
address public manager;
81+
/// @notice The next committee id.
82+
uint64 public nextCommitteeId;
83+
/// @notice The oldest committee id still stored in the mapping
84+
uint64 private _oldestStoredCommitteeId;
85+
/// @notice The gap for future upgrades.
86+
uint256[48] private __gap;
87+
88+
/// @notice Modifier to check if the caller is the manager.
89+
modifier onlyManager() {
90+
_onlyManager();
91+
_;
92+
}
93+
94+
/// @notice Internal function to check if the caller is the manager.
95+
function _onlyManager() internal view {
96+
if (msg.sender != manager) {
97+
revert NotManager(msg.sender);
98+
}
99+
}
100+
101+
constructor() {
102+
_disableInitializers();
103+
}
104+
105+
/**
106+
* @notice This function is used to initialize the contract.
107+
* @dev Reverts if the manager is the zero address.
108+
* @dev Assumes that the manager is valid.
109+
* @dev This must be called once when the contract is first deployed.
110+
* @param initialManager The initial manager.
111+
*/
112+
function initialize(address initialManager) external initializer {
113+
if (initialManager == address(0)) {
114+
revert InvalidAddress();
115+
}
116+
__Ownable_init(msg.sender);
117+
__UUPSUpgradeable_init();
118+
manager = initialManager;
119+
}
120+
121+
/**
122+
* @notice This function is used to authorize the upgrade of the contract.
123+
* @dev Reverts if the caller is not the owner.
124+
* @dev Assumes that the new implementation is valid.
125+
* @param newImplementation The new implementation.
126+
*/
127+
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
128+
129+
/**
130+
* @notice This function is used to set the manager.
131+
* @dev Reverts if the manager is the zero address or the same as the current manager.
132+
* @dev Reverts if the caller is not the owner.
133+
* @dev Assumes that the manager is valid.
134+
* @param newManager The new manager.
135+
*/
136+
function setManager(address newManager) external virtual onlyOwner {
137+
if (newManager == address(0) || newManager == manager) {
138+
revert InvalidAddress();
139+
}
140+
address oldManager = manager;
141+
manager = newManager;
142+
emit ManagerChanged(oldManager, newManager);
143+
}
144+
145+
/**
146+
* @notice This function is used to set the threshold encryption key.
147+
* @dev Reverts if the threshold encryption key is already set.
148+
* @dev Reverts if the caller is not the manager.
149+
* @dev Assumes that the threshold encryption key is valid.
150+
* @param newThresholdEncryptionKey The threshold encryption key.
151+
*/
152+
function setThresholdEncryptionKey(bytes calldata newThresholdEncryptionKey) external virtual onlyManager {
153+
if (thresholdEncryptionKey.length > 0) {
154+
revert ThresholdEncryptionKeyAlreadySet();
155+
}
156+
thresholdEncryptionKey = newThresholdEncryptionKey;
157+
emit ThresholdEncryptionKeyUpdated(newThresholdEncryptionKey);
158+
}
159+
160+
/**
161+
* @notice This function is used to set the next committee.
162+
* @dev Reverts if the members array is empty.
163+
* @dev Reverts if the effective timestamp is less than the last effective timestamp.
164+
* @dev Reverts if the committees mapping is at uint64.max.
165+
* @dev Assumes that the committee members are valid.
166+
* @param effectiveTimestamp The effective timestamp of the committee.
167+
* @param members The committee members.
168+
* @return committeeId The id of the new committee.
169+
*/
170+
function setNextCommittee(uint64 effectiveTimestamp, CommitteeMember[] calldata members)
171+
external
172+
virtual
173+
onlyManager
174+
returns (uint64 committeeId)
175+
{
176+
if (members.length == 0) {
177+
revert EmptyCommitteeMembers();
178+
}
179+
180+
// ensure the effective timestamp is greater than the last effective timestamp
181+
if (nextCommitteeId > 0) {
182+
uint64 lastTimestamp = committees[nextCommitteeId - 1].effectiveTimestamp;
183+
if (effectiveTimestamp <= lastTimestamp) {
184+
revert InvalidEffectiveTimestamp(effectiveTimestamp, lastTimestamp);
185+
}
186+
}
187+
188+
if (nextCommitteeId == type(uint64).max) revert CommitteeIdOverflow();
189+
190+
committees[nextCommitteeId] =
191+
Committee({id: nextCommitteeId, effectiveTimestamp: effectiveTimestamp, members: members});
192+
193+
nextCommitteeId++;
194+
195+
emit CommitteeCreated(nextCommitteeId - 1);
196+
return nextCommitteeId - 1;
197+
}
198+
199+
/**
200+
* @notice This function is used to get the committee by id.
201+
* @dev Reverts if the id is greater than the length of the committees mapping.
202+
* @dev Reverts if the id is less than the head committee id.
203+
* @param id The id of the committee.
204+
* @return committee The committee.
205+
*/
206+
function getCommitteeById(uint64 id) external view virtual returns (Committee memory committee) {
207+
if (id < _oldestStoredCommitteeId || committees[id].id != id) {
208+
revert CommitteeIdDoesNotExist(id);
209+
}
210+
211+
return committees[id];
212+
}
213+
214+
/**
215+
* @notice This function is used to get the current committee id.
216+
* @dev Reverts if there is no committee scheduled at the current timestamp.
217+
* @dev Searches backwards through existing committees to find the active one.
218+
* @return committeeId The current committee id.
219+
*/
220+
function currentCommitteeId() public view virtual returns (uint64 committeeId) {
221+
uint64 currentTimestamp = uint64(block.timestamp);
222+
223+
if (nextCommitteeId == 0 || _oldestStoredCommitteeId >= nextCommitteeId) {
224+
revert NoCommitteeScheduled();
225+
}
226+
227+
// Search backwards from most recent committee to oldest stored
228+
uint64 currCommitteeId = nextCommitteeId - 1;
229+
while (currCommitteeId >= _oldestStoredCommitteeId) {
230+
if (currentTimestamp >= committees[currCommitteeId].effectiveTimestamp) {
231+
return currCommitteeId;
232+
}
233+
234+
if (currCommitteeId == 0) {
235+
break;
236+
}
237+
238+
currCommitteeId--;
239+
}
240+
241+
revert NoCommitteeScheduled();
242+
}
243+
244+
/**
245+
* @notice Prunes all committees from _oldestStoredCommitteeId up to and including upToCommitteeId.
246+
* @dev This matches timeboost's garbage collection behavior of removing old committees in bulk.
247+
* @dev Reverts if upToCommitteeId is not in a valid range for pruning.
248+
* @dev Reverts if any committee in the range became effective within the last 10 minutes.
249+
* @param upToCommitteeId The highest committee ID to prune (inclusive).
250+
*/
251+
function pruneUntil(uint64 upToCommitteeId) external virtual onlyManager {
252+
if (upToCommitteeId < _oldestStoredCommitteeId || upToCommitteeId >= nextCommitteeId) {
253+
revert InvalidPruneRange(upToCommitteeId, _oldestStoredCommitteeId, nextCommitteeId);
254+
}
255+
256+
// Delete all committees in range
257+
uint64 cutOffTime = uint64(block.timestamp - 10 minutes);
258+
uint64 oldOldestStored = _oldestStoredCommitteeId;
259+
for (uint64 id = _oldestStoredCommitteeId; id <= upToCommitteeId; id++) {
260+
if (committees[id].effectiveTimestamp >= cutOffTime) {
261+
revert CannotRemoveRecentCommittees();
262+
}
263+
delete committees[id];
264+
}
265+
266+
_oldestStoredCommitteeId = upToCommitteeId + 1;
267+
268+
emit CommitteesPruned(oldOldestStored, upToCommitteeId);
269+
}
270+
}

0 commit comments

Comments
 (0)