Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

all: v2 refactoring to use feed byte identifier #23

Merged
merged 19 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export ECDSA_VS="[]"
export ECDSA_RS="[]"
export ECDSA_SS="[]"
## IScribe::drop
export FEED_INDEX=
export FEED_ID=
## IScribeOptimistic::setOpChallengePeriod
export OP_CHALLENGE_PERIOD=
## IScribeOptimistic::setMaxChallengeReward
Expand All @@ -51,4 +51,4 @@ export TEST_POKE_VAL=
export TEST_POKE_AGE=
export TEST_SCHNORR_SIGNATURE=
export TEST_SCHNORR_COMMITMENT=
export TEST_SCHNORR_SIGNERS_BLOB=
export TEST_SCHNORR_FEED_IDS=
134 changes: 63 additions & 71 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,77 +1,69 @@
LibBytesTest:test_getByteAtIndex() (gas: 378)
LibSecp256k1Test:testVectors_addAffinePoint() (gas: 2502307)
LibSecp256k1Test:test_isZeroPoint() (gas: 465)
LibSecp256k1Test:test_yParity() (gas: 530)
ScribeInvariantTest:invariant_bar_IsNeverZero() (runs: 256, calls: 3840, reverts: 0)
ScribeInvariantTest:invariant_feeds_ImageIsZeroToLengthOfPubKeys() (runs: 256, calls: 3840, reverts: 0)
ScribeInvariantTest:invariant_feeds_LinkToTheirPublicKeys() (runs: 256, calls: 3840, reverts: 0)
ScribeInvariantTest:invariant_poke_PokeTimestampsAreStrictlyMonotonicallyIncreasing() (runs: 256, calls: 3840, reverts: 0)
ScribeInvariantTest:invariant_pubKeys_AtIndexZeroIsZeroPoint() (runs: 256, calls: 3840, reverts: 0)
ScribeInvariantTest:invariant_pubKeys_LengthIsStrictlyMonotonicallyIncreasing() (runs: 256, calls: 3840, reverts: 0)
ScribeInvariantTest:invariant_pubKeys_NonZeroPubKeyExistsAtMostOnce() (runs: 256, calls: 3840, reverts: 0)
ScribeInvariantTest:invariant_pubKeys_ZeroPointIsNeverAddedAsPubKey() (runs: 256, calls: 3840, reverts: 0)
ScribeOptimisticTest:test_Deployment() (gas: 59359)
ScribeOptimisticTest:test_afterAuthedAction_1_drop() (gas: 248804)
ScribeOptimisticTest:test_afterAuthedAction_1_setBar() (gas: 295659)
ScribeOptimisticTest:test_afterAuthedAction_1_setChallengePeriod() (gas: 294180)
ScribeOptimisticTest:test_afterAuthedAction_2_drop() (gas: 325741)
ScribeOptimisticTest:test_afterAuthedAction_2_setBar() (gas: 384823)
ScribeOptimisticTest:test_afterAuthedAction_2_setChallengePeriod() (gas: 384423)
ScribeOptimisticTest:test_afterAuthedAction_3_drop() (gas: 251454)
ScribeOptimisticTest:test_afterAuthedAction_3_setBar() (gas: 298843)
ScribeOptimisticTest:test_afterAuthedAction_3_setChallengePeriod() (gas: 298316)
ScribeOptimisticTest:test_afterAuthedAction_4_drop() (gas: 327250)
ScribeOptimisticTest:test_afterAuthedAction_4_setBar() (gas: 386044)
ScribeOptimisticTest:test_afterAuthedAction_4_setChallengePeriod() (gas: 384786)
ScribeOptimisticTest:test_drop_IndexZero() (gas: 44243)
ScribeOptimisticTest:test_drop_Multiple_IsAuthProtected() (gas: 14770)
ScribeOptimisticTest:test_drop_Single_IsAuthProtected() (gas: 12782)
ScribeOptimisticTest:test_latestAnswer_isTollProtected() (gas: 12505)
ScribeInvariantTest:invariant_pubKeys_CannotIndexOutOfBoundsViaUint8Index() (runs: 256, calls: 3840, reverts: 0)
ScribeInvariantTest:invariant_pubKeys_IndexedViaFeedId() (runs: 256, calls: 3840, reverts: 0)
ScribeOptimisticTest:test_Deployment() (gas: 1204337)
ScribeOptimisticTest:test_afterAuthedAction_1_drop() (gas: 202463)
ScribeOptimisticTest:test_afterAuthedAction_1_setBar() (gas: 238545)
ScribeOptimisticTest:test_afterAuthedAction_1_setChallengePeriod() (gas: 237041)
ScribeOptimisticTest:test_afterAuthedAction_2_drop() (gas: 285432)
ScribeOptimisticTest:test_afterAuthedAction_2_setBar() (gas: 325402)
ScribeOptimisticTest:test_afterAuthedAction_2_setChallengePeriod() (gas: 324955)
ScribeOptimisticTest:test_afterAuthedAction_3_drop() (gas: 205138)
ScribeOptimisticTest:test_afterAuthedAction_3_setBar() (gas: 241782)
ScribeOptimisticTest:test_afterAuthedAction_3_setChallengePeriod() (gas: 241184)
ScribeOptimisticTest:test_afterAuthedAction_4_drop() (gas: 286761)
ScribeOptimisticTest:test_afterAuthedAction_4_setBar() (gas: 326467)
ScribeOptimisticTest:test_afterAuthedAction_4_setChallengePeriod() (gas: 325228)
ScribeOptimisticTest:test_drop_Multiple_IsAuthProtected() (gas: 14650)
ScribeOptimisticTest:test_drop_Single_IsAuthProtected() (gas: 13533)
ScribeOptimisticTest:test_latestAnswer_isTollProtected() (gas: 12518)
ScribeOptimisticTest:test_latestRoundData_isTollProtected() (gas: 14030)
ScribeOptimisticTest:test_lift_Multiple_FailsIf_ECDSADataInvalid() (gas: 106973)
ScribeOptimisticTest:test_lift_Multiple_FailsIf_MaxFeedsReached() (gas: 20286949)
ScribeOptimisticTest:test_lift_Multiple_IsAuthProtected() (gas: 16490)
ScribeOptimisticTest:test_lift_Single_FailsIf_ECDSADataInvalid() (gas: 22135)
ScribeOptimisticTest:test_lift_Single_FailsIf_MaxFeedsReached() (gas: 19731489)
ScribeOptimisticTest:test_lift_Single_IsAuthProtected() (gas: 12905)
ScribeOptimisticTest:test_opChallenge_FailsIf_CalledSubsequently() (gas: 311729)
ScribeOptimisticTest:test_opChallenge_FailsIf_InvalidSchnorrDataGiven() (gas: 283298)
ScribeOptimisticTest:test_opChallenge_FailsIf_NoOpPokeToChallenge() (gas: 14897)
ScribeOptimisticTest:test_peek_isTollProtected() (gas: 12870)
ScribeOptimisticTest:test_peep_isTollProtected() (gas: 13200)
ScribeOptimisticTest:test_poke_Initial_FailsIf_AgeIsZero() (gas: 242775)
ScribeOptimisticTest:test_readWithAge_isTollProtected() (gas: 12357)
ScribeOptimisticTest:test_read_isTollProtected() (gas: 11823)
ScribeOptimisticTest:test_setBar_FailsIf_BarIsZero() (gas: 11793)
ScribeOptimisticTest:test_setBar_IsAuthProtected() (gas: 13730)
ScribeOptimisticTest:test_setMaxChallengeReward_IsAuthProtected() (gas: 13730)
ScribeOptimisticTest:test_setOpChallengePeriod_DropsFinalizedOpPoke_If_NonFinalizedAfterUpdate() (gas: 299764)
ScribeOptimisticTest:test_setOpChallengePeriod_FailsIf_OpChallengePeriodIsZero() (gas: 11567)
ScribeOptimisticTest:test_setOpChallengePeriod_IsAuthProtected() (gas: 12332)
ScribeOptimisticTest:test_toll_diss_IsAuthProtected() (gas: 13260)
ScribeOptimisticTest:test_toll_kiss_IsAuthProtected() (gas: 12419)
ScribeOptimisticTest:test_tryReadWithAge_isTollProtected() (gas: 13746)
ScribeOptimisticTest:test_tryRead_isTollProtected() (gas: 12986)
ScribeTest:test_Deployment() (gas: 42326)
ScribeTest:test_drop_IndexZero() (gas: 16904)
ScribeTest:test_drop_Multiple_IsAuthProtected() (gas: 13794)
ScribeTest:test_drop_Single_IsAuthProtected() (gas: 12148)
ScribeTest:test_latestAnswer_isTollProtected() (gas: 12092)
ScribeTest:test_latestRoundData_isTollProtected() (gas: 13132)
ScribeTest:test_lift_Multiple_FailsIf_ECDSADataInvalid() (gas: 106626)
ScribeTest:test_lift_Multiple_FailsIf_MaxFeedsReached() (gas: 20286275)
ScribeTest:test_lift_Multiple_IsAuthProtected() (gas: 15605)
ScribeTest:test_lift_Single_FailsIf_ECDSADataInvalid() (gas: 21437)
ScribeTest:test_lift_Single_FailsIf_MaxFeedsReached() (gas: 19732318)
ScribeTest:test_lift_Single_IsAuthProtected() (gas: 12535)
ScribeTest:test_peek_isTollProtected() (gas: 12373)
ScribeTest:test_peep_isTollProtected() (gas: 12461)
ScribeTest:test_poke_Initial_FailsIf_AgeIsZero() (gas: 239335)
ScribeTest:test_readWithAge_isTollProtected() (gas: 11969)
ScribeTest:test_read_isTollProtected() (gas: 11695)
ScribeTest:test_setBar_FailsIf_BarIsZero() (gas: 11392)
ScribeTest:test_setBar_IsAuthProtected() (gas: 12823)
ScribeTest:test_toll_diss_IsAuthProtected() (gas: 12533)
ScribeTest:test_toll_kiss_IsAuthProtected() (gas: 12088)
ScribeTest:test_tryReadWithAge_isTollProtected() (gas: 12871)
ScribeTest:test_tryRead_isTollProtected() (gas: 12287)
ScribeOptimisticTest:test_lift_Multiple_FailsIf_ECDSADataInvalid() (gas: 78843)
ScribeOptimisticTest:test_lift_Multiple_IsAuthProtected() (gas: 16509)
ScribeOptimisticTest:test_lift_Single_FailsIf_ECDSADataInvalid() (gas: 21864)
ScribeOptimisticTest:test_lift_Single_FailsIf_FeedIdAlreadyLifted() (gas: 73859)
ScribeOptimisticTest:test_lift_Single_IsAuthProtected() (gas: 13053)
ScribeOptimisticTest:test_opChallenge_CalldataEncodingAttack() (gas: 1528876)
ScribeOptimisticTest:test_opChallenge_FailsIf_CalledSubsequently() (gas: 256890)
ScribeOptimisticTest:test_opChallenge_FailsIf_InvalidSchnorrDataGiven() (gas: 226291)
ScribeOptimisticTest:test_opChallenge_FailsIf_NoOpPokeToChallenge() (gas: 14879)
ScribeOptimisticTest:test_peek_isTollProtected() (gas: 12886)
ScribeOptimisticTest:test_peep_isTollProtected() (gas: 13216)
ScribeOptimisticTest:test_poke_Initial_FailsIf_AgeIsZero() (gas: 185331)
ScribeOptimisticTest:test_readWithAge_isTollProtected() (gas: 12397)
ScribeOptimisticTest:test_read_isTollProtected() (gas: 11836)
ScribeOptimisticTest:test_setBar_FailsIf_BarIsZero() (gas: 11908)
ScribeOptimisticTest:test_setBar_IsAuthProtected() (gas: 13803)
ScribeOptimisticTest:test_setMaxChallengeReward_IsAuthProtected() (gas: 13698)
ScribeOptimisticTest:test_setOpChallengePeriod_DropsFinalizedOpPoke_If_NonFinalizedAfterUpdate() (gas: 242619)
ScribeOptimisticTest:test_setOpChallengePeriod_FailsIf_OpChallengePeriodIsZero() (gas: 11633)
ScribeOptimisticTest:test_setOpChallengePeriod_IsAuthProtected() (gas: 12393)
ScribeOptimisticTest:test_toll_diss_IsAuthProtected() (gas: 13268)
ScribeOptimisticTest:test_toll_kiss_IsAuthProtected() (gas: 12517)
ScribeOptimisticTest:test_tryReadWithAge_isTollProtected() (gas: 13786)
ScribeOptimisticTest:test_tryRead_isTollProtected() (gas: 13047)
ScribeTest:test_Deployment() (gas: 1185225)
ScribeTest:test_drop_Multiple_IsAuthProtected() (gas: 13643)
ScribeTest:test_drop_Single_IsAuthProtected() (gas: 12648)
ScribeTest:test_latestAnswer_isTollProtected() (gas: 12053)
ScribeTest:test_latestRoundData_isTollProtected() (gas: 13080)
ScribeTest:test_lift_Multiple_FailsIf_ECDSADataInvalid() (gas: 78456)
ScribeTest:test_lift_Multiple_IsAuthProtected() (gas: 15556)
ScribeTest:test_lift_Single_FailsIf_ECDSADataInvalid() (gas: 21117)
ScribeTest:test_lift_Single_FailsIf_FeedIdAlreadyLifted() (gas: 73352)
ScribeTest:test_lift_Single_IsAuthProtected() (gas: 12631)
ScribeTest:test_peek_isTollProtected() (gas: 12337)
ScribeTest:test_peep_isTollProtected() (gas: 12425)
ScribeTest:test_poke_Initial_FailsIf_AgeIsZero() (gas: 181717)
ScribeTest:test_readWithAge_isTollProtected() (gas: 11957)
ScribeTest:test_read_isTollProtected() (gas: 11678)
ScribeTest:test_setBar_FailsIf_BarIsZero() (gas: 11457)
ScribeTest:test_setBar_IsAuthProtected() (gas: 12846)
ScribeTest:test_toll_diss_IsAuthProtected() (gas: 12489)
ScribeTest:test_toll_kiss_IsAuthProtected() (gas: 12156)
ScribeTest:test_tryReadWithAge_isTollProtected() (gas: 12838)
ScribeTest:test_tryRead_isTollProtected() (gas: 12298)
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# VSCode settings
.vscode/

