Skip to content

Commit

Permalink
all: v2 refactoring to use feed byte identifier (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmerkleplant committed Nov 27, 2023
1 parent 529816b commit 41f25a8
Show file tree
Hide file tree
Showing 43 changed files with 1,977 additions and 1,561 deletions.
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

0 comments on commit 41f25a8

Please sign in to comment.