Skip to content

Commit 63fc5d9

Browse files
author
baku-ccron
committed
Merge remote-tracking branch 'origin/main' into fix-192
2 parents 6bdd786 + fcbeabc commit 63fc5d9

21 files changed

Lines changed: 913 additions & 128 deletions

‎README.md‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,15 @@ Suites are a **registry the abstract iterates**, not a chain of `else if`.
101101
Adding a suite is adding an array entry. A mistyped `DEPLOYMENT_SUITE` reports
102102
the valid keys built from that same array, so the error cannot fall behind the
103103
suites it describes, and keys are checked unique because the key is what selects
104-
what gets broadcast. They are checked **non-empty** for the same reason from the
105-
other side: the empty string is what an unset `DEPLOYMENT_SUITE` arrives as, so
106-
leaving it declarable would let a dispatch with the suite input blank select
107-
something instead of reporting that it was told nothing.
104+
what gets broadcast. They are also checked against an **alphabet**: kebab case,
105+
optionally followed by `@` and a release tag, which is the shape a repo writes
106+
by hand and the shape a cut release generates. A key outside it is one the
107+
reported list cannot be read back as — a comma or a space in a key renders as
108+
two keys and sends a reader after one that is declared nowhere — and the empty
109+
key falls to the same rule, which matters most: the empty string is what an
110+
unset `DEPLOYMENT_SUITE` arrives as, so leaving it declarable would let a
111+
dispatch with the suite input blank select something instead of reporting that
112+
it was told nothing.
108113

109114
Every suite is individually selectable, including a frozen release — which is
110115
how a snapshot from before a network existed reaches that network.

‎script/Build.sol‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,28 +59,34 @@ contract Build is BuildScript, RegistryDeploySuites {
5959
return names;
6060
}
6161

62+
/// The directory every generated lib is written into.
63+
///
64+
/// Overridable for the reason `BuildScript.recordRoot` is: a hook that can
65+
/// only be pointed at the committed tree can only be RUN by overwriting
66+
/// files the rest of the suite compiles and reads, and forge runs test
67+
/// contracts in parallel. Only the per-contract libs and the aggregate go
68+
/// here — `regenerateSnapshots` has no equivalent, because `LibFs` confines
69+
/// every snapshot it writes to `src/generated/`.
70+
/// @return The lib directory.
71+
function libDir() internal view virtual returns (string memory) {
72+
return LibRainDeploySnapshot.LIB_DIR;
73+
}
74+
6275
/// @inheritdoc BuildScript
6376
/// @dev Every alias lib, every released-suites lib and the aggregate over
6477
/// them.
6578
function regenerateLibs() internal override {
6679
GeneratedContract[] memory contracts = generatedContracts();
80+
string memory dir = libDir();
6781
for (uint256 i = 0; i < contracts.length; i++) {
6882
LibRainDeploySnapshot.writeAliasLib(
69-
vm,
70-
LibRainDeploySnapshot.LIB_DIR,
71-
contracts[i].contractName,
72-
contracts[i].constantPrefix,
73-
LibRainDeploySnapshot.CANDIDATE
83+
vm, dir, contracts[i].contractName, contracts[i].constantPrefix, LibRainDeploySnapshot.CANDIDATE
7484
);
7585
LibRainDeploySnapshot.writeReleasedSuitesLib(
76-
vm,
77-
LibRainDeploySnapshot.LIB_DIR,
78-
recordRoot(),
79-
contracts[i].contractName,
80-
contracts[i].candidate.snapshot
86+
vm, dir, recordRoot(), contracts[i].contractName, contracts[i].candidate.snapshot
8187
);
8288
}
83-
LibRainDeploySnapshot.writeReleasedSuitesAggregate(vm, LibRainDeploySnapshot.LIB_DIR, snapshotContractNames());
89+
LibRainDeploySnapshot.writeReleasedSuitesAggregate(vm, dir, snapshotContractNames());
8490
}
8591