# Compiler files
cache/
out/
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@ All notable changes to this project will be documented in this file.

The format is based on [Common Changelog](https://common-changelog.org/).

[2.0.0]: https://github.com/chronicleprotocol/scribe/releases/tag/v2.0.0
[1.2.0]: https://github.com/chronicleprotocol/scribe/releases/tag/v1.2.0
[1.1.0]: https://github.com/chronicleprotocol/scribe/releases/tag/v1.1.0
[1.0.0]: https://github.com/chronicleprotocol/scribe/releases/tag/v1.0.0

## [2.0.0] - 2023-11-27

### Changed

- **Breaking** Use 1-byte identifier for feeds based on highest-order byte of their addresses instead of their storage array's index ([#23](https://github.com/chronicleprotocol/scribe/pull/23))
- **Breaking** Change `IScribe` and `IScribeOptimistic` interfaces to account for new feed identification ([#23](https://github.com/chronicleprotocol/scribe/pull/23))

### Fixed

- DOS vector in `ScribeOptimistic::opPoke` making `ScribeOptimistic::opChallenge` economically unprofitable ([#23](https://github.com/chronicleprotocol/scribe/pull/23))
- Possibility to successfully `opChallenge` a valid `opPoke` via non-default calldata encoding ([#23](https://github.com/chronicleprotocol/scribe/pull/23))

## [1.2.0] - 2023-09-29

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ $ FOUNDRY_PROFILE=intense forge test # Run all tests in intense mode
$ forge test --nmt "FuzzDifferentialOracleSuite" # Run only non-differential fuzz tests
```

Note that in order to run the whole test suite, i.e. including differential fuzz tests, the oracle-suite's [`schnorr`](https://github.com/chronicleprotocol/oracle-suite) binary needs to be present inside the `bin/` directory.
Note that in order to run the whole test suite, i.e. including differential fuzz tests, the oracle-suite's musig [`schnorr`](https://github.com/chronicleprotocol/musig/tree/master/cmd/schnorr) binary needs to be present inside the `bin/` directory.

Lint:

Expand Down
Binary file modified assets/benchmarks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added audits/Cantina@v2.0.0.pdf
Binary file not shown.
18 changes: 10 additions & 8 deletions docs/Benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

The benchmark for `Scribe` is based on the `poke()` function while the benchmark for `ScribeOptimistic` being based on the `opPoke()` function.

| `bar` | `Scribe::poke()` | `ScribeOptimistic::opPoke()` |
|-------|--------------------|------------------------------|
| 5 | 79,428 | 66,462 |
| 10 | 106,862 | 66,534 |
| 15 | 131,834 | 66,603 |
| 20 | 158,263 | 66,663 |
| 50 | 315,655 | 67,437 |
| 100 | 577,919 | 68,845 |
| `bar` | `Scribe::poke()` | `ScribeOptimistic::opPoke()` |
| ----- | ---------------- | ---------------------------- |
| 5 | 81,025 | 68,944 |
| 10 | 106,395 | 69,004 |
| 15 | 134,342 | 69,061 |
| 20 | 159,488 | 69,133 |
| 50 | 320,473 | 69,908 |
| 100 | 585,993 | 71,315 |
| 200 | 1,119,535 | 73,759 |
| 255 | 1,411,702 | 74,852 |

The following visualization shows the gas usage for different numbers of `bar`:

Expand Down
55 changes: 6 additions & 49 deletions docs/Invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,67 +68,24 @@ This document specifies invariants of the Scribe and ScribeOptimistic oracle con

## `{Scribe, ScribeOptimistic}::_pubKeys`

* `_pubKeys[0]` is the zero point:
* `_pubKeys`' length is 256:
```
_pubKeys[0].isZeroPoint()
_pubKeys.length == 256
```

* A non-zero public key exists at most once:
* Public keys are stored at the index of their address' first byte:
```
∀x ∊ PublicKeys: x.isZeroPoint() ∨ count(x in _pubKeys) <= 1
```

* Length is strictly monotonically increasing:
```
preTx(_pubKeys.length) != postTx(_pubKeys.length)
→ preTx(_pubKeys.length) < posTx(_pubKeys.length)
```

* Existing public key may only be deleted, never mutated:
```
∀x ∊ uint: x < _pubKeys.length ⋀ preTx(_pubKeys[x]) != postTx(_pubKeys[x])
→ postTx(_pubKeys[x].isZeroPoint())
```

* Newly added public key is non-zero:
```
preTx(_pubKeys.length) != postTx(_pubKeys.length)
→ postTx(!_pubKeys[_pubKeys.length-1].isZeroPoint())
∀id ∊ Uint8: _pubKeys[id].isZeroPoint() ∨ (_pubKeys[id].toAddress() >> 152) == id
```

* Only functions `lift` and `drop` may mutate the array's state:
```
xuint: preTx(_pubKeys[x]) != postTx(_pubKeys[x])
idUint8: preTx(_pubKeys[id]) != postTx(_pubKeys[id])
→ msg.sig ∊ {"lift", "drop"}
```

* Array's state may only be mutated by auth'ed caller:
```
∀x ∊ uint: preTx(_pubKeys[x]) != postTx(_pubKeys[x])
→ authed(msg.sender)
```

## `{Scribe, ScribeOptimistic}::_feeds`

* Image of mapping is `[0, _pubKeys.length)`:
```
∀x ∊ Address: _feeds[x] ∊ [0, _pubKeys.length)
```

* Image of mapping links to feed's public key in `_pubKeys`:
```
∀x ∊ Address: _feeds[x] = y ⋀ y != 0
→ _pubKeys[y].toAddress() == x
```

* Only functions `lift` and `drop` may mutate the mapping's state:
```
∀x ∊ Address: preTx(_feeds[x]) != postTx(_feeds[x])
→ msg.sig ∊ {"lift", "drop"}
```

* Mapping's state may only be mutated by auth'ed caller:
```
∀x ∊ Address: preTx(_feeds[x]) != postTx(_feeds[x])
∀id ∊ Uint8: preTx(_pubKeys[id]) != postTx(_pubKeys[id])
→ authed(msg.sender)
```
4 changes: 2 additions & 2 deletions docs/Management.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ $ forge script \

Set the following environment variables:

- `FEED_INDEX`: The feed's index
- `FEED_ID`: The feed's id

Run:

Expand All @@ -120,7 +120,7 @@ $ forge script \
--private-key $PRIVATE_KEY \
--broadcast \
--rpc-url $RPC_URL \
--sig $(cast calldata "drop(address,uint)" $SCRIBE $FEED_INDEX) \
--sig $(cast calldata "drop(address,uint)" $SCRIBE $FEED_ID) \
-vvv \
script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script
```
Expand Down
7 changes: 3 additions & 4 deletions docs/Scribe.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,24 @@ For more info, see [`LibSecp256k1::addAffinePoint()`](../src/libs/LibSecp256k1.s

The `poke()` function has to receive the set of feeds, i.e. public keys, that participated in the Schnorr multi-signature.

To reduce the calldata load, Scribe does not use type `address`, which uses 20 bytes per feed, but encodes the unique feeds' identifier's byte-wise into a `bytes` type called `signersBlob`.
To reduce the calldata load, Scribe does not use type `address`, which uses 20 bytes per feed, but encodes the feeds' identifier's byte-wise into a `bytes` type called `feedIds`.

For more info, see [`LibSchnorrData.sol`](../src/libs/LibSchnorrData.sol).
A feed's identifier is defined as the highest order byte of the feed's address and can be computed via `uint8(uint(uint160(feedAddress)) >> 152)`.

## Lifting Feeds

Feeds _must_ prove the integrity of their public key by proving the ownership of the corresponding private key. The `lift()` function therefore expects an ECDSA signed message, for more info see [`IScribe.feedRegistrationMessage()`](../src/IScribe.sol).

If public key's would not be verified, the Schnorr signature verification would be vulnerable to rogue-key attacks. For more info, see [`docs/Schnorr.md`](./Schnorr.md#key-aggregation-for-multisignatures).

Also, the number of state-changing `lift()` executions is limited to `type(uint8).max-1`, i.e. 254. After reaching this limit, no further `lift()` calls can be executed. For more info, see [`IScribe.maxFeeds()`](../src/IScribe.sol).

## Chainlink Compatibility

Scribe aims to be partially Chainlink compatible by implementing the most widely, and not deprecated, used functions of the `IChainlinkAggregatorV3` interface.

The following `IChainlinkAggregatorV3` functions are provided:
- `latestRoundData()`
- `decimals()`
- `latestAnswer()`

## Optimistic-Flavored Scribe

Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ via_ir = true
extra_output_files = ["metadata", "irOptimized"]

# Testing
fuzz = { runs = 10 }
fuzz = { runs = 50 }
block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT

[invariant]
Expand Down
Loading