|
2 | 2 | pragma solidity 0.8.29; |
3 | 3 |
|
4 | 4 | import {Ownable2Step} from "openzeppelin/contracts/access/Ownable2Step.sol"; |
| 5 | +import {Strings} from "openzeppelin/contracts/utils/Strings.sol"; |
5 | 6 |
|
6 | 7 | import {ISnapchainConfigRegistry} from "./interfaces/ISnapchainConfigRegistry.sol"; |
7 | 8 |
|
@@ -58,6 +59,18 @@ contract SnapchainConfigRegistry is ISnapchainConfigRegistry, Ownable2Step { |
58 | 59 | */ |
59 | 60 | uint256 internal constant _PEER_CHARS_HI = 0x07FFFFFE87FFFFFE; |
60 | 61 |
|
| 62 | + /** |
| 63 | + * @dev Lowercase hex alphabet, left-aligned in a word so that a nibble indexes it directly via |
| 64 | + * the `byte` opcode, which counts from the most significant byte. |
| 65 | + */ |
| 66 | + uint256 internal constant _HEX_SYMBOLS_ALIGNED = 0x3031323334353637383961626364656600000000000000000000000000000000; |
| 67 | + |
| 68 | + /** |
| 69 | + * @dev Bytes in one rendered key line: two spaces, a quote, 64 hex characters, a quote, a |
| 70 | + * comma, and a newline. |
| 71 | + */ |
| 72 | + uint256 internal constant _KEY_LINE_LENGTH = 70; |
| 73 | + |
61 | 74 | /*////////////////////////////////////////////////////////////// |
62 | 75 | STORAGE |
63 | 76 | //////////////////////////////////////////////////////////////*/ |
@@ -136,6 +149,31 @@ contract SnapchainConfigRegistry is ISnapchainConfigRegistry, Ownable2Step { |
136 | 149 | return sets; |
137 | 150 | } |
138 | 151 |
|
| 152 | + /*////////////////////////////////////////////////////////////// |
| 153 | + TOML RENDERING |
| 154 | + //////////////////////////////////////////////////////////////*/ |
| 155 | + |
| 156 | + /** |
| 157 | + * @inheritdoc ISnapchainConfigRegistry |
| 158 | + */ |
| 159 | + function configToml() external view returns (string memory) { |
| 160 | + return string(_rangeToml(0, _validatorSets.length, true)); |
| 161 | + } |
| 162 | + |
| 163 | + /** |
| 164 | + * @inheritdoc ISnapchainConfigRegistry |
| 165 | + */ |
| 166 | + function validatorSetsToml(uint256 start, uint256 end) external view returns (string memory) { |
| 167 | + return string(_rangeToml(start, end, false)); |
| 168 | + } |
| 169 | + |
| 170 | + /** |
| 171 | + * @inheritdoc ISnapchainConfigRegistry |
| 172 | + */ |
| 173 | + function peersToml() public view returns (string memory) { |
| 174 | + return string(_peersToml()); |
| 175 | + } |
| 176 | + |
139 | 177 | /*////////////////////////////////////////////////////////////// |
140 | 178 | PERMISSIONED ACTIONS |
141 | 179 | //////////////////////////////////////////////////////////////*/ |
@@ -219,7 +257,148 @@ contract SnapchainConfigRegistry is ISnapchainConfigRegistry, Ownable2Step { |
219 | 257 | } |
220 | 258 |
|
221 | 259 | /*////////////////////////////////////////////////////////////// |
222 | | - HELPERS |
| 260 | + RENDERING HELPERS |
| 261 | + //////////////////////////////////////////////////////////////*/ |
| 262 | + |
| 263 | + /** |
| 264 | + * @dev Render validator sets `[start, end)`, optionally followed by the `[gossip]` table. |
| 265 | + * |
| 266 | + * Assembly is two-level and deliberately so. Each entry is built on its own, and the |
| 267 | + * entries are joined into the result exactly once. A flat one-concat-per-line loop would |
| 268 | + * be quadratic in output bytes, and the EVM's quadratic memory term compounds it: at a |
| 269 | + * hundred entries that difference is the getter working versus exceeding every public |
| 270 | + * eth_call gas cap. |
| 271 | + */ |
| 272 | + function _rangeToml(uint256 start, uint256 end, bool includePeers) internal view returns (bytes memory) { |
| 273 | + if (start > end || end > _validatorSets.length) revert InvalidRange(); |
| 274 | + |
| 275 | + uint256 count = end - start; |
| 276 | + bytes[] memory parts = new bytes[](includePeers ? count + 1 : count); |
| 277 | + for (uint256 i; i < count; ++i) { |
| 278 | + parts[i] = _validatorSetToml(_validatorSets[start + i]); |
| 279 | + } |
| 280 | + if (includePeers) parts[count] = _peersToml(); |
| 281 | + |
| 282 | + return _join(parts); |
| 283 | + } |
| 284 | + |
| 285 | + /** |
| 286 | + * @dev Render one validator set as a `[[consensus.validator_sets]]` block. |
| 287 | + * |
| 288 | + * The block carries its own trailing blank line rather than sitting between separators. |
| 289 | + * That is what makes paginated ranges concatenate into exactly the unpaginated document. |
| 290 | + */ |
| 291 | + function _validatorSetToml( |
| 292 | + ValidatorSet storage validatorSet |
| 293 | + ) internal view returns (bytes memory) { |
| 294 | + uint32[] storage shardIds = validatorSet.shardIds; |
| 295 | + bytes32[] storage publicKeys = validatorSet.validatorPublicKeys; |
| 296 | + |
| 297 | + // Bounded at MAX_SHARD_IDS, so the quadratic term here is irrelevant. |
| 298 | + bytes memory shardList = bytes(Strings.toString(shardIds[0])); |
| 299 | + for (uint256 i = 1; i < shardIds.length; ++i) { |
| 300 | + shardList = bytes.concat(shardList, ", ", bytes(Strings.toString(shardIds[i]))); |
| 301 | + } |
| 302 | + |
| 303 | + uint256 keyCount = publicKeys.length; |
| 304 | + bytes memory keyLines = new bytes(keyCount * _KEY_LINE_LENGTH); |
| 305 | + for (uint256 i; i < keyCount; ++i) { |
| 306 | + _writeKeyLine(keyLines, i * _KEY_LINE_LENGTH, publicKeys[i]); |
| 307 | + } |
| 308 | + |
| 309 | + return bytes.concat( |
| 310 | + "[[consensus.validator_sets]]\n", |
| 311 | + "effective_at = ", |
| 312 | + bytes(Strings.toString(validatorSet.effectiveAt)), |
| 313 | + "\n", |
| 314 | + "shard_ids = [", |
| 315 | + shardList, |
| 316 | + "]\n", |
| 317 | + "validator_public_keys = [\n", |
| 318 | + keyLines, |
| 319 | + "]\n\n" |
| 320 | + ); |
| 321 | + } |
| 322 | + |
| 323 | + /** |
| 324 | + * @dev Render the `[gossip]` table. Both values are safe to interpolate unescaped because the |
| 325 | + * setters reject every byte that could close the string literal. |
| 326 | + */ |
| 327 | + function _peersToml() internal view returns (bytes memory) { |
| 328 | + return bytes.concat( |
| 329 | + "[gossip]\n", |
| 330 | + "bootstrap_peers = \"", |
| 331 | + bytes(bootstrapPeers), |
| 332 | + "\"\n", |
| 333 | + "direct_peers = \"", |
| 334 | + bytes(directPeers), |
| 335 | + "\"\n" |
| 336 | + ); |
| 337 | + } |
| 338 | + |
| 339 | + /** |
| 340 | + * @dev Concatenate `parts` into one buffer with a single allocation and one copy per part. |
| 341 | + */ |
| 342 | + function _join( |
| 343 | + bytes[] memory parts |
| 344 | + ) internal pure returns (bytes memory) { |
| 345 | + uint256 total; |
| 346 | + for (uint256 i; i < parts.length; ++i) { |
| 347 | + total += parts[i].length; |
| 348 | + } |
| 349 | + |
| 350 | + bytes memory out = new bytes(total); |
| 351 | + uint256 offset; |
| 352 | + for (uint256 i; i < parts.length; ++i) { |
| 353 | + bytes memory part = parts[i]; |
| 354 | + uint256 length = part.length; |
| 355 | + assembly ("memory-safe") { |
| 356 | + mcopy(add(add(out, 0x20), offset), add(part, 0x20), length) |
| 357 | + } |
| 358 | + offset += length; |
| 359 | + } |
| 360 | + return out; |
| 361 | + } |
| 362 | + |
| 363 | + /** |
| 364 | + * @dev Write one ` "<64 hex chars>",\n` line into `out` at `offset`. |
| 365 | + * |
| 366 | + * Written straight into the caller's buffer rather than returned as a fresh string, and in |
| 367 | + * assembly rather than through indexed writes to a `bytes memory`. Both matter: this runs |
| 368 | + * once per key per call, and the Solidity version -- an allocation, 64 bounds-checked |
| 369 | + * single-byte writes, then a concat that copies the result again -- costs roughly an order |
| 370 | + * of magnitude more. That difference is what decides whether the getter fits inside a |
| 371 | + * public RPC's eth_call gas cap once the history grows. |
| 372 | + */ |
| 373 | + function _writeKeyLine(bytes memory out, uint256 offset, bytes32 key) internal pure { |
| 374 | + assembly ("memory-safe") { |
| 375 | + let ptr := add(add(out, 0x20), offset) |
| 376 | + |
| 377 | + // ` "` |
| 378 | + mstore8(ptr, 0x20) |
| 379 | + mstore8(add(ptr, 1), 0x20) |
| 380 | + mstore8(add(ptr, 2), 0x22) |
| 381 | + |
| 382 | + // byte(n, symbols) indexes from the most significant byte, so the alphabet is |
| 383 | + // left-aligned in the word and a nibble indexes it directly. |
| 384 | + let symbols := _HEX_SYMBOLS_ALIGNED |
| 385 | + let hexPtr := add(ptr, 3) |
| 386 | + for { let i := 0 } lt(i, 32) { i := add(i, 1) } { |
| 387 | + let word := byte(i, key) |
| 388 | + let pos := add(hexPtr, mul(i, 2)) |
| 389 | + mstore8(pos, byte(shr(4, word), symbols)) |
| 390 | + mstore8(add(pos, 1), byte(and(word, 0x0F), symbols)) |
| 391 | + } |
| 392 | + |
| 393 | + // `",\n` |
| 394 | + mstore8(add(ptr, 67), 0x22) |
| 395 | + mstore8(add(ptr, 68), 0x2C) |
| 396 | + mstore8(add(ptr, 69), 0x0A) |
| 397 | + } |
| 398 | + } |
| 399 | + |
| 400 | + /*////////////////////////////////////////////////////////////// |
| 401 | + MUTATION HELPERS |
223 | 402 | //////////////////////////////////////////////////////////////*/ |
224 | 403 |
|
225 | 404 | /** |
|
0 commit comments