8692
/// @inheritdoc BuildScript
@@ -89,6 +95,7 @@ contract Build is BuildScript, RegistryDeploySuites {
8995
for (uint256 i = 0; i < contracts.length; i++) {
9096
LibRainDeploySnapshot.writeSnapshot(
9197
vm,
98+
recordRoot(),
9299
LibRainDeploySnapshot.CANDIDATE,
93100
contracts[i].contractName,
94101
contracts[i].candidate.sourceCreationCode,

‎src/abstract/RainDeploySuitesBase.sol‎

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,46 @@ pragma solidity ^0.8.25;
77
/// @param suite The key declared more than once.
88
error DuplicateDeploySuite(string suite);
99

10-
/// Thrown when a suite declares an EMPTY key.
10+
/// Thrown when a suite declares a key outside the key alphabet.
1111
///
12-
/// The empty string is the absent-sentinel: `RainDeployBroadcast.run()` reads
13-
/// `DEPLOYMENT_SUITE` through `vm.envOr` with `string("")` as the default, so a
14-
/// dispatch that left the suite input blank asks the registry for exactly this
15-
/// key. A declaration free to answer it turns "told nothing" into a selection,
16-
/// and `CREATE2` under a zero salt puts those bytes at their own permanent
17-
/// address on every chain that dispatch reached.
12+
/// A key is a NAME, or a name and a release TAG joined by an at sign: the name
13+
/// lowercase letters and hyphens, the tag those plus digits and underscores,
14+
/// neither half empty and at most one at sign. Digits and underscores after the
15+
/// at sign only, which is where a tag needs them and where nothing else does.
1816
///
19-
/// Reserved on the DECLARATION rather than beside the substitution, for the
20-
/// reason `NoDeployCandidates` is: a rule bound in the consumer is a rule most
17+
/// That is the kebab case a repo declares by hand, and it is what
18+
/// `LibRainDeploySnapshot` emits a released entry as — the candidate key the
19+
/// release was cut from, the at sign, and the record directory's tag, which
20+
/// `isTag` already holds to `X_Y_Z` — so a generated declaration needs no
21+
/// exemption from the rule a hand written one is held to. The at sign cannot
22+
/// appear in a name, so that emission carries exactly one however many releases
23+
/// a repo cuts.
24+
///
25+
/// The key list is why an alphabet exists at all. `suiteNames()` joins the
26+
/// declared keys on `", "` for `UnknownDeploymentSuite`, which exists so a
27+
/// caller who does NOT already know the valid keys is told them; a key free to
28+
/// carry either of those characters renders as two, so the reader is told a
29+
/// different number of suites exist than do and is sent after a key that is
30+
/// declared nowhere. A list that cannot be read back as the set it names is the
31+
/// hardcoded string the registry exists to replace, spelled differently.
32+
///
33+
/// The EMPTY key is refused by the same rule, and is the one refusal with a
34+
/// broadcast behind it: it is the value `RainDeployBroadcast.run()` substitutes
35+
/// for an absent `DEPLOYMENT_SUITE`, so a declaration allowed to answer it
36+
/// turns "told nothing" into a selection, and `CREATE2` under a zero salt puts
37+
/// those bytes at their own permanent address on every chain that dispatch
38+
/// reached. An alphabet admitting no zero byte key already says that; a length
39+
/// check beside it would be a second rule for one property, free to drift.
40+
///
41+
/// Held on the DECLARATION rather than beside the substitution, for the reason
42+
/// `NoDeployCandidates` is: a rule bound in the consumer is a rule most
2143
/// consumers do not run, and here every `suiteByName` caller pays for it rather
22-
/// than only `run()`. Uniqueness does not already cover it — a lone empty key
23-
/// collides with nothing.
44+
/// than only `run()`.
2445
/// @param index Position in `allSuites()`: the released suites in declaration
25-
/// order, then the candidates. The key itself names nothing, so the position is
46+
/// order, then the candidates. An empty key names nothing, so the position is
2647
/// the only thing that can.
27-
error EmptyDeploySuiteKey(uint256 index);
48+
/// @param suite The refused key.
49+
error InvalidDeploySuiteKey(uint256 index, string suite);
2850

2951
/// Thrown when `DEPLOYMENT_SUITE` names no declared suite. Carries the valid
3052
/// keys, because the whole point of a registry is that the answer is not one
@@ -79,11 +101,11 @@ error CandidateSourceMismatch(string suite, bytes32 storedCreationCodeHash, byte
79101
/// make that comparison derived-against-derived, and a guard that compares a
80102
/// value to itself is not a guard.
81103
struct DeploySuite {
82-
/// The key. Unique across every suite a repo declares, and never empty: it
83-
/// is what `DEPLOYMENT_SUITE` selects for broadcasting, and the label every
84-
/// verification error names. The empty string is the value an unset
85-
/// `DEPLOYMENT_SUITE` arrives as, so it is reserved rather than declarable
86-
/// — see `EmptyDeploySuiteKey`.
104+
/// The key. Unique across every suite a repo declares, and held to the key
105+
/// alphabet — see `InvalidDeploySuiteKey`. It is what
106+
/// `DEPLOYMENT_SUITE` selects for broadcasting, the label every
107+
/// verification error names, and what the valid-key list an unknown suite
108+
/// reports has to read back as.
87109
///
88110
/// A repo with one contract and several frozen releases gives each release
89111
/// its own key, because each is separately deployable — a chain added after
@@ -253,6 +275,43 @@ abstract contract RainDeploySuitesBase {
253275
}
254276
}
255277

278+
/// Refuses a key the registry cannot carry — see `InvalidDeploySuiteKey`.
279+
///
280+
/// Held against the WHOLE key rather than against a name a released key is
281+
/// derived from, because `releasedSuites()` declares finished keys: a rule
282+
/// that only knew the name half could not be asked about a released entry
283+
/// at all, and this is the one place every key a repo declares is read.
284+
/// @param index Position in `allSuites()`, for the refusal to name.
285+
/// @param suite The key to check.
286+
function checkSuiteKey(uint256 index, string memory suite) internal pure {
287+
bytes memory key = bytes(suite);
288+
// Where the tag starts, and zero until an `@` is seen. Zero is not a
289+
// position a tag can start at, because an `@` opening the key leaves
290+
// the name half empty and is refused below.
291+
uint256 tagStart = 0;
292+
293+
for (uint256 i = 0; i < key.length; i++) {
294+
bytes1 char = key[i];
295+
if (char == "@") {
296+
if (i == 0 || tagStart != 0) {
297+
revert InvalidDeploySuiteKey(index, suite);
298+
}
299+
tagStart = i + 1;
300+
} else {
301+
bool nameAlphabet = (char >= "a" && char <= "z") || char == "-";
302+
bool tagAlphabet = (char >= "0" && char <= "9") || char == "_";
303+
if (!(nameAlphabet || (tagStart != 0 && tagAlphabet))) {
304+
revert InvalidDeploySuiteKey(index, suite);
305+
}
306+
}
307+
}
308+
309+
// The empty key and a trailing at sign: the loop only refuses bytes that are there.
310+
if (tagStart == key.length) {
311+
revert InvalidDeploySuiteKey(index, suite);
312+
}
313+
}
314+
256315
/// Every suite this repo declares: the released ones followed by the
257316
/// candidates. This is the verification set and the deploy registry, which
258317
/// are the same set because they are the same declaration.
@@ -261,13 +320,13 @@ abstract contract RainDeploySuitesBase {
261320
/// pay for the check and neither can be handed a registry that is ambiguous
262321
/// or that answers the absent-sentinel. One pass over the whole set, so a
263322
/// candidate colliding with another candidate is caught by the same code
264-
/// that catches a candidate colliding with a release, and an empty key is
265-
/// refused wherever in the declaration it was spelled — there is no second
266-
/// rule to keep in step.
323+
/// that catches a candidate colliding with a release, and a key the
324+
/// alphabet refuses is refused wherever in the declaration it was spelled —
325+
/// there is no second rule to keep in step.
267326
///
268-
/// Unique AND non-empty, because neither implies the other: a lone empty
269-
/// key collides with nothing, and it is the one key `run()` can be handed
270-
/// by accident.
327+
/// Unique AND in the alphabet, because neither implies the other: a lone
328+
/// unreadable key collides with nothing, and two keys can be spelled
329+
/// perfectly and still be the same key.
271330
/// @return Every declared suite.
272331
function allSuites() internal pure returns (DeploySuite[] memory) {
273332
DeploySuite[] memory released = releasedSuites();
@@ -282,9 +341,7 @@ abstract contract RainDeploySuitesBase {
282341
}
283342

284343
for (uint256 i = 0; i < suites.length; i++) {
285-
if (bytes(suites[i].suite).length == 0) {
286-
revert EmptyDeploySuiteKey(i);
287-
}
344+
checkSuiteKey(i, suites[i].suite);
288345
for (uint256 j = i + 1; j < suites.length; j++) {
289346
if (keccak256(bytes(suites[i].suite)) == keccak256(bytes(suites[j].suite))) {
290347
revert DuplicateDeploySuite(suites[i].suite);
@@ -296,6 +353,10 @@ abstract contract RainDeploySuitesBase {
296353
}
297354

298355
/// Every declared key, comma separated, for the unknown-suite error.
356+
///
357+
/// Splitting the result on `", "` recovers exactly the declared keys,
358+
/// because the alphabet `allSuites` holds them to carries neither of those
359+
/// two characters anywhere but between two keys.
299360
/// @return The declared keys.
300361
function suiteNames() internal pure returns (string memory) {
301362
DeploySuite[] memory suites = allSuites();

0 commit comments

Comments
 (0)