From 6d0e56d5882844cf0ee93567fdfd232f52795f92 Mon Sep 17 00:00:00 2001 From: pascal Date: Tue, 26 Sep 2023 11:53:43 +0200 Subject: [PATCH] all: v2 refactoring to use feed byte identifier --- .env.example | 4 +- .gas-snapshot | 129 +-- .gitignore | 3 + assets/benchmarks.png | Bin 32949 -> 31124 bytes docs/Benchmarks.md | 18 +- docs/Invariants.md | 55 +- docs/Management.md | 4 +- docs/Scribe.md | 6 +- foundry.toml | 2 +- script/Scribe.s.sol | 30 +- script/benchmarks/ScribeBenchmark.s.sol | 32 +- .../ScribeOptimisticBenchmark.s.sol | 32 +- script/benchmarks/run.sh | 4 + script/benchmarks/visualize.py | 6 +- script/chaincheck/IScribeChaincheck.sol | 69 +- .../IScribeOptimisticChaincheck.sol | 4 - script/dev/ScribeOptimisticTester.s.sol | 26 +- script/dev/ScribeTester.s.sol | 13 +- script/dev/invalid-oppoker.sh | 3 +- script/dev/test-feeds.json | 1024 +++++++++++++++++ script/libs/LibFeed.sol | 110 +- src/IScribe.sol | 94 +- src/IScribeOptimistic.sol | 16 +- src/Scribe.sol | 310 +++-- src/ScribeOptimistic.sol | 48 +- src/libs/LibBytes.sol | 6 +- src/libs/LibSchnorrData.sol | 88 +- test/EVMTest.sol | 6 +- test/IScribeOptimisticTest.sol | 141 ++- test/IScribeTest.sol | 528 ++++----- test/LibBytesTest.sol | 6 +- test/LibSchnorrDataTest.sol | 96 +- test/LibSchnorrTest.sol | 18 +- test/LibSecp256k1Test.sol | 8 +- test/inspectable/ScribeInspectable.sol | 16 +- test/invariants/FeedSet.sol | 4 + test/invariants/IScribeInvariantTest.sol | 95 +- test/invariants/ScribeHandler.sol | 73 +- 38 files changed, 1932 insertions(+), 1195 deletions(-) create mode 100644 script/dev/test-feeds.json diff --git a/.env.example b/.env.example index ab17c1c..1fdfe84 100644 --- a/.env.example +++ b/.env.example @@ -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 @@ -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= diff --git a/.gas-snapshot b/.gas-snapshot index 0ba2410..9680a1e 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -3,73 +3,64 @@ 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: 56925) -ScribeOptimisticTest:test_afterAuthedAction_1_drop() (gas: 248716) -ScribeOptimisticTest:test_afterAuthedAction_1_setBar() (gas: 295527) -ScribeOptimisticTest:test_afterAuthedAction_1_setChallengePeriod() (gas: 294070) -ScribeOptimisticTest:test_afterAuthedAction_2_drop() (gas: 325609) -ScribeOptimisticTest:test_afterAuthedAction_2_setBar() (gas: 384647) -ScribeOptimisticTest:test_afterAuthedAction_2_setChallengePeriod() (gas: 384247) -ScribeOptimisticTest:test_afterAuthedAction_3_drop() (gas: 251331) -ScribeOptimisticTest:test_afterAuthedAction_3_setBar() (gas: 298667) -ScribeOptimisticTest:test_afterAuthedAction_3_setChallengePeriod() (gas: 298140) -ScribeOptimisticTest:test_afterAuthedAction_4_drop() (gas: 327074) -ScribeOptimisticTest:test_afterAuthedAction_4_setBar() (gas: 385846) -ScribeOptimisticTest:test_afterAuthedAction_4_setChallengePeriod() (gas: 384610) -ScribeOptimisticTest:test_drop_IndexZero() (gas: 44243) -ScribeOptimisticTest:test_drop_Multiple_IsAuthProtected() (gas: 14726) -ScribeOptimisticTest:test_drop_Single_IsAuthProtected() (gas: 12760) -ScribeOptimisticTest:test_latestRoundData_isTollProtected() (gas: 13986) -ScribeOptimisticTest:test_lift_Multiple_FailsIf_ECDSADataInvalid() (gas: 106951) -ScribeOptimisticTest:test_lift_Multiple_FailsIf_MaxFeedsReached() (gas: 20286905) -ScribeOptimisticTest:test_lift_Multiple_IsAuthProtected() (gas: 16446) -ScribeOptimisticTest:test_lift_Single_FailsIf_ECDSADataInvalid() (gas: 22113) -ScribeOptimisticTest:test_lift_Single_FailsIf_MaxFeedsReached() (gas: 19731467) -ScribeOptimisticTest:test_lift_Single_IsAuthProtected() (gas: 12905) -ScribeOptimisticTest:test_opChallenge_FailsIf_CalledSubsequently() (gas: 311597) -ScribeOptimisticTest:test_opChallenge_FailsIf_InvalidSchnorrDataGiven() (gas: 283188) -ScribeOptimisticTest:test_opChallenge_FailsIf_NoOpPokeToChallenge() (gas: 14853) -ScribeOptimisticTest:test_peek_isTollProtected() (gas: 12826) -ScribeOptimisticTest:test_peep_isTollProtected() (gas: 13178) -ScribeOptimisticTest:test_poke_Initial_FailsIf_AgeIsZero() (gas: 242731) -ScribeOptimisticTest:test_readWithAge_isTollProtected() (gas: 12357) -ScribeOptimisticTest:test_read_isTollProtected() (gas: 11801) -ScribeOptimisticTest:test_setBar_FailsIf_BarIsZero() (gas: 11771) -ScribeOptimisticTest:test_setBar_IsAuthProtected() (gas: 13686) -ScribeOptimisticTest:test_setMaxChallengeReward_IsAuthProtected() (gas: 13686) -ScribeOptimisticTest:test_setOpChallengePeriod_DropsFinalizedOpPoke_If_NonFinalizedAfterUpdate() (gas: 299610) -ScribeOptimisticTest:test_setOpChallengePeriod_FailsIf_OpChallengePeriodIsZero() (gas: 11545) -ScribeOptimisticTest:test_setOpChallengePeriod_IsAuthProtected() (gas: 12310) -ScribeOptimisticTest:test_toll_diss_IsAuthProtected() (gas: 13216) -ScribeOptimisticTest:test_toll_kiss_IsAuthProtected() (gas: 12397) -ScribeOptimisticTest:test_tryReadWithAge_isTollProtected() (gas: 13702) -ScribeOptimisticTest:test_tryRead_isTollProtected() (gas: 12964) -ScribeTest:test_Deployment() (gas: 40852) -ScribeTest:test_drop_IndexZero() (gas: 16904) -ScribeTest:test_drop_Multiple_IsAuthProtected() (gas: 13750) -ScribeTest:test_drop_Single_IsAuthProtected() (gas: 12126) -ScribeTest:test_latestRoundData_isTollProtected() (gas: 13088) -ScribeTest:test_lift_Multiple_FailsIf_ECDSADataInvalid() (gas: 106604) -ScribeTest:test_lift_Multiple_FailsIf_MaxFeedsReached() (gas: 20286231) -ScribeTest:test_lift_Multiple_IsAuthProtected() (gas: 15561) -ScribeTest:test_lift_Single_FailsIf_ECDSADataInvalid() (gas: 21415) -ScribeTest:test_lift_Single_FailsIf_MaxFeedsReached() (gas: 19732296) -ScribeTest:test_lift_Single_IsAuthProtected() (gas: 12535) -ScribeTest:test_peek_isTollProtected() (gas: 12329) -ScribeTest:test_peep_isTollProtected() (gas: 12439) -ScribeTest:test_poke_Initial_FailsIf_AgeIsZero() (gas: 239291) -ScribeTest:test_readWithAge_isTollProtected() (gas: 11969) -ScribeTest:test_read_isTollProtected() (gas: 11673) -ScribeTest:test_setBar_FailsIf_BarIsZero() (gas: 11370) -ScribeTest:test_setBar_IsAuthProtected() (gas: 12779) -ScribeTest:test_toll_diss_IsAuthProtected() (gas: 12489) -ScribeTest:test_toll_kiss_IsAuthProtected() (gas: 12066) -ScribeTest:test_tryReadWithAge_isTollProtected() (gas: 12827) -ScribeTest:test_tryRead_isTollProtected() (gas: 12265) \ No newline at end of file +ScribeInvariantTest:invariant_pubKeys_IndexedViaFeedId() (runs: 256, calls: 3840, reverts: 0) +ScribeOptimisticTest:test_Deployment() (gas: 1228808) +ScribeOptimisticTest:test_afterAuthedAction_1_drop() (gas: 203457) +ScribeOptimisticTest:test_afterAuthedAction_1_setBar() (gas: 239071) +ScribeOptimisticTest:test_afterAuthedAction_1_setChallengePeriod() (gas: 237611) +ScribeOptimisticTest:test_afterAuthedAction_2_drop() (gas: 286191) +ScribeOptimisticTest:test_afterAuthedAction_2_setBar() (gas: 325422) +ScribeOptimisticTest:test_afterAuthedAction_2_setChallengePeriod() (gas: 324975) +ScribeOptimisticTest:test_afterAuthedAction_3_drop() (gas: 206091) +ScribeOptimisticTest:test_afterAuthedAction_3_setBar() (gas: 242256) +ScribeOptimisticTest:test_afterAuthedAction_3_setChallengePeriod() (gas: 241658) +ScribeOptimisticTest:test_afterAuthedAction_4_drop() (gas: 287468) +ScribeOptimisticTest:test_afterAuthedAction_4_setBar() (gas: 326457) +ScribeOptimisticTest:test_afterAuthedAction_4_setChallengePeriod() (gas: 325240) +ScribeOptimisticTest:test_drop_Multiple_IsAuthProtected() (gas: 14532) +ScribeOptimisticTest:test_drop_Single_IsAuthProtected() (gas: 13415) +ScribeOptimisticTest:test_latestRoundData_isTollProtected() (gas: 13912) +ScribeOptimisticTest:test_lift_Multiple_FailsIf_ECDSADataInvalid() (gas: 79306) +ScribeOptimisticTest:test_lift_Multiple_IsAuthProtected() (gas: 16424) +ScribeOptimisticTest:test_lift_Single_FailsIf_ECDSADataInvalid() (gas: 21768) +ScribeOptimisticTest:test_lift_Single_FailsIf_FeedIdAlreadyLifted() (gas: 74296) +ScribeOptimisticTest:test_lift_Single_IsAuthProtected() (gas: 12979) +ScribeOptimisticTest:test_opChallenge_FailsIf_CalledSubsequently() (gas: 255199) +ScribeOptimisticTest:test_opChallenge_FailsIf_InvalidSchnorrDataGiven() (gas: 226894) +ScribeOptimisticTest:test_opChallenge_FailsIf_NoOpPokeToChallenge() (gas: 14764) +ScribeOptimisticTest:test_peek_isTollProtected() (gas: 12768) +ScribeOptimisticTest:test_peep_isTollProtected() (gas: 13120) +ScribeOptimisticTest:test_poke_Initial_FailsIf_AgeIsZero() (gas: 186100) +ScribeOptimisticTest:test_readWithAge_isTollProtected() (gas: 12323) +ScribeOptimisticTest:test_read_isTollProtected() (gas: 11784) +ScribeOptimisticTest:test_setBar_FailsIf_BarIsZero() (gas: 11812) +ScribeOptimisticTest:test_setBar_IsAuthProtected() (gas: 13685) +ScribeOptimisticTest:test_setMaxChallengeReward_IsAuthProtected() (gas: 13580) +ScribeOptimisticTest:test_setOpChallengePeriod_DropsFinalizedOpPoke_If_NonFinalizedAfterUpdate() (gas: 243099) +ScribeOptimisticTest:test_setOpChallengePeriod_FailsIf_OpChallengePeriodIsZero() (gas: 11537) +ScribeOptimisticTest:test_setOpChallengePeriod_IsAuthProtected() (gas: 12297) +ScribeOptimisticTest:test_toll_diss_IsAuthProtected() (gas: 13150) +ScribeOptimisticTest:test_toll_kiss_IsAuthProtected() (gas: 12465) +ScribeOptimisticTest:test_tryReadWithAge_isTollProtected() (gas: 13668) +ScribeOptimisticTest:test_tryRead_isTollProtected() (gas: 12951) +ScribeTest:test_Deployment() (gas: 1209573) +ScribeTest:test_drop_Multiple_IsAuthProtected() (gas: 13577) +ScribeTest:test_drop_Single_IsAuthProtected() (gas: 12582) +ScribeTest:test_latestRoundData_isTollProtected() (gas: 13014) +ScribeTest:test_lift_Multiple_FailsIf_ECDSADataInvalid() (gas: 78937) +ScribeTest:test_lift_Multiple_IsAuthProtected() (gas: 15517) +ScribeTest:test_lift_Single_FailsIf_ECDSADataInvalid() (gas: 21073) +ScribeTest:test_lift_Single_FailsIf_FeedIdAlreadyLifted() (gas: 73849) +ScribeTest:test_lift_Single_IsAuthProtected() (gas: 12609) +ScribeTest:test_peek_isTollProtected() (gas: 12271) +ScribeTest:test_peep_isTollProtected() (gas: 12381) +ScribeTest:test_poke_Initial_FailsIf_AgeIsZero() (gas: 182660) +ScribeTest:test_readWithAge_isTollProtected() (gas: 11935) +ScribeTest:test_read_isTollProtected() (gas: 11656) +ScribeTest:test_setBar_FailsIf_BarIsZero() (gas: 11413) +ScribeTest:test_setBar_IsAuthProtected() (gas: 12780) +ScribeTest:test_toll_diss_IsAuthProtected() (gas: 12423) +ScribeTest:test_toll_kiss_IsAuthProtected() (gas: 12134) +ScribeTest:test_tryReadWithAge_isTollProtected() (gas: 12772) +ScribeTest:test_tryRead_isTollProtected() (gas: 12254) \ No newline at end of file diff --git a/.gitignore b/.gitignore index a5677dc..dbce94b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# VSCode settings +.vscode/ + # Compiler files cache/ out/ diff --git a/assets/benchmarks.png b/assets/benchmarks.png index 2211c8257a1d3ee2b6edfed3e0e6ae110b57c663..de2dec7977d1d980b654ac073da7fd34779286c5 100644 GIT binary patch literal 31124 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 literal 32949 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

IK+}LJ7y36+H=v;$b9eP<_Cu@ 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 rI50{6hS*qZ(>rX diff --git a/docs/Benchmarks.md b/docs/Benchmarks.md index 0635f69..80b31f7 100644 --- a/docs/Benchmarks.md +++ b/docs/Benchmarks.md @@ -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 | 80,280 | 68,815 | +| 10 | 105,070 | 68,887 | +| 15 | 132,414 | 68,944 | +| 20 | 156,983 | 69,004 | +| 50 | 314,455 | 69,791 | +| 100 | 574,227 | 71,186 | +| 200 | 1,096,599 | 73,630 | +| 255 | 1,382,810 | 74,735 | The following visualization shows the gas usage for different numbers of `bar`: diff --git a/docs/Invariants.md b/docs/Invariants.md index 3c0bad5..fa5441a 100644 --- a/docs/Invariants.md +++ b/docs/Invariants.md @@ -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: ``` - ∀x ∊ uint: preTx(_pubKeys[x]) != postTx(_pubKeys[x]) + ∀id ∊ Uint8: 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) ``` diff --git a/docs/Management.md b/docs/Management.md index 4884f8e..9fcbea0 100644 --- a/docs/Management.md +++ b/docs/Management.md @@ -111,7 +111,7 @@ $ forge script \ Set the following environment variables: -- `FEED_INDEX`: The feed's index +- `FEED_ID`: The feed's id Run: @@ -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 ``` diff --git a/docs/Scribe.md b/docs/Scribe.md index 28cad0a..bacbcda 100644 --- a/docs/Scribe.md +++ b/docs/Scribe.md @@ -50,7 +50,9 @@ 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`. + +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)`. For more info, see [`LibSchnorrData.sol`](../src/libs/LibSchnorrData.sol). @@ -60,8 +62,6 @@ Feeds _must_ prove the integrity of their public key by proving the ownership of 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. diff --git a/foundry.toml b/foundry.toml index bcd5f95..0f96259 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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] diff --git a/script/Scribe.s.sol b/script/Scribe.s.sol index 5fb2b22..8dd7dc8 100644 --- a/script/Scribe.s.sol +++ b/script/Scribe.s.sol @@ -89,7 +89,7 @@ contract ScribeScript is Script { require(!pubKey.isZeroPoint(), "Public key cannot be zero point"); require(pubKey.isOnCurve(), "Public key must be valid secp256k1 point"); bool isFeed; - (isFeed, /*feedIndex*/ ) = IScribe(self).feeds(pubKey.toAddress()); + (isFeed, /*feedId*/ ) = IScribe(self).feeds(pubKey.toAddress()); require(!isFeed, "Public key already lifted"); address recovered = @@ -143,8 +143,7 @@ contract ScribeScript is Script { "Public key must be valid secp256k1 point" ); bool isFeed; - (isFeed, /*feedIndex*/ ) = - IScribe(self).feeds(pubKeys[i].toAddress()); + (isFeed, /*feedId*/ ) = IScribe(self).feeds(pubKeys[i].toAddress()); require(!isFeed, "Public key already lifted"); } @@ -180,15 +179,13 @@ contract ScribeScript is Script { } } - /// @dev Drops feed with index `feedIndex`. - function drop(address self, uint feedIndex) public { - require(feedIndex != 0, "Feed index cannot be zero"); - + /// @dev Drops feed with id `feedId`. + function drop(address self, uint8 feedId) public { vm.startBroadcast(); - IScribe(self).drop(feedIndex); + IScribe(self).drop(feedId); vm.stopBroadcast(); - console2.log("Dropped", feedIndex); + console2.log("Dropped", feedId); } // -- View Functions @@ -314,13 +311,13 @@ contract ScribeScript is Script { /// script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script /// ``` function deactivate(address self) public { - // Get current feeds' indexes. - uint[] memory feedIndexes; - ( /*feeds*/ , feedIndexes) = IScribe(self).feeds(); + // Get lifted feed ids. + uint8[] memory feedIds; + ( /*feeds*/ , feedIds) = IScribe(self).feeds(); // Drop all feeds. vm.startBroadcast(); - IScribe(self).drop(feedIndexes); + IScribe(self).drop(feedIds); vm.stopBroadcast(); // Create new random private key. @@ -336,12 +333,9 @@ contract ScribeScript is Script { // Lift feed. vm.startBroadcast(); - uint feedIndex = IScribe(self).lift(feed.pubKey, ecdsaData); + IScribe(self).lift(feed.pubKey, ecdsaData); vm.stopBroadcast(); - // Set feed's assigned feedIndex. - feed.index = uint8(feedIndex); - // Set bar to 1. vm.startBroadcast(); IScribe(self).setBar(uint8(1)); @@ -365,7 +359,7 @@ contract ScribeScript is Script { // Drop feed again. vm.startBroadcast(); - IScribe(self).drop(feed.index); + IScribe(self).drop(feed.id); vm.stopBroadcast(); // Set bar to type(uint8).max. diff --git a/script/benchmarks/ScribeBenchmark.s.sol b/script/benchmarks/ScribeBenchmark.s.sol index 8953296..1605d63 100644 --- a/script/benchmarks/ScribeBenchmark.s.sol +++ b/script/benchmarks/ScribeBenchmark.s.sol @@ -107,18 +107,32 @@ contract ScribeBenchmark is Script { scribe.poke(pokeData, schnorrData); } - function _createFeeds(uint amount) + function _createFeeds(uint numberFeeds) internal returns (LibFeed.Feed[] memory) { - uint startPrivKey = 2; - - LibFeed.Feed[] memory feeds = new LibFeed.Feed[](amount); - for (uint i; i < amount; i++) { - feeds[i] = LibFeed.newFeed({ - privKey: startPrivKey + i, - index: uint8(i + 1) - }); + LibFeed.Feed[] memory feeds = new LibFeed.Feed[](numberFeeds); + + // Note to not start with privKey=1. This is because the sum of public + // keys would evaluate to: + // pubKeyOf(1) + pubKeyOf(2) + pubKeyOf(3) + ... + // = pubKeyOf(3) + pubKeyOf(3) + ... + // Note that pubKeyOf(3) would be doubled. Doubling is not supported by + // LibSecp256k1 as this would indicate a double-signing attack. + uint privKey = 2; + uint bloom; + uint ctr; + while (ctr != numberFeeds) { + LibFeed.Feed memory feed = LibFeed.newFeed({privKey: privKey}); + + // Check whether feed with id already created, if not create. + if (bloom & (1 << feed.id) == 0) { + bloom |= 1 << feed.id; + + feeds[ctr++] = feed; + } + + privKey++; } return feeds; diff --git a/script/benchmarks/ScribeOptimisticBenchmark.s.sol b/script/benchmarks/ScribeOptimisticBenchmark.s.sol index 90c0072..8d436f4 100644 --- a/script/benchmarks/ScribeOptimisticBenchmark.s.sol +++ b/script/benchmarks/ScribeOptimisticBenchmark.s.sol @@ -154,18 +154,32 @@ contract ScribeOptimisticBenchmark is Script { opScribe.opPoke(pokeData, schnorrData, ecdsaData); } - function _createFeeds(uint amount) + function _createFeeds(uint numberFeeds) internal returns (LibFeed.Feed[] memory) { - uint startPrivKey = 2; - - LibFeed.Feed[] memory feeds = new LibFeed.Feed[](amount); - for (uint i; i < amount; i++) { - feeds[i] = LibFeed.newFeed({ - privKey: startPrivKey + i, - index: uint8(i + 1) - }); + LibFeed.Feed[] memory feeds = new LibFeed.Feed[](numberFeeds); + + // Note to not start with privKey=1. This is because the sum of public + // keys would evaluate to: + // pubKeyOf(1) + pubKeyOf(2) + pubKeyOf(3) + ... + // = pubKeyOf(3) + pubKeyOf(3) + ... + // Note that pubKeyOf(3) would be doubled. Doubling is not supported by + // LibSecp256k1 as this would indicate a double-signing attack. + uint privKey = 2; + uint bloom; + uint ctr; + while (ctr != numberFeeds) { + LibFeed.Feed memory feed = LibFeed.newFeed({privKey: privKey}); + + // Check whether feed with id already created, if not create. + if (bloom & (1 << feed.id) == 0) { + bloom |= 1 << feed.id; + + feeds[ctr++] = feed; + } + + privKey++; } return feeds; diff --git a/script/benchmarks/run.sh b/script/benchmarks/run.sh index e9bcfd3..a916a64 100755 --- a/script/benchmarks/run.sh +++ b/script/benchmarks/run.sh @@ -78,6 +78,8 @@ run_Scribe 15 run_Scribe 20 run_Scribe 50 run_Scribe 100 +run_Scribe 200 +run_Scribe 255 echo "=== Scribe Optimistic Benchmarks (Printing cost of non-initial opPoke())" run_ScribeOptimistic 5 @@ -86,3 +88,5 @@ run_ScribeOptimistic 15 run_ScribeOptimistic 20 run_ScribeOptimistic 50 run_ScribeOptimistic 100 +run_ScribeOptimistic 200 +run_ScribeOptimistic 255 diff --git a/script/benchmarks/visualize.py b/script/benchmarks/visualize.py index 70b3a13..f94e2ca 100644 --- a/script/benchmarks/visualize.py +++ b/script/benchmarks/visualize.py @@ -7,13 +7,13 @@ import matplotlib.pyplot as plt # Bar configuration -x = [5, 10, 15, 20, 50, 100] +x = [5, 10, 15, 20, 50, 100, 200, 255] # Scribe benchmark results received via `run.sh` -scribe = [79428, 106862, 131834, 158263, 315655, 577919] +scribe = [80280, 105070, 132414, 156983, 314455, 574227, 1096599, 1382810] # ScribeOptimistic benchmark results received via `run.sh` -opScribe = [66462, 66534, 66603, 66663, 67437, 68845] +opScribe = [68815, 68887, 68944, 69004, 69791, 71186, 73630, 74735] # Plotting the benchmark data plt.plot(x, scribe, label='Scribe') diff --git a/script/chaincheck/IScribeChaincheck.sol b/script/chaincheck/IScribeChaincheck.sol index 476a172..bfc0217 100644 --- a/script/chaincheck/IScribeChaincheck.sol +++ b/script/chaincheck/IScribeChaincheck.sol @@ -40,10 +40,6 @@ import { * "", * ... * ], - * "feedIndexes": [ - * 0, - * ... - * ], * "feedPublicKeys": { * "xCoordinates": [ * , @@ -119,7 +115,6 @@ contract IScribeChaincheck is Chaincheck { check_bar(); check_feeds_AllExpectedFeedsAreLifted(); check_feeds_OnlyExpectedFeedsAreLifted(); - check_feeds_AllExpectedFeedIndexesLinkToCorrectFeed(); check_feeds_AllPublicKeysAreLifted(); check_feeds_PublicKeysCorrectlyOrdered(); @@ -145,7 +140,6 @@ contract IScribeChaincheck is Chaincheck { function check_feeds_ConfigSanity() internal { address[] memory feeds = config.readAddressArray(".IScribe.feeds"); - uint[] memory feedIndexes = config.readUintArray(".IScribe.feedIndexes"); uint[] memory feedPublicKeysXCoordinates = config.readUintArray(".IScribe.feedPublicKeys.xCoordinates"); uint[] memory feedPublicKeysYCoordinates = @@ -153,16 +147,6 @@ contract IScribeChaincheck is Chaincheck { uint wantLen = feeds.length; - if (feedIndexes.length != wantLen) { - logs.push( - string.concat( - StdStyle.red( - "Config error: IScribe.feeds.length != IScribe.feedIndexes.length" - ) - ) - ); - } - if (feedPublicKeysXCoordinates.length != wantLen) { logs.push( string.concat( @@ -248,7 +232,7 @@ contract IScribeChaincheck is Chaincheck { wantFeed = wantFeeds[i]; bool isFeed; - (isFeed, /*feedIndex*/ ) = self.feeds(wantFeed); + (isFeed, /*feedId*/ ) = self.feeds(wantFeed); if (!isFeed) { logs.push( @@ -268,57 +252,26 @@ contract IScribeChaincheck is Chaincheck { // Check that only expected feeds are lifted. address[] memory gotFeeds; - (gotFeeds, /*feedIndexes*/ ) = self.feeds(); + (gotFeeds, /*feedId*/ ) = self.feeds(); for (uint i; i < gotFeeds.length; i++) { + bool found = false; + for (uint j; j < wantFeeds.length; j++) { if (gotFeeds[i] == wantFeeds[j]) { - // Feed is expected, break inner loop. - break; - } - - if (j == wantFeeds.length - 1) { - // Feed not found. - logs.push( - string.concat( - StdStyle.red("Unknown feed lifted:"), - " feed=", - vm.toString(gotFeeds[i]) - ) - ); + found = true; + break; // Found feed. Continue with outer loop. } } - } - } - - function check_feeds_AllExpectedFeedIndexesLinkToCorrectFeed() internal { - address[] memory wantFeeds = config.readAddressArray(".IScribe.feeds"); - uint[] memory wantFeedIndexes = - config.readUintArray(".IScribe.feedIndexes"); - - // Check that each feed index links to correct feed. - address wantFeed; - uint wantFeedIndex; - for (uint i; i < wantFeeds.length; i++) { - wantFeed = wantFeeds[i]; - wantFeedIndex = wantFeedIndexes[i]; - bool isFeed; - uint gotFeedIndex; - (isFeed, gotFeedIndex) = self.feeds(wantFeed); - - if (wantFeedIndex != gotFeedIndex) { + if (!found) { + // Feed not found. logs.push( string.concat( - StdStyle.red("Expected feed index does not match:"), + StdStyle.red("Unknown feed lifted:"), " feed=", - vm.toString(wantFeed), - ", expectedIndex=", - vm.toString(wantFeedIndex), - ", actualIndex=", - vm.toString(gotFeedIndex) + vm.toString(gotFeeds[i]) ) ); - continue; } } } @@ -348,7 +301,7 @@ contract IScribeChaincheck is Chaincheck { // Check that each address derived from public key is lifted. for (uint i; i < addrs.length; i++) { bool isFeed; - (isFeed, /*feedIndex*/ ) = self.feeds(addrs[i]); + (isFeed, /*feedId*/ ) = self.feeds(addrs[i]); if (!isFeed) { logs.push( diff --git a/script/chaincheck/IScribeOptimisticChaincheck.sol b/script/chaincheck/IScribeOptimisticChaincheck.sol index 1a1ab91..bd21949 100644 --- a/script/chaincheck/IScribeOptimisticChaincheck.sol +++ b/script/chaincheck/IScribeOptimisticChaincheck.sol @@ -34,10 +34,6 @@ import {IScribeChaincheck} from "./IScribeChaincheck.sol"; * "", * ... * ], - * "feedIndexes": [ - * 0, - * ... - * ], * "feedPublicKeys": { * "xCoordinates": [ * , diff --git a/script/dev/ScribeOptimisticTester.s.sol b/script/dev/ScribeOptimisticTester.s.sol index 42358ce..55ad623 100644 --- a/script/dev/ScribeOptimisticTester.s.sol +++ b/script/dev/ScribeOptimisticTester.s.sol @@ -45,18 +45,16 @@ contract ScribeOptimisticTesterScript is ScribeTesterScript { // Setup feeds. LibFeed.Feed[] memory feeds = new LibFeed.Feed[](privKeys.length); for (uint i; i < feeds.length; i++) { - feeds[i] = - LibFeed.newFeed({privKey: privKeys[i], index: uint8(i + 1)}); + feeds[i] = LibFeed.newFeed({privKey: privKeys[i]}); vm.label( feeds[i].pubKey.toAddress(), string.concat("Feed #", vm.toString(i + 1)) ); - // Update feed's index. + // Verify feed is lifted. bool isFeed; - uint feedIndex; - (isFeed, feedIndex) = + (isFeed, /*feedId*/ ) = IScribe(self).feeds(feeds[i].pubKey.toAddress()); require( isFeed, @@ -64,8 +62,6 @@ contract ScribeOptimisticTesterScript is ScribeTesterScript { "Private key not feed, privKey=", vm.toString(privKeys[i]) ) ); - - feeds[i].index = uint8(feedIndex); } // Create poke data. @@ -125,18 +121,16 @@ contract ScribeOptimisticTesterScript is ScribeTesterScript { // Setup feeds. LibFeed.Feed[] memory feeds = new LibFeed.Feed[](privKeys.length); for (uint i; i < feeds.length; i++) { - feeds[i] = - LibFeed.newFeed({privKey: privKeys[i], index: uint8(i + 1)}); + feeds[i] = LibFeed.newFeed({privKey: privKeys[i]}); vm.label( feeds[i].pubKey.toAddress(), string.concat("Feed #", vm.toString(i + 1)) ); - // Update feed's index. + // Verify feed is lifted. bool isFeed; - uint feedIndex; - (isFeed, feedIndex) = + (isFeed, /*feedId*/ ) = IScribe(self).feeds(feeds[i].pubKey.toAddress()); require( isFeed, @@ -144,8 +138,6 @@ contract ScribeOptimisticTesterScript is ScribeTesterScript { "Private key not feed, privKey=", vm.toString(privKeys[i]) ) ); - - feeds[i].index = uint8(feedIndex); } // Create poke data. @@ -196,7 +188,7 @@ contract ScribeOptimisticTesterScript is ScribeTesterScript { /// --private-key $PRIVATE_KEY \ /// --broadcast \ /// --rpc-url $RPC_URL \ - /// --sig $(cast calldata "opChallenge(address,uint128,uint32,bytes32,address,bytes)" $SCRIBE $TEST_POKE_VAL $TEST_POKE_AGE $TEST_SCHNORR_SIGNATURE $TEST_SCHNORR_COMMITMENT $TEST_SCHNORR_SIGNERS_BLOB) \ + /// --sig $(cast calldata "opChallenge(address,uint128,uint32,bytes32,address,bytes)" $SCRIBE $TEST_POKE_VAL $TEST_POKE_AGE $TEST_SCHNORR_SIGNATURE $TEST_SCHNORR_COMMITMENT $TEST_SCHNORR_FEED_IDS) \ /// -vvv \ /// script/dev/ScribeOptimisticTester.s.sol:ScribeOptimisticTesterScript /// ``` @@ -206,7 +198,7 @@ contract ScribeOptimisticTesterScript is ScribeTesterScript { uint32 age, bytes32 schnorrSignature, address schnorrCommitment, - bytes memory schnorrSignersBlob + bytes memory schnorrFeedIds ) public { // Construct pokeData and schnorrData. IScribe.PokeData memory pokeData; @@ -216,7 +208,7 @@ contract ScribeOptimisticTesterScript is ScribeTesterScript { IScribe.SchnorrData memory schnorrData; schnorrData.signature = schnorrSignature; schnorrData.commitment = schnorrCommitment; - schnorrData.signersBlob = schnorrSignersBlob; + schnorrData.feedIds = schnorrFeedIds; // Create poke message from pokeData. bytes32 pokeMessage = IScribe(self).constructPokeMessage(pokeData); diff --git a/script/dev/ScribeTester.s.sol b/script/dev/ScribeTester.s.sol index d80f386..8601e39 100644 --- a/script/dev/ScribeTester.s.sol +++ b/script/dev/ScribeTester.s.sol @@ -41,8 +41,7 @@ contract ScribeTesterScript is Script { // Setup feeds. LibFeed.Feed[] memory feeds = new LibFeed.Feed[](privKeys.length); for (uint i; i < feeds.length; i++) { - feeds[i] = - LibFeed.newFeed({privKey: privKeys[i], index: uint8(i + 1)}); + feeds[i] = LibFeed.newFeed({privKey: privKeys[i]}); vm.label( feeds[i].pubKey.toAddress(), @@ -93,18 +92,16 @@ contract ScribeTesterScript is Script { // Setup feeds. LibFeed.Feed[] memory feeds = new LibFeed.Feed[](privKeys.length); for (uint i; i < feeds.length; i++) { - feeds[i] = - LibFeed.newFeed({privKey: privKeys[i], index: uint8(i + 1)}); + feeds[i] = LibFeed.newFeed({privKey: privKeys[i]}); vm.label( feeds[i].pubKey.toAddress(), string.concat("Feed #", vm.toString(i + 1)) ); - // Update feed's index. + // Verify feed is lifted. bool isFeed; - uint feedIndex; - (isFeed, feedIndex) = + (isFeed, /*feedId*/ ) = IScribe(self).feeds(feeds[i].pubKey.toAddress()); require( isFeed, @@ -112,8 +109,6 @@ contract ScribeTesterScript is Script { "Private key not feed, privKey=", vm.toString(privKeys[i]) ) ); - - feeds[i].index = uint8(feedIndex); } // Create poke data. diff --git a/script/dev/invalid-oppoker.sh b/script/dev/invalid-oppoker.sh index e5d3ebc..d8abd6a 100755 --- a/script/dev/invalid-oppoker.sh +++ b/script/dev/invalid-oppoker.sh @@ -16,7 +16,8 @@ PRIVATE_KEY= RPC_URL= SCRIBE= -FEED_PRIVATE_KEYS='[2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]' +# Feed private keys taken from script/dev/test-feeds.json. +FEED_PRIVATE_KEYS='[2,3,4,5,6,7,8,9,10,11,12,14,15,16,17,18,19,20,21,22]' while true; do # Lift feeds diff --git a/script/dev/test-feeds.json b/script/dev/test-feeds.json new file mode 100644 index 0000000..2045189 --- /dev/null +++ b/script/dev/test-feeds.json @@ -0,0 +1,1024 @@ +{ + "feeds": [ + { + "privKey": 2, + "address": "0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF" + }, + { + "privKey": 3, + "address": "0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69" + }, + { + "privKey": 4, + "address": "0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718" + }, + { + "privKey": 5, + "address": "0xe1AB8145F7E55DC933d51a18c793F901A3A0b276" + }, + { + "privKey": 6, + "address": "0xE57bFE9F44b819898F47BF37E5AF72a0783e1141" + }, + { + "privKey": 7, + "address": "0xd41c057fd1c78805AAC12B0A94a405c0461A6FBb" + }, + { + "privKey": 8, + "address": "0xF1F6619B38A98d6De0800F1DefC0a6399eB6d30C" + }, + { + "privKey": 9, + "address": "0xF7Edc8FA1eCc32967F827C9043FcAe6ba73afA5c" + }, + { + "privKey": 10, + "address": "0x4CCeBa2d7D2B4fdcE4304d3e09a1fea9fbEb1528" + }, + { + "privKey": 11, + "address": "0x3DA8D322CB2435dA26E9C9fEE670f9fB7Fe74E49" + }, + { + "privKey": 12, + "address": "0xDbc23AE43a150ff8884B02Cea117b22D1c3b9796" + }, + { + "privKey": 14, + "address": "0x5A83529ff76Ac5723A87008c4D9B436AD4CA7d28" + }, + { + "privKey": 15, + "address": "0x8735015837bD10e05d9cf5EA43A2486Bf4Be156F" + }, + { + "privKey": 16, + "address": "0xfaE394561e33e242c551d15D4625309EA4c0B97f" + }, + { + "privKey": 17, + "address": "0x252Dae0A4b9d9b80F504F6418acd2d364C0c59cD" + }, + { + "privKey": 18, + "address": "0x79196B90D1E952C5A43d4847CAA08d50b967c34A" + }, + { + "privKey": 19, + "address": "0x4bd1280852Cadb002734647305AFC1db7ddD6Acb" + }, + { + "privKey": 20, + "address": "0x811da72aCA31e56F770Fc33DF0e45fD08720E157" + }, + { + "privKey": 21, + "address": "0x157bFBEcd023fD6384daD2Bded5DAD7e27Bf92E4" + }, + { + "privKey": 22, + "address": "0x37dA28C050E3c0A1c0aC3BE97913EC038783dA4C" + }, + { + "privKey": 23, + "address": "0x3Bc8287F1D872df4217283b7920D363F13Cf39D8" + }, + { + "privKey": 24, + "address": "0xf4e2B0fcbd0DC4b326d8A52B718A7bb43BdBd072" + }, + { + "privKey": 25, + "address": "0x9a5279029e9A2D6E787c5A09CB068AB3D45e209d" + }, + { + "privKey": 26, + "address": "0xc39677F5F47d5fE65ab24e66750e8FCa127c15BE" + }, + { + "privKey": 27, + "address": "0x1dc728786E09F862E39Be1f39dD218EE37feB68D" + }, + { + "privKey": 28, + "address": "0x636CC65783084b9F370789c90F733DBBeb88925D" + }, + { + "privKey": 29, + "address": "0x4a7A7c2E09209dbE44A582cD92b0eDd7129E74be" + }, + { + "privKey": 30, + "address": "0xA56160A359F2EAa66f5c9df5245542B07339A9a6" + }, + { + "privKey": 31, + "address": "0x6b09D6433a379752157fD1a9E537c5CAe5fa3168" + }, + { + "privKey": 32, + "address": "0x32E77DE0D74a5C7AF861aAEd324c6a4c488142a8" + }, + { + "privKey": 33, + "address": "0x093d49D617a10F26915553255Ec3FEE532d2C12F" + }, + { + "privKey": 34, + "address": "0x138854708D8B603c9b7d4d6e55b6d32D40557F4D" + }, + { + "privKey": 35, + "address": "0x7dc0a40D64d72bb4590652B8f5C687bF7F26400c" + }, + { + "privKey": 36, + "address": "0x9358A525CC25aa571af0BCB5B98fBEAb045a5e36" + }, + { + "privKey": 37, + "address": "0xd8E8EA89D71de89214fA39Ba13bA9FCDDc0d9467" + }, + { + "privKey": 38, + "address": "0xb56eD8f48979e1A948AD129199a600d0562cac51" + }, + { + "privKey": 39, + "address": "0xf65Ac7003E905d72c666bFec1DC0960ecC9d0D6e" + }, + { + "privKey": 41, + "address": "0xf2ADB90aa27a3C61a95C50063B20919d811e1476" + }, + { + "privKey": 42, + "address": "0xae3DfFEE97f92db0201d11CB8877C89738353bCE" + }, + { + "privKey": 43, + "address": "0xEB3025e7aC2764040384316b33476E048961a71F" + }, + { + "privKey": 44, + "address": "0x9e3289708Dc5709926A542fCf260fD4B210461F0" + }, + { + "privKey": 45, + "address": "0x6C23faCE014F20B3ebb65aE96D0D7FF32aB94c17" + }, + { + "privKey": 46, + "address": "0xB83B6241f966B1685C8B2fFce3956E21F35B4DcB" + }, + { + "privKey": 48, + "address": "0x673C638147fe91e4277646d86D5AE82f775EeA5C" + }, + { + "privKey": 53, + "address": "0xe6869CC98283aB53E8a1a5312857Ef0bE9d189FE" + }, + { + "privKey": 55, + "address": "0xa1A625AE13b80A9c48b7C0331C83bc4541aC137f" + }, + { + "privKey": 56, + "address": "0xa33C9D26e1E33b84247dEFCA631c1d30FFc76F5d" + }, + { + "privKey": 57, + "address": "0xF9807a8719AE154E74591CB2d452797707faDF73" + }, + { + "privKey": 59, + "address": "0x366c20b40048556e5682e360997537c3715AcA0e" + }, + { + "privKey": 64, + "address": "0xe0dd44773F7657b11019062879D65F3D9862460c" + }, + { + "privKey": 65, + "address": "0x756Be12856A8F44AB22FdBCbD42B70b843377d09" + }, + { + "privKey": 66, + "address": "0x6f4c950442e1Af093BcfF730381E63Ae9171b87a" + }, + { + "privKey": 67, + "address": "0x4d1Bf28514A4451249908E611366Ec967C3d1558" + }, + { + "privKey": 68, + "address": "0xB0142D883494197B02c6ECE84f571D81bd831124" + }, + { + "privKey": 71, + "address": "0x7a601ffA997cEdE6435aeABf4Fa2091f09e149Ec" + }, + { + "privKey": 72, + "address": "0xa92F4b5c4FddCC37E5139873AC28a4A0a42d68df" + }, + { + "privKey": 73, + "address": "0x850Cc185d6Cae4A7fDFB3dd81F977dD1dF7D6503" + }, + { + "privKey": 74, + "address": "0xB1b7c87e8a0Bf2E7fd1a1C582bd353E4C4529341" + }, + { + "privKey": 75, + "address": "0xFF844Fdb49e00776Ad538Db9EA2f9fA98EC0cAF7" + }, + { + "privKey": 76, + "address": "0x1aC6F9601f2F616badcEa8A0a307e1A3C14767A4" + }, + { + "privKey": 77, + "address": "0xC2aA6271409c10deE630e79df90C968989ccF2B7" + }, + { + "privKey": 78, + "address": "0x883d01EaE6eAAC077e126dDb32CD53550966ED76" + }, + { + "privKey": 79, + "address": "0x127688bBc070DD69a4dB8C3BA5d43909E13d8F77" + }, + { + "privKey": 80, + "address": "0x0b54A50c0409Dab2e63c3566324268ED53Ec019a" + }, + { + "privKey": 81, + "address": "0xafd46e3549CC63d7a240D6177D056857679e6F99" + }, + { + "privKey": 83, + "address": "0xAC32DeF421e36b43629f785Fd04523260E7F2b28" + }, + { + "privKey": 84, + "address": "0xFe6032A0810e90D025a3a39dd29844F964ee102c" + }, + { + "privKey": 85, + "address": "0x5CB6F3E6499D1f068B33351D0CaE4B68cDf501Bf" + }, + { + "privKey": 86, + "address": "0x84B743441B7bDF65cB4293126dB4C1B709d7d95e" + }, + { + "privKey": 89, + "address": "0x2853dC9ca40d012969e25360ccE0D9D326b24a86" + }, + { + "privKey": 90, + "address": "0x802271c02F76701929E1ea772e72783D28e4b60f" + }, + { + "privKey": 91, + "address": "0x7bd2aA0726AC3b9E752b120DE8568E90b0423ae4" + }, + { + "privKey": 93, + "address": "0xA72392cD4be285ab6681dA1bF1C45f0B370Cb7B4" + }, + { + "privKey": 94, + "address": "0xcf484269182Ac2388A4bFE6d19FB0687e3534B7f" + }, + { + "privKey": 95, + "address": "0x994907CB80BfD175F9b0B32672CFDE0091368e2E" + }, + { + "privKey": 97, + "address": "0x440db3AB842910D2a39F4e1be9E017C6823Fb658" + }, + { + "privKey": 99, + "address": "0x24D881139Ee639C2A774B4B1851CB7a9d0fce122" + }, + { + "privKey": 100, + "address": "0xd9A284367b6D3e25A91c91b5A430AF2593886EB9" + }, + { + "privKey": 103, + "address": "0x7231C364597f3BfDB72Cf52b197cc59111e71794" + }, + { + "privKey": 104, + "address": "0x043aEd06383F290Ee28FA02794Ec7215CA099683" + }, + { + "privKey": 105, + "address": "0x0c95931d95694B3ef74071241827C09f25d40620" + }, + { + "privKey": 106, + "address": "0x417f3b59eF57C641283C2300fae0f27fe98D518C" + }, + { + "privKey": 107, + "address": "0xD6B931D8d441B1ec98F55F8Ec8ADb532DC140c78" + }, + { + "privKey": 108, + "address": "0x9220625B1a30680387D542E6b5F753786Ca5530e" + }, + { + "privKey": 110, + "address": "0xB961768b578514Debf079017FF78c47b0A6AdBf6" + }, + { + "privKey": 111, + "address": "0x052b91ad9732D1bcE0dDAe15a4545e5c65D02443" + }, + { + "privKey": 112, + "address": "0x8dF64de79608F0aE9e72ECAe3A400582AeD8101C" + }, + { + "privKey": 113, + "address": "0x0e7B23Cd1fdb7ea3CCC80320AB43843a2f193C36" + }, + { + "privKey": 114, + "address": "0xFBBC41289f834A76e4320aC238512035560467Ee" + }, + { + "privKey": 115, + "address": "0x61E1DA6C7B8B211E6e5Dd921EFe27e73Ad226DAc" + }, + { + "privKey": 117, + "address": "0x2ACf0D6fdaC920081A446868B2f09A8dAc797448" + }, + { + "privKey": 118, + "address": "0x1715eb68AfBA4D516ef1e068b55f5093BB4A2f59" + }, + { + "privKey": 119, + "address": "0x58Bab2f728dc4FC227A4C38CaB2ec93B73B4E828" + }, + { + "privKey": 121, + "address": "0xA01CCA6367A84304b6607B76676C66c360b74741" + }, + { + "privKey": 125, + "address": "0xE8C5025c803F3279D94eA55598E147f601929Bf4" + }, + { + "privKey": 127, + "address": "0xB3087F34EDab33A8182BA29ADEa4D739D9831A94" + }, + { + "privKey": 128, + "address": "0xc6A210606f2EE6e64aFB9584DB054F3476a5Cc66" + }, + { + "privKey": 129, + "address": "0xD01C9d93efC83C00B30f768F832182BEfF65696f" + }, + { + "privKey": 130, + "address": "0x00eDf2d16AfbC028FB1e879559b07997Af79539f" + }, + { + "privKey": 131, + "address": "0xF5d722030D17CA01F2813Af9E7be158D7a037997" + }, + { + "privKey": 133, + "address": "0x0DC8B8eF8457b1e45ac277D65ac5987B547Ba775" + }, + { + "privKey": 134, + "address": "0xdE521346f9327a4314A18B2cda96B2a33603177A" + }, + { + "privKey": 135, + "address": "0x69842e12d6F36f9f93F06086B70795bFc7e02745" + }, + { + "privKey": 136, + "address": "0x9b7BdF6ad17d5fC9a168Acaa24495e52A65f3b79" + }, + { + "privKey": 137, + "address": "0xa2d47D2C42009520075cB15f5855052008d0c44D" + }, + { + "privKey": 139, + "address": "0x839D96957F21E82fBCCA0D42a1f12EF6e1CF82E9" + }, + { + "privKey": 141, + "address": "0xA4f8c598927EaB2f1898F8F2d6F8121578De2344" + }, + { + "privKey": 143, + "address": "0x21289CD01f9f58FC44962B6E213a0fbBd015bEb6" + }, + { + "privKey": 146, + "address": "0x64e582C17Ab7c3b90E171795B504cA3C04108501" + }, + { + "privKey": 148, + "address": "0x5FE015779Fb36006B01f9C5A5DbcAA6FFA56F0c0" + }, + { + "privKey": 150, + "address": "0x271D65AF9a5A7B4cD7AF264f251184c2a4b9e7A3" + }, + { + "privKey": 151, + "address": "0xddf44e34ed40c40624C7B9f20a1030b505a4FAC0" + }, + { + "privKey": 154, + "address": "0x7E9961FA09dd52f945F8143844785cF0E51bb4Ce" + }, + { + "privKey": 155, + "address": "0xf33D2f7d96F92d912Ca8418f9d62eB54c1A9889F" + }, + { + "privKey": 156, + "address": "0xEEc566c793A89f388BbABfC0225183a6a95C4263" + }, + { + "privKey": 157, + "address": "0x2001f8CdCdEEF1bbCC188cA59CF04Fb44133D55a" + }, + { + "privKey": 160, + "address": "0x18Eb36d090eEadF82f3454a6DA690fC398d3EBa1" + }, + { + "privKey": 161, + "address": "0xd2431CA38735C2fd438e2cAa23F094191D89675b" + }, + { + "privKey": 168, + "address": "0xE2A09565167D4e3F826ADeC6bEF82B97e0A4383f" + }, + { + "privKey": 171, + "address": "0x07748403082b29a45abD6C124A37E6B14e6B1803" + }, + { + "privKey": 173, + "address": "0x5181bE40152CAaBa8e123a55b7762755D4e8e416" + }, + { + "privKey": 174, + "address": "0x9481Da7766c043EEfeECC9589ee7ADE61316b0ff" + }, + { + "privKey": 175, + "address": "0x42Aba3530DD1cCb1dda27BFaa7C6a832cfDB4446" + }, + { + "privKey": 184, + "address": "0x8eE8813FB9d41cc58EF87D28b36E948B1234e71d" + }, + { + "privKey": 186, + "address": "0x31eB18Dd6f5A8064Ab750eABb281Cf162f43CCD0" + }, + { + "privKey": 190, + "address": "0x3c1638a25aD7e8c2a84B53b661dD1BD048407E8f" + }, + { + "privKey": 191, + "address": "0x2eeDf8a799B73BC02E4664183eB72422C377153B" + }, + { + "privKey": 192, + "address": "0xcdeF6f23a26F960B53468f497967CE74c026AF52" + }, + { + "privKey": 193, + "address": "0x0A2035683FE5587B0B09DB0e757306aD729FE6c1" + }, + { + "privKey": 197, + "address": "0x556330e8d92912cCf133851BA03abD2DB70Da404" + }, + { + "privKey": 200, + "address": "0x5304FB08724D73f2bB5E04C582407c33cDE6c8d3" + }, + { + "privKey": 203, + "address": "0xD5CC10c45fc0f9f956Acd7559F61EdBFeC9F6C3d" + }, + { + "privKey": 204, + "address": "0x381c7A71035BDb42fB5D77523Df2Ff00d9f9Df1b" + }, + { + "privKey": 205, + "address": "0x45CdbEEA730D8212F451A6A8d0Eb5998B04CccCa" + }, + { + "privKey": 207, + "address": "0x598E94EB5E050045272d8417F6AB363bD874D568" + }, + { + "privKey": 211, + "address": "0x1Cd21f00b58894260f7ABeD65ad23DCe3CeA0226" + }, + { + "privKey": 212, + "address": "0x26324733d604aBb6176cF18e4f4a0624CeedDC09" + }, + { + "privKey": 216, + "address": "0x3F588A72d94d0D0986B112C671c2343320a19386" + }, + { + "privKey": 217, + "address": "0x7cFa9eEE1D752da599211BC8a68d0687708DaBFC" + }, + { + "privKey": 219, + "address": "0xC4ad60337B04fC721912531a52a5D77878293FB9" + }, + { + "privKey": 220, + "address": "0xFc5Ba3041F750f9B6820Ce066C153EB396aAC1ff" + }, + { + "privKey": 224, + "address": "0xf084BbaaBEe1a700a8FAa404027DB620A5Aa0059" + }, + { + "privKey": 225, + "address": "0x602d562b4Ef2544f851587619B56F77a9d965d45" + }, + { + "privKey": 227, + "address": "0x11Eb17B20113AE923D72E52870D40BF59A08B40D" + }, + { + "privKey": 230, + "address": "0x161c2E10407e2A87959C0bae1f342C80EaeA28f3" + }, + { + "privKey": 234, + "address": "0x4F81e991f76276A17cA92a1321f37189b1727F77" + }, + { + "privKey": 235, + "address": "0xBa95e317EAd06b55c8B70276FC63904b3339dFa1" + }, + { + "privKey": 237, + "address": "0x73377d6228266393747eFa710017872d6dd5B9A6" + }, + { + "privKey": 241, + "address": "0x7492EbbC1E7F2838Fc7191eDc031581d5712978A" + }, + { + "privKey": 242, + "address": "0xC0Af3981f9C0dfcB8955Fea07a3e4F23806FAB51" + }, + { + "privKey": 243, + "address": "0x8621Dd642245dF371b584b48c081e8863313A70d" + }, + { + "privKey": 249, + "address": "0xA8CE5C40c4aA9278DdEaA418e775985549960E7A" + }, + { + "privKey": 252, + "address": "0x90f022E3ca8453F5E5765cd3054003b544526eec" + }, + { + "privKey": 254, + "address": "0x0311afD3Bc2Ae250D5f9F2706BAE2eF4164d6912" + }, + { + "privKey": 255, + "address": "0x5044a80bD3eff58302e638018534BbDA8896c48A" + }, + { + "privKey": 263, + "address": "0x14a6E94f5c4F109d31ec0ff3cd002561b2525bcc" + }, + { + "privKey": 269, + "address": "0x702B1E972fE11B34148287785d76928F9a9c3A76" + }, + { + "privKey": 270, + "address": "0x222c1424ad90a40B505Be6dF879189668984a9C8" + }, + { + "privKey": 271, + "address": "0xFdb4750B55B1c21E632F140921b9753a26446e20" + }, + { + "privKey": 273, + "address": "0x718811e2d1170db844d0c5de6D276b299f2916a9" + }, + { + "privKey": 279, + "address": "0x7fBdFba6b300A5F54a71C0a3D047BA8F21610e4a" + }, + { + "privKey": 287, + "address": "0x3AD2A5A73385E48e476386ACccEEea79692ABe2f" + }, + { + "privKey": 288, + "address": "0x4Ec254AD8A448b2773D816083efc5441a2fC8ea4" + }, + { + "privKey": 291, + "address": "0x476C88ED464EFD251a8b18Eb84785F7C46807873" + }, + { + "privKey": 292, + "address": "0x57EF22b9b9C791850FE16b9Fcb79407aCD151478" + }, + { + "privKey": 293, + "address": "0xdA5cceAf028127E7Ce75d841295170C0e7Cb4113" + }, + { + "privKey": 296, + "address": "0x0130c3D5e6Bb374aCa10f0026700c671aE82cd1B" + }, + { + "privKey": 297, + "address": "0xC11B8c4B34d627a40dA72b065d3B67002EE5EC9F" + }, + { + "privKey": 299, + "address": "0xAd3EAF2Bf6361a1138Cd301f65FA19BE86E65f74" + }, + { + "privKey": 301, + "address": "0x3577496a436bC4E85842e8061a287D88B1e73523" + }, + { + "privKey": 305, + "address": "0xD3ff90f011b0160D4d37067E037EABd7d2041663" + }, + { + "privKey": 307, + "address": "0x2c9410ACEfF398dF7eD4e1490c5290a3eA3DF4Fb" + }, + { + "privKey": 308, + "address": "0x6Db15e33e66739809758684e431952b572924ebc" + }, + { + "privKey": 309, + "address": "0xb77b1AeA5601396Ee1f9BEAB2A0825a027422f11" + }, + { + "privKey": 313, + "address": "0xbd68a0e6A1dE1ffE12eBB7783d2f8F7973c8969d" + }, + { + "privKey": 315, + "address": "0xdF5b9234F7ee51FDBEDdB29aF433Aec4083e5f18" + }, + { + "privKey": 319, + "address": "0xD77b6f0AebAC54FB80f7c0064aBFb7114ad89478" + }, + { + "privKey": 320, + "address": "0x8A9D78FaC62b156F810beC0717e1c0eA13aeeB75" + }, + { + "privKey": 321, + "address": "0xBc737ac2Cc40ed40dccfd8746CFcEB719c5ACA12" + }, + { + "privKey": 322, + "address": "0x5EAa383ccd66f65736a01e5219E2e0407737062b" + }, + { + "privKey": 326, + "address": "0x6eeD3A9d19916ba5E21a8f7b4d60d6cA0965C974" + }, + { + "privKey": 334, + "address": "0x2F2F681Ee0b1504500c9962435616E32Aa64dEFd" + }, + { + "privKey": 336, + "address": "0x29A3b098572e6C7A53065Cc0E18eF6d9f474f4fE" + }, + { + "privKey": 338, + "address": "0x1BB93559cfB76098A2c54ea963d88609afd4810A" + }, + { + "privKey": 343, + "address": "0x5D231c7C81EE99907BF6B2FD2947Fa4557de219c" + }, + { + "privKey": 348, + "address": "0x2D5cEfE9dBc4D5274ea00C6b870c11d6f0265c7e" + }, + { + "privKey": 353, + "address": "0x23952e6b6aE9A89FDe6204bE4312cc9E4fE0c59a" + }, + { + "privKey": 357, + "address": "0x560697e82e48323b827F26862be60C5b19c1d2CF" + }, + { + "privKey": 364, + "address": "0xEa6ffDCcf9167E754531553D3F7b743fB296ee72" + }, + { + "privKey": 365, + "address": "0x403398d0775A70a233Cb00551422De5d620b6842" + }, + { + "privKey": 372, + "address": "0x08963ADC118C91aed106aE1987bfd53fc3011249" + }, + { + "privKey": 373, + "address": "0x5B3FCD13cf91f5C1a9724116333A570a2CA2116b" + }, + { + "privKey": 374, + "address": "0xc8ff52ecE34a58D1f56ab25A0D50e6350B44e739" + }, + { + "privKey": 379, + "address": "0xaa7625e14fC367C8Cbc2BaDAB9935e477164520f" + }, + { + "privKey": 385, + "address": "0x46FA34F88b8612006f78F4655aeFa624DdD93e19" + }, + { + "privKey": 386, + "address": "0xF8b10dF973Ce20Ed07E70Eb3AFc9C2E35c06225b" + }, + { + "privKey": 390, + "address": "0x78B59e306274Dd4C0AF3195Fc89350a3dC407dE4" + }, + { + "privKey": 394, + "address": "0xb63bCAf2DF4b621E83caE95A1364b8a0ffb40998" + }, + { + "privKey": 400, + "address": "0x101cA1E6e819Fe852B7C1BFF5cA9eE1820F9d86F" + }, + { + "privKey": 406, + "address": "0x3EfA17AAe19619Ea3a061a50a3dbD5A4657d1267" + }, + { + "privKey": 412, + "address": "0x8b7421B86d9fF375F13416FbB57956d63504fDE9" + }, + { + "privKey": 413, + "address": "0xD1A0Da208fDE814c052222FBE9a0acC5F077b7a0" + }, + { + "privKey": 415, + "address": "0xCed1B0433c1347A750e2035a26ebdBd798992Fe7" + }, + { + "privKey": 419, + "address": "0xC7346965bfeCFa8A065D7C0A55FbCD15d8314E0b" + }, + { + "privKey": 432, + "address": "0x8C6134AcfdfE43Ae1F1c8E6d9e2aa201C2246942" + }, + { + "privKey": 437, + "address": "0xEfb3f853E9666F5d34E001eA27aa10C8C6d7dF16" + }, + { + "privKey": 448, + "address": "0xE75721D039E8D1fEAA9e59b19cE344A2B1eA6751" + }, + { + "privKey": 452, + "address": "0x333fe004b28627eA079682F37eB6102657D9b835" + }, + { + "privKey": 476, + "address": "0xBb8A8FE4f12953a5840C6570d5e836079329BE46" + }, + { + "privKey": 483, + "address": "0xCa7B487EF7E9E78A0462d1870C4Bd75de321f32A" + }, + { + "privKey": 487, + "address": "0x8fF0eb618c061e00a3fAB5Fc9B8a0f2F24547d48" + }, + { + "privKey": 488, + "address": "0xbf4CAB99a68355608Bed736B3B945aCd3C1Ee298" + }, + { + "privKey": 490, + "address": "0xC5229a9Aba6247D2B4ab61954735A972D08A74aF" + }, + { + "privKey": 491, + "address": "0x5415c45e539fa1D0E3b81C12e706F4087a04c16C" + }, + { + "privKey": 496, + "address": "0x98f3E259e91391E7ba7d2F2D0E8D60D9c6dB7E84" + }, + { + "privKey": 500, + "address": "0x3447659E07332bb8D6E2E1985FB12e8fD6175645" + }, + { + "privKey": 512, + "address": "0x06d31BD867343e5A9D8A82b99618253b38f63b8c" + }, + { + "privKey": 536, + "address": "0x022c53567A99D26aAbDd033B4Efef258ED6DD76A" + }, + { + "privKey": 538, + "address": "0x48159CF1F76426b2ac5ad8208e1B3E5517438D43" + }, + { + "privKey": 540, + "address": "0x9510BB587234430871d9246C98Ece4a13Fb9116d" + }, + { + "privKey": 549, + "address": "0x6282DE81b2455222C4B15159b36c03F55d4903D6" + }, + { + "privKey": 570, + "address": "0xAB8ECD9c951F7F57b77b3c4578C7e9c26c4eE8D9" + }, + { + "privKey": 572, + "address": "0xB40d73153EA796AF4e5Eef4626942022aaB84175" + }, + { + "privKey": 595, + "address": "0xEDdE91eD94463c10aC9B0001edCA3C068d8E226b" + }, + { + "privKey": 606, + "address": "0xB2AC99BC1140e9F9211843dAb96374414702A64A" + }, + { + "privKey": 608, + "address": "0xCC7EF4957B0816607eBcE0a00FA5861ACf95E463" + }, + { + "privKey": 625, + "address": "0x5275D06Bc9dE3661783331380a810Da55cD31544" + }, + { + "privKey": 637, + "address": "0xe34ac72Ca84deEdF06f8B3fc74114ADbb5a6c507" + }, + { + "privKey": 638, + "address": "0x1934c9b2AAFd1D902987951858584F80bf2CE77f" + }, + { + "privKey": 647, + "address": "0x76da31D0e141AD2f7b7D8A2a3Eb86C387b02D838" + }, + { + "privKey": 654, + "address": "0xdC249431F31b36439071A2a31B132E3775178cB2" + }, + { + "privKey": 657, + "address": "0x9DB78Ef366F0AB9A7B1C0e12b4371C8C922d2e16" + }, + { + "privKey": 670, + "address": "0x778eC180D7286B8fea84a2d42429965F35ec16F6" + }, + { + "privKey": 671, + "address": "0x0FC4530b8F6FE6e6a73453CDE827b9C695a3ebe5" + }, + { + "privKey": 679, + "address": "0xbEc5c09999375F60f91633a30bfED85440B10496" + }, + { + "privKey": 691, + "address": "0xC92Fb2E3F1a8f9861876ee812DfdEdb3e845D13A" + }, + { + "privKey": 702, + "address": "0x89cDb38D3aAc16f35d280bf8e85261F2Cc1CB082" + }, + { + "privKey": 790, + "address": "0x9fBe67E4A55BebC5A91886CF53963d487EbA5B6C" + }, + { + "privKey": 813, + "address": "0xcb16f249831588C81f80da9995867F27C627e3F2" + }, + { + "privKey": 819, + "address": "0x9CaD951a57D174A8F0A425D5Ee6920415165fdAA" + }, + { + "privKey": 823, + "address": "0xa636D0d79DCecc69Ef6b546aCaC88232867BE2E0" + }, + { + "privKey": 825, + "address": "0x6ae34Ce38B5491A4b2Ec31Fcd9Ff69ba4bBA0e45" + }, + { + "privKey": 836, + "address": "0x493c8bC625ce5Ca0B9cdd1eA9D939757721480eE" + }, + { + "privKey": 844, + "address": "0x300D76C7145753f6f2515f150F39a14A6f198F7F" + }, + { + "privKey": 865, + "address": "0x966F91FCA291e85C7d1024EA27f62d28D8fd3Fd7" + }, + { + "privKey": 878, + "address": "0x91add7f40C5447fE8CfB9Fb3c9A8d19fD644630c" + }, + { + "privKey": 903, + "address": "0x8247CDE48C7e7c4f95dfF6647b566a533934D224" + }, + { + "privKey": 916, + "address": "0x438Ca190209A8E7C14A92198C575Eae3f92152D0" + }, + { + "privKey": 947, + "address": "0xE4682E4379706A7a84fF6c967e7C0816251b8CfB" + }, + { + "privKey": 967, + "address": "0x39eB22b26d5Ef4708Bc9e90fa394d35349E7F583" + }, + { + "privKey": 1011, + "address": "0x657551717B4045e2A31bf4f0Db82f5A131510Aff" + }, + { + "privKey": 1028, + "address": "0x1FD03A5a9408f206065fF7A1788A3045C08A0779" + }, + { + "privKey": 1133, + "address": "0x660CD9db3944358B0FCf191b80Afc3005F3032FE" + }, + { + "privKey": 1216, + "address": "0xec470B5aC4ADfa27577967AE0f6A74fD89f9AFB0" + }, + { + "privKey": 1238, + "address": "0x97c004220a1c96F377C58AFc2ebAb589E25455Ef" + } + ] +} diff --git a/script/libs/LibFeed.sol b/script/libs/LibFeed.sol index 46c81b8..6d31346 100644 --- a/script/libs/LibFeed.sol +++ b/script/libs/LibFeed.sol @@ -28,29 +28,21 @@ library LibFeed { Vm(address(uint160(uint(keccak256("hevm cheat code"))))); /// @dev Feed encapsulates a private key, derived public key, and the - /// public keys index in a Scribe instance. + /// corresponding feed id. struct Feed { uint privKey; LibSecp256k1.Point pubKey; - uint8 index; + uint8 id; } - /// @dev Returns a new feed instance with private key `privKey` and feed - /// index 0. Note that 0 is never a valid index! + /// @dev Returns a new feed instance with private key `privKey`. function newFeed(uint privKey) internal returns (Feed memory) { - return newFeed(privKey, 0); - } + LibSecp256k1.Point memory pubKey = privKey.derivePublicKey(); - /// @dev Returns a new feed instance with private key `privKey` and feed - /// index `index` in a Scribe instance. - function newFeed(uint privKey, uint8 index) - internal - returns (Feed memory) - { return Feed({ privKey: privKey, - pubKey: privKey.derivePublicKey(), - index: index + pubKey: pubKey, + id: uint8(uint(uint160(pubKey.toAddress())) >> 152) }); } @@ -77,7 +69,7 @@ library LibFeed { return IScribe.SchnorrData({ signature: bytes32(signature), commitment: commitment, - signersBlob: abi.encodePacked(self.index) + feedIds: abi.encodePacked(self.id) }); } @@ -94,96 +86,16 @@ library LibFeed { } (uint signature, address commitment) = privKeys.signMessage(message); - // Create signersBlob with sorted indexes. - bytes memory signersBlob; - uint8[] memory sortedIndexes = selfs.getIndexesSortedByAddress(); - for (uint i; i < sortedIndexes.length; i++) { - signersBlob = abi.encodePacked(signersBlob, sortedIndexes[i]); - } - - return IScribe.SchnorrData({ - signature: bytes32(signature), - commitment: commitment, - signersBlob: signersBlob - }); - } - - /// @dev Returns a Schnorr multi-signature (aggregated signature) of type - /// IScribe.SchnorrData signing `message` via `selfs`' private keys. - /// Note that SchnorrData's signersBlob is not ordered and the signature - /// therefore unacceptable for Scribe. - function signSchnorr_withoutOrderingSignerIndexes( - Feed[] memory selfs, - bytes32 message - ) internal returns (IScribe.SchnorrData memory) { - // Create multi-signature. - uint[] memory privKeys = new uint[](selfs.length); + // Create blob of feedIds. + bytes memory feedIds; for (uint i; i < selfs.length; i++) { - privKeys[i] = selfs[i].privKey; - } - (uint signature, address commitment) = privKeys.signMessage(message); - - // Create list of signerIndexes. - uint8[] memory signerIndexes = new uint8[](selfs.length); - for (uint i; i < selfs.length; i++) { - signerIndexes[i] = selfs[i].index; - } - - // Create signersBlob. - bytes memory signersBlob; - for (uint i; i < signerIndexes.length; i++) { - signersBlob = abi.encodePacked(signersBlob, signerIndexes[i]); + feedIds = abi.encodePacked(feedIds, selfs[i].id); } return IScribe.SchnorrData({ signature: bytes32(signature), commitment: commitment, - signersBlob: signersBlob + feedIds: feedIds }); } - - /// @dev Returns the list of `selfs` indexes sorted by `selfs`' addresses. - function getIndexesSortedByAddress(Feed[] memory selfs) - internal - pure - returns (uint8[] memory) - { - // Create array of feeds' indexes. - uint8[] memory indexes = new uint8[](selfs.length); - for (uint i; i < selfs.length; i++) { - indexes[i] = selfs[i].index; - } - - // Create array of feeds' addresses. - address[] memory addrs = new address[](selfs.length); - for (uint i; i < selfs.length; i++) { - addrs[i] = selfs[i].pubKey.toAddress(); - } - - // Sort indexes array based on addresses array. - for (uint i = 1; i < selfs.length; i++) { - for ( - uint j = i; - j > 0 && uint160(addrs[j - 1]) > uint160(addrs[j]); - j-- - ) { - // Swap in indexes array. - { - uint8 tmp = indexes[j]; - indexes[j] = indexes[j - 1]; - indexes[j - 1] = tmp; - } - - // Swap in addresses array. - { - address tmp = addrs[j]; - addrs[j] = addrs[j - 1]; - addrs[j - 1] = tmp; - } - } - } - - // Return sorted list of indexes. - return indexes; - } } diff --git a/src/IScribe.sol b/src/IScribe.sol index 443eb26..4c7e1df 100644 --- a/src/IScribe.sol +++ b/src/IScribe.sol @@ -17,7 +17,7 @@ interface IScribe is IChronicle { struct SchnorrData { bytes32 signature; address commitment; - bytes signersBlob; + bytes feedIds; } /// @dev ECDSAData encapsulates an ECDSA signature. @@ -45,13 +45,13 @@ interface IScribe is IChronicle { /// @param bar The bar security parameter. error BarNotReached(uint8 numberSigners, uint8 bar); - /// @notice Thrown if signature signed by non-feed. - /// @param signer The signer's address not being a feed. - error SignerNotFeed(address signer); + /// @notice Thrown if given feed id invalid. + /// @param feedId The invalid feed id. + error InvalidFeedId(uint8 feedId); - /// @notice Thrown if signer indexes are not encoded so that their - /// addresses are in ascending order. - error SignersNotOrdered(); + /// @notice Thrown if double signing attempted. + /// @param feedId The id of the feed attempting to double sign. + error DoubleSigningAttempted(uint8 feedId); /// @notice Thrown if Schnorr signature verification failed. error SchnorrSignatureInvalid(); @@ -65,17 +65,17 @@ interface IScribe is IChronicle { /// @notice Emitted when new feed lifted. /// @param caller The caller's address. /// @param feed The feed address lifted. - /// @param index The feed's index identifier. + /// @param feedId The feed's id. event FeedLifted( - address indexed caller, address indexed feed, uint indexed index + address indexed caller, address indexed feed, uint8 indexed feedId ); /// @notice Emitted when feed dropped. /// @param caller The caller's address. /// @param feed The feed address dropped. - /// @param index The feed's index identifier. + /// @param feedId The feed's id. event FeedDropped( - address indexed caller, address indexed feed, uint indexed index + address indexed caller, address indexed feed, uint8 indexed feedId ); /// @notice Emitted when bar updated. @@ -93,12 +93,6 @@ interface IScribe is IChronicle { view returns (bytes32 feedRegistrationMessage); - /// @notice The maximum number of feed lifts supported. - /// @dev Note that the constraint comes from feed's indexes being encoded as - /// uint8 in SchnorrData.signersBlob. - /// @return maxFeeds The maximum number of feed lifts supported. - function maxFeeds() external view returns (uint maxFeeds); - /// @notice Returns the bar security parameter. /// @return bar The bar security parameter. function bar() external view returns (uint8 bar); @@ -131,6 +125,7 @@ interface IScribe is IChronicle { /// @notice Pokes the oracle. /// @dev Expects `pokeData`'s age to be greater than the timestamp of the /// last successful poke. + /// @dev Expects `pokeData`'s age to not be greater than the current time. /// @dev Expects `schnorrData` to prove `pokeData`'s integrity. /// See `isAcceptableSchnorrSignatureNow(bytes32,SchnorrData)(bool)`. /// @param pokeData The PokeData being poked. @@ -143,7 +138,7 @@ interface IScribe is IChronicle { /// currently acceptable for message `message`. /// @dev Note that a valid Schnorr signature is only acceptable if the /// signature was signed by exactly bar many feeds. - /// For more info, see `bar()(uint8)` and `feeds()(address[],uint[])`. + /// For more info, see `bar()(uint8)` and `feeds()(address[],uint8[])`. /// @dev Note that bar and feeds are configurable, meaning a once acceptable /// Schnorr signature may become unacceptable in the future. /// @param message The message expected to be signed via `schnorrData`. @@ -160,73 +155,74 @@ interface IScribe is IChronicle { /// @dev The message is defined as: /// H(tag ‖ H(wat ‖ pokeData)), where H() is the keccak256 function. /// @param pokeData The pokeData to create the message for. - /// @return Message for `pokeData`. + /// @return pokeMessage Message for `pokeData`. function constructPokeMessage(PokeData calldata pokeData) external view - returns (bytes32); + returns (bytes32 pokeMessage); - /// @notice Returns whether address `who` is a feed and its feed index - /// identifier. + /// @notice Returns whether address `who` is a feed and its feed id. /// @param who The address to check. /// @return isFeed True if `who` is feed, false otherwise. - /// @return feedIndex Non-zero if `who` is feed, zero otherwise. + /// @return feedId The feed id for address `who`. function feeds(address who) external view - returns (bool isFeed, uint feedIndex); - - /// @notice Returns whether feedIndex `index` maps to a feed and, if so, - /// the feed's address. - /// @param index The feedIndex to check. - /// @return isFeed True if `index` maps to a feed, false otherwise. - /// @return feed Address of the feed with feedIndex `index` if `index` maps - /// to feed, zero-address otherwise. - function feeds(uint index) + returns (bool isFeed, uint8 feedId); + + /// @notice Returns whether feed id `feedId` is a feed and, if so, the + /// feed's address. + /// @param feedId The feed id to check. + /// @return isFeed True if `feedId` is a feed, false otherwise. + /// @return feed Address of the feed with id `feedId` if `feedId` is a feed, + /// zero-address otherwise. + function feeds(uint8 feedId) external view returns (bool isFeed, address feed); - /// @notice Returns list of feed addresses and their index identifiers. + /// @notice Returns list of feed addresses and corresponding feed ids. + /// @dev Note that this function has a high gas consumption and is not + /// intended to be called onchain. /// @return feeds List of feed addresses. - /// @return feedIndexes List of feed's indexes. + /// @return feedIds List of feed ids. function feeds() external view - returns (address[] memory feeds, uint[] memory feedIndexes); + returns (address[] memory feeds, uint8[] memory feedIds); /// @notice Lifts public key `pubKey` to being a feed. /// @dev Only callable by auth'ed address. - /// @dev The message expected to be signed by `ecdsaData` is defined as via - /// `feedRegistrationMessage()(bytes32)` function. + /// @dev The message expected to be signed by `ecdsaData` is defined via + /// `feedRegistrationMessage()(bytes32)`. /// @param pubKey The public key of the feed. /// @param ecdsaData ECDSA signed message by the feed's public key. - /// @return The feed index of the newly lifted feed. + /// @return feedId The id of the newly lifted feed. function lift(LibSecp256k1.Point memory pubKey, ECDSAData memory ecdsaData) external - returns (uint); + returns (uint8 feedId); /// @notice Lifts public keys `pubKeys` to being feeds. /// @dev Only callable by auth'ed address. - /// @dev The message expected to be signed by `ecdsaDatas` is defined as via - /// `feedRegistrationMessage()(bytes32)` function. + /// @dev The message expected to be signed by `ecdsaDatas` is defined via + /// `feedRegistrationMessage()(bytes32)`. /// @param pubKeys The public keys of the feeds. /// @param ecdsaDatas ECDSA signed message by the feeds' public keys. - /// @return List of feed indexes of the newly lifted feeds. + /// @return List of feed ids of the newly lifted feeds. function lift( LibSecp256k1.Point[] memory pubKeys, ECDSAData[] memory ecdsaDatas - ) external returns (uint[] memory); + ) external returns (uint8[] memory); - /// @notice Drops feed with index `feedIndex` from being a feed. + /// @notice Drops feed with id `feedId`. /// @dev Only callable by auth'ed address. - /// @param feedIndex The feed index identifier of the feed to drop. - function drop(uint feedIndex) external; + /// @param feedId The feed id to drop. + function drop(uint8 feedId) external; - /// @notice Drops feeds with indexes `feedIndexes` from being feeds. + /// @notice Drops feeds with ids' `feedIds`. /// @dev Only callable by auth'ed address. - /// @param feedIndexes The feed's index identifiers of the feeds to drop. - function drop(uint[] memory feedIndexes) external; + /// @param feedIds The feed ids to drop. + function drop(uint8[] memory feedIds) external; /// @notice Updates the bar security parameters to `bar`. /// @dev Only callable by auth'ed address. diff --git a/src/IScribeOptimistic.sol b/src/IScribeOptimistic.sol index 5bb86f9..340c624 100644 --- a/src/IScribeOptimistic.sol +++ b/src/IScribeOptimistic.sol @@ -8,8 +8,7 @@ interface IScribeOptimistic is IScribe { /// in challenge period. error InChallengePeriod(); - /// @notice Thrown if opChallenge called while no opPoke exists thats - /// challengeable. + /// @notice Thrown if opChallenge called while no challengeable opPoke exists. error NoOpPokeToChallenge(); /// @notice Thrown if opChallenge called with SchnorrData not matching @@ -19,6 +18,10 @@ interface IScribeOptimistic is IScribe { /// argument. error SchnorrDataMismatch(uint160 gotHash, uint160 wantHash); + /// @notice Thrown if opPoke called with non-feed ECDSA signature. + /// @param signer The ECDSA signature's signer. + error SignerNotFeed(address signer); + /// @notice Emitted when oracles was successfully opPoked. /// @param caller The caller's address. /// @param opFeed The feed that signed the opPoke. @@ -51,7 +54,7 @@ interface IScribeOptimistic is IScribe { /// @notice Emitted when an opPoke dropped. /// @dev opPoke's are dropped if security parameters are updated that could - /// lead to an initially valid opPoke becoming invalid or an opPoke was + /// lead to an initially valid opPoke becoming invalid or if an opPoke /// was successfully challenged. /// @param caller The caller's address. /// @param pokeData The pokeData dropped. @@ -80,6 +83,7 @@ interface IScribeOptimistic is IScribe { /// @notice Optimistically pokes the oracle. /// @dev Expects `pokeData`'s age to be greater than the timestamp of the /// last successful poke. + /// @dev Expects `pokeData`'s age to not be greater than the current time. /// @dev Expects `ecdsaData` to be a signature from a feed. /// @dev Expects `ecdsaData` to prove the integrity of the `pokeData` and /// `schnorrData`. @@ -122,9 +126,9 @@ interface IScribeOptimistic is IScribe { SchnorrData calldata schnorrData ) external view returns (bytes32 opPokeMessage); - /// @notice Returns the feed index of the feed last opPoke'd. - /// @return opFeedIndex Feed index of the feed last opPoke'd. - function opFeedIndex() external view returns (uint8 opFeedIndex); + /// @notice Returns the feed id of the feed last opPoke'd. + /// @return opFeedId Feed id of the feed last opPoke'd. + function opFeedId() external view returns (uint8 opFeedId); /// @notice Returns the opChallengePeriod security parameter. /// @return opChallengePeriod The opChallengePeriod security parameter. diff --git a/src/Scribe.sol b/src/Scribe.sol index 76f71aa..acd0bf4 100644 --- a/src/Scribe.sol +++ b/src/Scribe.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.16; +import {console2} from "forge-std/console2.sol"; + import {IChronicle} from "chronicle-std/IChronicle.sol"; import {Auth} from "chronicle-std/auth/Auth.sol"; import {Toll} from "chronicle-std/toll/Toll.sol"; @@ -13,7 +15,7 @@ import {LibSchnorrData} from "./libs/LibSchnorrData.sol"; /** * @title Scribe - * @custom:version 1.1.0 + * @custom:version 2.0.0 * * @notice Efficient Schnorr multi-signature based Oracle */ @@ -23,9 +25,6 @@ contract Scribe is IScribe, Auth, Toll { using LibSecp256k1 for LibSecp256k1.JacobianPoint; using LibSchnorrData for SchnorrData; - /// @inheritdoc IScribe - uint public constant maxFeeds = type(uint8).max - 1; - /// @inheritdoc IScribe uint8 public constant decimals = 18; @@ -40,20 +39,12 @@ contract Scribe is IScribe, Auth, Toll { /// @inheritdoc IChronicle bytes32 public immutable wat; - /// @dev The storage slot of _pubKeys[0]. - uint internal immutable SLOT_pubKeys; - // -- Storage -- /// @dev Scribe's current value and corresponding age. PokeData internal _pokeData; - /// @dev List of feeds' public keys. - LibSecp256k1.Point[] internal _pubKeys; - - /// @dev Mapping of feeds' addresses to their public key indexes in - /// _pubKeys. - mapping(address => uint) internal _feeds; + LibSecp256k1.Point[256] internal _pubKeys; /// @inheritdoc IScribe /// @dev Note to have as last in storage to enable downstream contracts to @@ -62,7 +53,10 @@ contract Scribe is IScribe, Auth, Toll { // -- Constructor -- - constructor(address initialAuthed, bytes32 wat_) Auth(initialAuthed) { + constructor(address initialAuthed, bytes32 wat_) + payable + Auth(initialAuthed) + { require(wat_ != 0); // Set wat immutable. @@ -70,17 +64,6 @@ contract Scribe is IScribe, Auth, Toll { // Let initial bar be 2. _setBar(2); - - // Let _pubKeys[0] be the zero point. - _pubKeys.push(LibSecp256k1.ZERO_POINT()); - - // Let SLOT_pubKeys be _pubKeys[0].slot. - uint pubKeysSlot; - assembly ("memory-safe") { - mstore(0x00, _pubKeys.slot) - pubKeysSlot := keccak256(0x00, 0x20) - } - SLOT_pubKeys = pubKeysSlot; } // -- Poke Functionality -- @@ -162,70 +145,66 @@ contract Scribe is IScribe, Auth, Toll { } /// @custom:invariant Reverts iff out of gas. - /// @custom:invariant Runtime is Θ(bar). + /// @custom:invariant Runtime is O(bar). function _verifySchnorrSignature( bytes32 message, SchnorrData calldata schnorrData ) internal view returns (bool, bytes memory) { - // Let signerIndex be the current signer's index read from schnorrData. - uint signerIndex; - // Let signerPubKey be the public key stored for signerIndex. - LibSecp256k1.Point memory signerPubKey; - // Let signer be the address of signerPubKey. - address signer; - // Let lastSigner be the previous processed signer. - address lastSigner; - // Let aggPubKey be the sum of processed signers' public keys. + // Let feedPubKey be the currently processed feed's public key. + LibSecp256k1.Point memory feedPubKey; + // Let feedId be the currently processed feed's id. + uint8 feedId; + // Let aggPubKey be the sum of processed feeds' public keys. // Note that Jacobian coordinates are used. LibSecp256k1.JacobianPoint memory aggPubKey; + // Let bloom be a bloom filter to check for double signing attempts. + uint bloom; - // Fail if number signers unequal to bar. + // Fail if number feeds unequal to bar. // // Note that requiring equality constrains the verification's runtime // from Ω(bar) to Θ(bar). - uint numberSigners = schnorrData.getSignerIndexLength(); - if (numberSigners != bar) { - return (false, _errorBarNotReached(uint8(numberSigners), bar)); + uint numberFeeds = schnorrData.numberFeeds(); + if (numberFeeds != bar) { + return (false, _errorBarNotReached(uint8(numberFeeds), bar)); } - // Initiate signer variables with schnorrData's 0's signer index. - signerIndex = schnorrData.getSignerIndex(0); - signerPubKey = _unsafeLoadPubKeyAt(signerIndex); - signer = signerPubKey.toAddress(); + // Initiate feed variables with schnorrData's 0's feed index. + feedId = schnorrData.loadFeedId(0); + feedPubKey = _sloadPubKey(feedId); - // Fail if signer not feed. - if (signerPubKey.isZeroPoint()) { - return (false, _errorSignerNotFeed(signer)); + // Fail if feed not lifted. + if (feedPubKey.isZeroPoint()) { + return (false, _errorInvalidFeedId(feedId)); } - // Initiate aggPubKey with value of first signerPubKey. - aggPubKey = signerPubKey.toJacobian(); + // Initiate bloom filter with feedId set. + bloom = 1 << feedId; + + // Initiate aggPubKey with value of first feed's public key. + aggPubKey = feedPubKey.toJacobian(); - // Aggregate remaining encoded signers. - for (uint i = 1; i < bar;) { - // Update Signer Variables. - lastSigner = signer; - signerIndex = schnorrData.getSignerIndex(i); - signerPubKey = _unsafeLoadPubKeyAt(signerIndex); - signer = signerPubKey.toAddress(); + for (uint8 i = 1; i < bar;) { + // Update feed variables. + feedId = schnorrData.loadFeedId(i); + feedPubKey = _sloadPubKey(feedId); - // Fail if signer not feed. - if (signerPubKey.isZeroPoint()) { - return (false, _errorSignerNotFeed(signer)); + // Fail if feed not lifted. + if (feedPubKey.isZeroPoint()) { + return (false, _errorInvalidFeedId(feedId)); } - // Fail if signers not strictly monotonically increasing. - // - // Note that this prevents double signing attacks and enforces - // strict ordering. - if (uint160(lastSigner) >= uint160(signer)) { - return (false, _errorSignersNotOrdered()); + // Fail if double signing attempted. + if (bloom & (1 << feedId) != 0) { + return (false, _errorDoubleSigningAttempted(feedId)); } + // Update bloom filter. + bloom |= 1 << feedId; - // assert(aggPubKey.x != signerPubKey.x); // Indicates rogue-key attack + // assert(aggPubKey.x != feedPubKey.x); // Indicates rogue-key attack - // Add signerPubKey to already aggregated public keys. - aggPubKey.addAffinePoint(signerPubKey); + // Add feedPubKey to already aggregated public keys. + aggPubKey.addAffinePoint(feedPubKey); // forgefmt: disable-next-item unchecked { ++i; } @@ -282,7 +261,7 @@ contract Scribe is IScribe, Auth, Toll { { uint val = _pokeData.val; uint age = _pokeData.age; - return (val != 0, val, age); + return val != 0 ? (true, val, age) : (false, 0, 0); } // - MakerDAO Compatibility @@ -329,66 +308,59 @@ contract Scribe is IScribe, Auth, Toll { // -- Public Read Functionality -- /// @inheritdoc IScribe - function feeds(address who) external view returns (bool, uint) { - uint index = _feeds[who]; - // assert(index != 0 ? !_pubKeys[index].isZeroPoint() : true); - return (index != 0, index); + function feeds(address who) external view returns (bool, uint8) { + uint8 feedId = uint8(uint(uint160(who)) >> 152); + + LibSecp256k1.Point memory pubKey = _sloadPubKey(feedId); + bool isFeed = !pubKey.isZeroPoint() && pubKey.toAddress() == who; + + return (isFeed, feedId); } /// @inheritdoc IScribe - function feeds(uint index) external view returns (bool, address) { - if (index >= _pubKeys.length) { - return (false, address(0)); - } + function feeds(uint8 feedId) external view returns (bool, address) { + LibSecp256k1.Point memory pubKey = _sloadPubKey(feedId); - LibSecp256k1.Point memory pubKey = _pubKeys[index]; - if (pubKey.isZeroPoint()) { - return (false, address(0)); - } - - return (true, pubKey.toAddress()); + return pubKey.isZeroPoint() + ? (false, address(0)) + : (true, pubKey.toAddress()); } /// @inheritdoc IScribe - function feeds() external view returns (address[] memory, uint[] memory) { + function feeds() external view returns (address[] memory, uint8[] memory) { // Initiate arrays with upper limit length. - uint upperLimitLength = _pubKeys.length; - address[] memory feedsList = new address[](upperLimitLength); - uint[] memory feedsIndexesList = new uint[](upperLimitLength); + address[] memory feeds_ = new address[](256); + uint8[] memory ids = new uint8[](256); - // Iterate over feeds' public keys. If a public key is non-zero, their - // corresponding address is a feed. - uint ctr; LibSecp256k1.Point memory pubKey; address feed; - uint feedIndex; - for (uint i; i < upperLimitLength;) { - pubKey = _pubKeys[i]; + uint8 id; + uint ctr; + + for (uint i; i < 256;) { + pubKey = _sloadPubKey(uint8(i)); if (!pubKey.isZeroPoint()) { feed = pubKey.toAddress(); - // assert(feed != address(0)); - - feedIndex = _feeds[feed]; - // assert(feedIndex != 0); + id = uint8(uint(uint160(feed)) >> 152); - feedsList[ctr] = feed; - feedsIndexesList[ctr] = feedIndex; + feeds_[ctr] = feed; + ids[ctr] = id; - ctr++; + // forgefmt: disable-next-item + unchecked { ++ctr; } } // forgefmt: disable-next-item unchecked { ++i; } } - // Set length of arrays to number of feeds actually included. assembly ("memory-safe") { - mstore(feedsList, ctr) - mstore(feedsIndexesList, ctr) + mstore(feeds_, ctr) + mstore(ids, ctr) } - return (feedsList, feedsIndexesList); + return (feeds_, ids); } // -- Auth'ed Functionality -- @@ -397,7 +369,7 @@ contract Scribe is IScribe, Auth, Toll { function lift(LibSecp256k1.Point memory pubKey, ECDSAData memory ecdsaData) external auth - returns (uint) + returns (uint8) { return _lift(pubKey, ecdsaData); } @@ -406,24 +378,23 @@ contract Scribe is IScribe, Auth, Toll { function lift( LibSecp256k1.Point[] memory pubKeys, ECDSAData[] memory ecdsaDatas - ) external auth returns (uint[] memory) { + ) external auth returns (uint8[] memory) { require(pubKeys.length == ecdsaDatas.length); - uint[] memory indexes = new uint[](pubKeys.length); + uint8[] memory feedIds = new uint8[](pubKeys.length); for (uint i; i < pubKeys.length;) { - indexes[i] = _lift(pubKeys[i], ecdsaDatas[i]); + feedIds[i] = _lift(pubKeys[i], ecdsaDatas[i]); // forgefmt: disable-next-item unchecked { ++i; } } - // Note that indexes contains duplicates iff duplicate pubKeys provided. - return indexes; + return feedIds; } function _lift(LibSecp256k1.Point memory pubKey, ECDSAData memory ecdsaData) internal - returns (uint) + returns (uint8) { address feed = pubKey.toAddress(); // assert(feed != address(0)); @@ -437,44 +408,43 @@ contract Scribe is IScribe, Auth, Toll { ); require(feed == recovered); - uint index = _feeds[feed]; - if (index == 0) { - _pubKeys.push(pubKey); - index = _pubKeys.length - 1; - _feeds[feed] = index; + uint8 feedId = uint8(uint(uint160(feed)) >> 152); - emit FeedLifted(msg.sender, feed, index); + LibSecp256k1.Point memory sPubKey = _sloadPubKey(feedId); + if (sPubKey.isZeroPoint()) { + _sstorePubKey(feedId, pubKey); - require(index <= maxFeeds); + emit FeedLifted(msg.sender, feed, feedId); + } else { + // Note to be idempotent. However, disallow updating an id's feed + // via lifting without dropping the previous feed. + require(feed == sPubKey.toAddress()); } - return index; + return feedId; } /// @inheritdoc IScribe - function drop(uint feedIndex) external auth { - _drop(msg.sender, feedIndex); + function drop(uint8 feedId) external auth { + _drop(msg.sender, feedId); } /// @inheritdoc IScribe - function drop(uint[] memory feedIndexes) external auth { - for (uint i; i < feedIndexes.length;) { - _drop(msg.sender, feedIndexes[i]); + function drop(uint8[] memory feedIds) external auth { + for (uint i; i < feedIds.length;) { + _drop(msg.sender, feedIds[i]); // forgefmt: disable-next-item unchecked { ++i; } } } - function _drop(address caller, uint feedIndex) internal virtual { - require(feedIndex < _pubKeys.length); - address feed = _pubKeys[feedIndex].toAddress(); - - if (_feeds[feed] != 0) { - emit FeedDropped(caller, feed, _feeds[feed]); + function _drop(address caller, uint8 feedId) internal virtual { + LibSecp256k1.Point memory pubKey = _sloadPubKey(feedId); + if (!pubKey.isZeroPoint()) { + _sstorePubKey(feedId, LibSecp256k1.ZERO_POINT()); - _feeds[feed] = 0; - _pubKeys[feedIndex] = LibSecp256k1.ZERO_POINT(); + emit FeedDropped(caller, pubKey.toAddress(), feedId); } } @@ -494,48 +464,58 @@ contract Scribe is IScribe, Auth, Toll { // -- Internal Helpers -- - /// @dev Halts execution by reverting with `err`. - function _revert(bytes memory err) internal pure { - // assert(err.length != 0); - assembly ("memory-safe") { - let size := mload(err) - let offset := add(err, 0x20) - revert(offset, size) - } - } - - /// @dev Returns the public key at `_pubKeys[index]`, or zero point if - /// `index` out of bounds. - function _unsafeLoadPubKeyAt(uint index) + function _sloadPubKey(uint8 index) internal view returns (LibSecp256k1.Point memory) { - // Push immutable to stack as accessing through assembly not supported. - uint slotPubKeys = SLOT_pubKeys; - LibSecp256k1.Point memory pubKey; assembly ("memory-safe") { - // Note that a pubKey consists of two words. - let realIndex := mul(index, 2) - - // Compute slot of _pubKeys[index]. - let slot := add(slotPubKeys, realIndex) + let slot := add(_pubKeys.slot, shl(1, index)) - // Load _pubKeys[index]'s coordinates to stack. let x := sload(slot) let y := sload(add(slot, 1)) - // Store coordinates in pubKey memory location. mstore(pubKey, x) - mstore(add(pubKey, 0x20), y) + mstore(add(pubKey, 32), y) } - // assert(index < _pubKeys.length || pubKey.isZeroPoint()); - // Note that pubKey is zero if index out of bounds. + // assert( + // pubKey.isZeroPoint() + // || uint(uint160(pubKey.toAddress())) >> 152 == index + // ); + return pubKey; } + function _sstorePubKey(uint8 index, LibSecp256k1.Point memory pubKey) + internal + { + // assert( + // pubKey.isZeroPoint() + // || uint(uint160(pubKey.toAddress())) >> 152 == index + // ); + + assembly ("memory-safe") { + let slot := add(_pubKeys.slot, shl(1, index)) + + let pubKey_x := mload(pubKey) + let pubKey_y := mload(add(pubKey, 32)) + + sstore(slot, pubKey_x) + sstore(add(slot, 1), pubKey_y) + } + } + + function _revert(bytes memory err) internal pure { + // assert(err.length != 0); + assembly ("memory-safe") { + let size := mload(err) + let offset := add(err, 0x20) + revert(offset, size) + } + } + function _errorBarNotReached(uint8 got, uint8 want) internal pure @@ -545,17 +525,23 @@ contract Scribe is IScribe, Auth, Toll { return abi.encodeWithSelector(IScribe.BarNotReached.selector, got, want); } - function _errorSignerNotFeed(address signer) + function _errorInvalidFeedId(uint8 feedId) internal pure returns (bytes memory) { - // assert(_feeds[signer] == 0); - return abi.encodeWithSelector(IScribe.SignerNotFeed.selector, signer); + // assert(_sloadPubKey(feedId).isZeroPoint()); + return abi.encodeWithSelector(IScribe.InvalidFeedId.selector, feedId); } - function _errorSignersNotOrdered() internal pure returns (bytes memory) { - return abi.encodeWithSelector(IScribe.SignersNotOrdered.selector); + function _errorDoubleSigningAttempted(uint8 feedId) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + IScribe.DoubleSigningAttempted.selector, feedId + ); } function _errorSchnorrSignatureInvalid() diff --git a/src/ScribeOptimistic.sol b/src/ScribeOptimistic.sol index 0307a35..7738fc8 100644 --- a/src/ScribeOptimistic.sol +++ b/src/ScribeOptimistic.sol @@ -21,16 +21,13 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { using LibSecp256k1 for LibSecp256k1.Point; using LibSecp256k1 for LibSecp256k1.Point[]; - /// @dev The initial opChallengePeriod set during construction. - uint16 private constant _INITIAL_OP_CHALLENGE_PERIOD = 1 hours; - // -- Storage -- /// @inheritdoc IScribeOptimistic uint16 public opChallengePeriod; /// @inheritdoc IScribeOptimistic - uint8 public opFeedIndex; + uint8 public opFeedId; /// @dev The truncated hash of the schnorrData provided in last opPoke. /// Binds the opFeed to their schnorrData. @@ -50,10 +47,14 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { // -- Constructor and Receive Functionality -- constructor(address initialAuthed, bytes32 wat_) + payable Scribe(initialAuthed, wat_) { // Note to have a non-zero challenge period. - _setOpChallengePeriod(_INITIAL_OP_CHALLENGE_PERIOD); + _setOpChallengePeriod(1 hours); + + // Set maxChallengeReward to type(uint).max. + _setMaxChallengeRewards(type(uint).max); } receive() external payable {} @@ -155,27 +156,25 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { ecdsaData.s ); - // Load signer's index. - uint signerIndex = _feeds[signer]; + // Compute feed id of signer. + uint8 feedId = uint8(uint(uint160(signer)) >> 152); // Revert if signer not feed. - if (signerIndex == 0) { + // assert(_sloadPubKey(id).toAddress() != address(0)); + if (_sloadPubKey(feedId).toAddress() != signer) { revert SignerNotFeed(signer); } - // Store the signerIndex as opFeedIndex and bind them to their provided + // Store the feed's id as opFeedId and bind them to their provided // schnorrData. - // - // Note that cast is safe as _feed's image is [0, _pubKeys.length) and - // _pubKeys' length is bounded by maxFeeds, i.e. type(uint8).max - 1. - opFeedIndex = uint8(signerIndex); + opFeedId = feedId; _schnorrDataCommitment = uint160( uint( keccak256( abi.encodePacked( schnorrData.signature, schnorrData.commitment, - schnorrData.signersBlob + schnorrData.feedIds ) ) ) @@ -222,7 +221,7 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { abi.encodePacked( schnorrData.signature, schnorrData.commitment, - schnorrData.signersBlob + schnorrData.feedIds ) ) ) @@ -260,7 +259,7 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { // Drop opFeed and delete invalid _opPokeData. // Note to use address(this) as caller to indicate self-governed // drop of feed. - _drop(address(this), opFeedIndex); + _drop(address(this), opFeedId); // Pay ETH reward to challenger. uint reward = challengeReward(); @@ -275,6 +274,7 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { return !ok; } + // @todo Why not single public function? /// @inheritdoc IScribeOptimistic function constructOpPokeMessage( PokeData calldata pokeData, @@ -297,7 +297,7 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { pokeData.age, schnorrData.signature, schnorrData.commitment, - schnorrData.signersBlob + schnorrData.feedIds ) ) ) @@ -349,6 +349,8 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { return (pokeData.val, pokeData.age); } + /// @inheritdoc IChronicle + /// @dev Only callable by toll'ed address. function tryReadWithAge() external view @@ -357,7 +359,9 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { returns (bool, uint, uint) { PokeData memory pokeData = _currentPokeData(); - return (pokeData.val != 0, pokeData.val, pokeData.age); + return pokeData.val != 0 + ? (true, pokeData.val, pokeData.age) + : (false, 0, 0); } // - MakerDAO Compatibility @@ -452,8 +456,8 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { _afterAuthedAction(); } - function _drop(address caller, uint feedIndex) internal override(Scribe) { - super._drop(caller, feedIndex); + function _drop(address caller, uint8 feedId) internal override(Scribe) { + super._drop(caller, feedId); _afterAuthedAction(); } @@ -538,6 +542,10 @@ contract ScribeOptimistic is IScribeOptimistic, Scribe { /// @inheritdoc IScribeOptimistic function setMaxChallengeReward(uint maxChallengeReward_) external auth { + _setMaxChallengeRewards(maxChallengeReward_); + } + + function _setMaxChallengeRewards(uint maxChallengeReward_) internal { if (maxChallengeReward != maxChallengeReward_) { emit MaxChallengeRewardUpdated( msg.sender, maxChallengeReward, maxChallengeReward_ diff --git a/src/libs/LibBytes.sol b/src/libs/LibBytes.sol index ebf09e0..7c914d0 100644 --- a/src/libs/LibBytes.sol +++ b/src/libs/LibBytes.sol @@ -15,14 +15,12 @@ library LibBytes { function getByteAtIndex(uint word, uint index) internal pure - returns (uint) + returns (uint8) { - uint result; + uint8 result; assembly ("memory-safe") { result := byte(sub(31, index), word) } - - // Note that the resulting byte is returned as word. return result; } } diff --git a/src/libs/LibSchnorrData.sol b/src/libs/LibSchnorrData.sol index af83d31..31cc9f7 100644 --- a/src/libs/LibSchnorrData.sol +++ b/src/libs/LibSchnorrData.sol @@ -13,33 +13,40 @@ import {LibBytes} from "./LibBytes.sol"; library LibSchnorrData { using LibBytes for uint; - /// @dev Size of a word is 32 bytes, i.e. 256 bits. - uint private constant WORD_SIZE = 32; + /// @dev Mask to compute word index of an array index. + /// + /// @dev Equals `type(uint).max << 5`. + uint private constant WORD_MASK = + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0; + + /// @dev Mask to compute byte index of an array index. + /// + /// @dev Equals `type(uint).max >> (256 - 5)`. + uint private constant BYTE_MASK = 31; - /// @dev Returns the signer index from schnorrData.signersBlob with index - /// `index`. + /// @dev Returns the feedId from schnorrData.feedIds with index `index`. /// - /// @dev Note that schnorrData.signersBlob is big-endian encoded and - /// counting starts at the highest order byte, i.e. the signer index 0 - /// is the highest order byte of schnorrData.signersBlob. + /// @dev Note that schnorrData.feedIds is big-endian encoded and + /// counting starts at the highest order byte, i.e. the feedId with + /// index 0 is the highest order byte of schnorrData.feedIds. /// - /// @custom:example SignersBlob encoding via Solidity: + /// @custom:example Encoding feedIds via Solidity: /// /// ```solidity - /// bytes memory signersBlob; - /// uint8[] memory indexes = someFuncReturningUint8Array(); - /// for (uint i; i < indexes.length; i++) { - /// signersBlob = abi.encodePacked(signersBlob, indexes[i]); + /// bytes memory feedIds; + /// uint8[] memory ids = someFuncReturningUint8Array(); + /// for (uint i; i < ids.length; i++) { + /// feedIds = abi.encodePacked(feedIds, ids[i]); /// } /// ``` /// /// @dev Calldata layout for `schnorrData`: /// - /// [schnorrData] signature -> schnorrData.signature - /// [schnorrData + 0x20] commitment -> schnorrData.commitment - /// [schnorrData + 0x40] offset(signersBlob) - /// [schnorrData + 0x60] len(signersBlob) -> schnorrData.signersBlob.length - /// [schnorrData + 0x80] signersBlob[0] -> schnorrData.signersBlob[0] + /// [schnorrData] signature -> schnorrData.signature + /// [schnorrData + 0x20] commitment -> schnorrData.commitment + /// [schnorrData + 0x40] offset(feedIds) + /// [schnorrData + 0x60] len(feedIds) -> schnorrData.feedIds.length + /// [schnorrData + 0x80] feedIds[0] -> schnorrData.feedIds[0] /// ... /// /// Note that the `schnorrData` variable holds the offset to the @@ -53,50 +60,43 @@ library LibSchnorrData { /// assert(signature == schnorrData.signature) /// ``` /// - /// Note that `offset(signersBlob)` is the offset to `signersBlob[0]` - /// from the index `offset(signersBlob)`. + /// Note that `offset(feedIds)` is the offset to `feedIds[0]` from the + /// index `offset(feedIds)`. /// /// @custom:invariant Reverts iff out of gas. - function getSignerIndex( - IScribe.SchnorrData calldata schnorrData, - uint index - ) internal pure returns (uint) { + function loadFeedId(IScribe.SchnorrData calldata schnorrData, uint8 index) + internal + pure + returns (uint8) + { + uint wordIndex = index & WORD_MASK; + uint byteIndex = (~index) & BYTE_MASK; + uint word; assembly ("memory-safe") { - let wordIndex := mul(div(index, WORD_SIZE), WORD_SIZE) - - // Calldata index for schnorrData.signersBlob[0] is schnorrData's - // offset plus 4 words, i.e. 0x80. - let start := add(schnorrData, 0x80) + // Calldata index for schnorrData.feedIds[0] is schnorrData's offset + // plus 4 words, i.e. 0x80. + let feedIdsOffset := add(schnorrData, 0x80) // Note that reading non-existing calldata returns zero. - // Note that overflow is no concern because index's upper limit is - // bounded by bar, which is of type uint8. - word := calldataload(add(start, wordIndex)) - } - - // Unchecked because the subtrahend is guaranteed to be less than or - // equal to 31 due to being a (mod 32) result. - uint byteIndex; - unchecked { - byteIndex = 31 - (index % WORD_SIZE); + word := calldataload(add(feedIdsOffset, wordIndex)) } return word.getByteAtIndex(byteIndex); } - /// @dev Returns the number of signers encoded in schnorrData.signersBlob. - function getSignerIndexLength(IScribe.SchnorrData calldata schnorrData) + /// @dev Returns the number of feed ids' encoded in schnorrData.feedIds. + function numberFeeds(IScribe.SchnorrData calldata schnorrData) internal pure returns (uint) { - uint index; + uint result; assembly ("memory-safe") { - // Calldata index for schnorrData.signersBlob.length is + // Calldata index for schnorrData.feedIds.length is // schnorrData's offset plus 3 words, i.e. 0x60. - index := calldataload(add(schnorrData, 0x60)) + result := calldataload(add(schnorrData, 0x60)) } - return index; + return result; } } diff --git a/test/EVMTest.sol b/test/EVMTest.sol index 9ba2d8a..8d9fe73 100644 --- a/test/EVMTest.sol +++ b/test/EVMTest.sol @@ -33,10 +33,10 @@ abstract contract EVMTest is Test { uint sSeed ) public { // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); // Let s ∊ [Q, type(uint).max]. - bytes32 s = bytes32(bound(sSeed, LibSecp256k1.Q(), type(uint).max)); + bytes32 s = bytes32(_bound(sSeed, LibSecp256k1.Q(), type(uint).max)); // Create ECDSA signature. (, bytes32 r,) = vm.sign(privKey, keccak256("scribe")); @@ -51,7 +51,7 @@ abstract contract EVMTest is Test { public { // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); // Create ECDSA signature. (,, bytes32 s) = vm.sign(privKey, keccak256("scribe")); diff --git a/test/IScribeOptimisticTest.sol b/test/IScribeOptimisticTest.sol index c2dc784..587205f 100644 --- a/test/IScribeOptimisticTest.sol +++ b/test/IScribeOptimisticTest.sol @@ -59,11 +59,14 @@ abstract contract IScribeOptimisticTest is IScribeTest { function test_Deployment() public override(IScribeTest) { super.test_Deployment(); - // opFeedIndex not set. - assertEq(opScribe.opFeedIndex(), 0); + // opFeedId set to zero. + assertEq(opScribe.opFeedId(), 0); // OpChallengePeriod set to 1 hour. assertEq(opScribe.opChallengePeriod(), 1 hours); + + // MaxChallengeRewards set to type(uint).max. + assertEq(opScribe.maxChallengeReward(), type(uint).max); } // -- Test: Poke -- @@ -72,7 +75,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { uint val; uint age; - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); // Execute opPoke. IScribe.PokeData memory opPokeData; @@ -135,18 +138,18 @@ abstract contract IScribeOptimisticTest is IScribeTest { } } - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.SchnorrData memory schnorrData; IScribe.ECDSAData memory ecdsaData; uint feedIndex; for (uint i; i < pokeDatas.length; i++) { // Select random feed signing opPoke. - feedIndex = bound(feedIndexSeeds[i], 0, feeds.length - 1); + feedIndex = _bound(feedIndexSeeds[i], 0, feeds.length - 1); // Make sure val is non-zero and age not stale. pokeDatas[i].val = - uint128(bound(pokeDatas[i].val, 1, type(uint128).max)); + uint128(_bound(pokeDatas[i].val, 1, type(uint128).max)); // @todo Weird behaviour if compiled via --via-ir. // See comment in testFuzz_opPoke_FailsIf_AgeIsStale(). @@ -191,11 +194,11 @@ abstract contract IScribeOptimisticTest is IScribeTest { function testFuzz_opPoke_FailsIf_AgeIsStale( IScribe.PokeData memory pokeData ) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); vm.assume(pokeData.val != 0); // Let pokeData's age ∊ [1, block.timestamp]. - pokeData.age = uint32(bound(pokeData.age, 1, block.timestamp)); + pokeData.age = uint32(_bound(pokeData.age, 1, block.timestamp)); IScribe.SchnorrData memory schnorrData; schnorrData = feeds.signSchnorr(opScribe.constructPokeMessage(pokeData)); @@ -208,12 +211,12 @@ abstract contract IScribeOptimisticTest is IScribeTest { // Execute opPoke. opScribe.opPoke(pokeData, schnorrData, ecdsaData); - // opPoke'd age is set to block.timestamp. + // opPoke's age is set to block.timestamp. uint lastAge = uint32(block.timestamp); console2.log("lastAge", lastAge); // Set pokeData's age ∊ [0, block.timestamp]. - pokeData.age = uint32(bound(pokeData.age, 0, block.timestamp)); + pokeData.age = uint32(_bound(pokeData.age, 0, block.timestamp)); // Wait until opPokeData finalized. vm.warp(block.timestamp + opScribe.opChallengePeriod()); @@ -242,12 +245,12 @@ abstract contract IScribeOptimisticTest is IScribeTest { function testFuzz_opPoke_FailsIf_AgeIsInTheFuture( IScribe.PokeData memory pokeData ) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); vm.assume(pokeData.val != 0); // Let pokeData's age ∊ [block.timestamp+1, type(uint32).max]. pokeData.age = - uint32(bound(pokeData.age, block.timestamp + 1, type(uint32).max)); + uint32(_bound(pokeData.age, block.timestamp + 1, type(uint32).max)); IScribe.SchnorrData memory schnorrData; schnorrData = feeds.signSchnorr(opScribe.constructPokeMessage(pokeData)); @@ -273,7 +276,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { uint rMask, uint sMask ) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory pokeData; pokeData.val = 1; @@ -302,7 +305,9 @@ abstract contract IScribeOptimisticTest is IScribeTest { ); vm.expectRevert( - abi.encodeWithSelector(IScribe.SignerNotFeed.selector, recovered) + abi.encodeWithSelector( + IScribeOptimistic.SignerNotFeed.selector, recovered + ) ); opScribe.opPoke(pokeData, schnorrData, ecdsaData); } @@ -310,7 +315,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { function testFuzz_opPoke_FailsIf_opPokeDataInChallengePeriodExists( uint warpSeed ) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory pokeData; pokeData.val = 1; @@ -342,7 +347,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { function testFuzz_opChallenge_opPokeDataValidAndNotStale(uint warpSeed) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory pokeData; pokeData.val = 1; @@ -394,7 +399,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { function testFuzz_opChallenge_opPokeDataValidButStale(uint warpSeed) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory pokeData; pokeData.val = 1; @@ -451,7 +456,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { ) public { vm.assume(schnorrSignatureMask != 0 || schnorrCommitmentMask != 0); - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory pokeData; pokeData.val = 1; @@ -526,7 +531,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { } function test_opChallenge_FailsIf_InvalidSchnorrDataGiven() public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory pokeData; pokeData.val = 1; @@ -552,7 +557,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { } function test_opChallenge_FailsIf_CalledSubsequently() public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory pokeData; pokeData.val = 1; @@ -639,7 +644,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { function test_setOpChallengePeriod_DropsFinalizedOpPoke_If_NonFinalizedAfterUpdate( ) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); // Execute opPoke. IScribe.PokeData memory opPokeData; @@ -689,8 +694,9 @@ abstract contract IScribeOptimisticTest is IScribeTest { function _setUpFeedsAndOpPokeOnce(IScribe.PokeData memory pokeData) private + returns (LibFeed.Feed[] memory) { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.SchnorrData memory schnorrData; schnorrData = feeds.signSchnorr(opScribe.constructPokeMessage(pokeData)); @@ -702,6 +708,8 @@ abstract contract IScribeOptimisticTest is IScribeTest { opScribe.constructOpPokeMessage(pokeData, schnorrData) ) ); + + return feeds; } function testFuzz_setOpChallengePeriod_IsAfterAuthedActionProtected( @@ -730,7 +738,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { pokeData.val = 1; pokeData.age = uint32(block.timestamp); - _setUpFeedsAndOpPokeOnce(pokeData); + LibFeed.Feed[] memory feeds = _setUpFeedsAndOpPokeOnce(pokeData); if (opPokeFinalized) { vm.warp(block.timestamp + opScribe.opChallengePeriod()); @@ -739,7 +747,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { emit OpPokeDataDropped(address(this), pokeData); } - opScribe.drop(1); + opScribe.drop(feeds[0].id); } function testFuzz_drop_Multiple_IsAfterAuthedActionProtected( @@ -749,7 +757,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { pokeData.val = 1; pokeData.age = uint32(block.timestamp); - _setUpFeedsAndOpPokeOnce(pokeData); + LibFeed.Feed[] memory feeds = _setUpFeedsAndOpPokeOnce(pokeData); if (opPokeFinalized) { vm.warp(block.timestamp + opScribe.opChallengePeriod()); @@ -758,10 +766,12 @@ abstract contract IScribeOptimisticTest is IScribeTest { emit OpPokeDataDropped(address(this), pokeData); } - uint[] memory feedIndexes = new uint[](1); - feedIndexes[0] = 1; + uint8[] memory feedIds = new uint8[](feeds.length); + for (uint i; i < feeds.length; i++) { + feedIds[i] = feeds[i].id; + } - opScribe.drop(feedIndexes); + opScribe.drop(feedIds); } function testFuzz_setBar_IsAfterAuthedActionProtected(bool opPokeFinalized) @@ -799,9 +809,9 @@ abstract contract IScribeOptimisticTest is IScribeTest { function testFuzz_afterAuthedAction_ProvidesValue_If_MoreThanOncePoked( uint pathSeed ) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); - uint path = bound(pathSeed, 0, 6); + uint path = _bound(pathSeed, 0, 6); uint128 otherVal = 1; uint128 wantVal = 2; @@ -1046,9 +1056,13 @@ abstract contract IScribeOptimisticTest is IScribeTest { function _setUp_afterAuthedAction_1() internal - returns (IScribe.PokeData memory, IScribe.PokeData memory) + returns ( + IScribe.PokeData memory, + IScribe.PokeData memory, + LibFeed.Feed[] memory + ) { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory pokeData; IScribe.PokeData memory opPokeData = IScribe.PokeData(1, 1); @@ -1063,7 +1077,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { ) ); - return (pokeData, opPokeData); + return (pokeData, opPokeData, feeds); } function test_afterAuthedAction_1_setBar() public { @@ -1079,9 +1093,9 @@ abstract contract IScribeOptimisticTest is IScribeTest { } function test_afterAuthedAction_1_drop() public { - _setUp_afterAuthedAction_1(); + (,, LibFeed.Feed[] memory feeds) = _setUp_afterAuthedAction_1(); - opScribe.drop(1); + opScribe.drop(feeds[0].id); (bool ok,) = opScribe.tryRead(); assertFalse(ok); @@ -1116,9 +1130,13 @@ abstract contract IScribeOptimisticTest is IScribeTest { function _setUp_afterAuthedAction_2() internal - returns (IScribe.PokeData memory, IScribe.PokeData memory) + returns ( + IScribe.PokeData memory, + IScribe.PokeData memory, + LibFeed.Feed[] memory + ) { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory pokeData = IScribe.PokeData(2, 1); opScribe.poke( @@ -1140,11 +1158,11 @@ abstract contract IScribeOptimisticTest is IScribeTest { ) ); - return (pokeData, opPokeData); + return (pokeData, opPokeData, feeds); } function test_afterAuthedAction_2_setBar() public { - (IScribe.PokeData memory pokeData,) = _setUp_afterAuthedAction_2(); + (IScribe.PokeData memory pokeData,,) = _setUp_afterAuthedAction_2(); opScribe.setBar(3); @@ -1158,9 +1176,10 @@ abstract contract IScribeOptimisticTest is IScribeTest { } function test_afterAuthedAction_2_drop() public { - (IScribe.PokeData memory pokeData,) = _setUp_afterAuthedAction_2(); + (IScribe.PokeData memory pokeData,, LibFeed.Feed[] memory feeds) = + _setUp_afterAuthedAction_2(); - opScribe.drop(1); + opScribe.drop(feeds[0].id); (bool ok, uint val, uint age) = opScribe.tryReadWithAge(); assertTrue(ok); @@ -1172,7 +1191,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { } function test_afterAuthedAction_2_setChallengePeriod() public { - (IScribe.PokeData memory pokeData,) = _setUp_afterAuthedAction_2(); + (IScribe.PokeData memory pokeData,,) = _setUp_afterAuthedAction_2(); // Note that _opPokeData still non-finalized after challenge period // update. @@ -1199,9 +1218,13 @@ abstract contract IScribeOptimisticTest is IScribeTest { function _setUp_afterAuthedAction_3() internal - returns (IScribe.PokeData memory, IScribe.PokeData memory) + returns ( + IScribe.PokeData memory, + IScribe.PokeData memory, + LibFeed.Feed[] memory + ) { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory opPokeData = IScribe.PokeData(1, uint32(block.timestamp)); @@ -1218,11 +1241,11 @@ abstract contract IScribeOptimisticTest is IScribeTest { vm.warp(block.timestamp + opScribe.opChallengePeriod() + 1); - return (IScribe.PokeData(0, 0), opPokeData); + return (IScribe.PokeData(0, 0), opPokeData, feeds); } function test_afterAuthedAction_3_setBar() public { - (, IScribe.PokeData memory opPokeData) = _setUp_afterAuthedAction_3(); + (, IScribe.PokeData memory opPokeData,) = _setUp_afterAuthedAction_3(); opScribe.setBar(3); @@ -1236,9 +1259,10 @@ abstract contract IScribeOptimisticTest is IScribeTest { } function test_afterAuthedAction_3_drop() public { - (, IScribe.PokeData memory opPokeData) = _setUp_afterAuthedAction_3(); + (, IScribe.PokeData memory opPokeData, LibFeed.Feed[] memory feeds) = + _setUp_afterAuthedAction_3(); - opScribe.drop(1); + opScribe.drop(feeds[0].id); (bool ok, uint val, uint age) = opScribe.tryReadWithAge(); assertTrue(ok); @@ -1250,7 +1274,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { } function test_afterAuthedAction_3_setChallengePeriod() public { - (, IScribe.PokeData memory opPokeData) = _setUp_afterAuthedAction_3(); + (, IScribe.PokeData memory opPokeData,) = _setUp_afterAuthedAction_3(); // Update challenge period so that _opPokeData still finalized. opScribe.setOpChallengePeriod(1); @@ -1276,9 +1300,13 @@ abstract contract IScribeOptimisticTest is IScribeTest { function _setUp_afterAuthedAction_4() internal - returns (IScribe.PokeData memory, IScribe.PokeData memory) + returns ( + IScribe.PokeData memory, + IScribe.PokeData memory, + LibFeed.Feed[] memory + ) { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(opScribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(opScribe.bar()); IScribe.PokeData memory pokeData = IScribe.PokeData(2, 1); opScribe.poke( @@ -1302,11 +1330,11 @@ abstract contract IScribeOptimisticTest is IScribeTest { vm.warp(block.timestamp + opScribe.opChallengePeriod() + 1); - return (pokeData, opPokeData); + return (pokeData, opPokeData, feeds); } function test_afterAuthedAction_4_setBar() public { - (, IScribe.PokeData memory opPokeData) = _setUp_afterAuthedAction_4(); + (, IScribe.PokeData memory opPokeData,) = _setUp_afterAuthedAction_4(); opScribe.setBar(3); @@ -1319,9 +1347,10 @@ abstract contract IScribeOptimisticTest is IScribeTest { } function test_afterAuthedAction_4_drop() public { - (, IScribe.PokeData memory opPokeData) = _setUp_afterAuthedAction_4(); + (, IScribe.PokeData memory opPokeData, LibFeed.Feed[] memory feeds) = + _setUp_afterAuthedAction_4(); - opScribe.drop(1); + opScribe.drop(feeds[0].id); (bool ok, uint val,) = opScribe.tryReadWithAge(); assertTrue(ok); @@ -1332,7 +1361,7 @@ abstract contract IScribeOptimisticTest is IScribeTest { } function test_afterAuthedAction_4_setChallengePeriod() public { - (, IScribe.PokeData memory opPokeData) = _setUp_afterAuthedAction_4(); + (, IScribe.PokeData memory opPokeData,) = _setUp_afterAuthedAction_4(); // Update challenge period so that _opPokeData still finalized. opScribe.setOpChallengePeriod(1); diff --git a/test/IScribeTest.sol b/test/IScribeTest.sol index 2ff54f5..e78cebf 100644 --- a/test/IScribeTest.sol +++ b/test/IScribeTest.sol @@ -23,17 +23,13 @@ abstract contract IScribeTest is Test { bytes32 internal WAT; bytes32 internal FEED_REGISTRATION_MESSAGE; - LibFeed.Feed internal notFeed; - - mapping(address => bool) internal addressFilter; - // Events copied from IScribe. event Poked(address indexed caller, uint128 val, uint32 age); event FeedLifted( - address indexed caller, address indexed feed, uint indexed index + address indexed caller, address indexed feed, uint8 indexed feedId ); event FeedDropped( - address indexed caller, address indexed feed, uint indexed index + address indexed caller, address indexed feed, uint8 indexed feedId ); event BarUpdated(address indexed caller, uint8 oldBar, uint8 newBar); @@ -46,37 +42,38 @@ abstract contract IScribeTest is Test { // Toll address(this). IToll(address(scribe)).kiss(address(this)); - - // Create a non-lifted feed instance. - notFeed = LibFeed.newFeed({privKey: 0xdead, index: type(uint8).max}); } - function _createAndLiftFeeds(uint numberFeeds) + function _liftFeeds(uint8 numberFeeds) internal returns (LibFeed.Feed[] memory) { + LibFeed.Feed[] memory feeds = new LibFeed.Feed[](uint(numberFeeds)); + // Note to not start with privKey=1. This is because the sum of public // keys would evaluate to: // pubKeyOf(1) + pubKeyOf(2) + pubKeyOf(3) + ... // = pubKeyOf(3) + pubKeyOf(3) + ... // Note that pubKeyOf(3) would be doubled. Doubling is not supported by // LibSecp256k1 as this would indicate a double-signing attack. - uint startPrivKey = 2; - - LibFeed.Feed[] memory feeds = new LibFeed.Feed[](numberFeeds); - for (uint i; i < numberFeeds; i++) { - feeds[i] = LibFeed.newFeed({ - privKey: startPrivKey + i, - index: uint8(i + 1) - }); - vm.label( - feeds[i].pubKey.toAddress(), - string.concat("Feed #", vm.toString(i + 1)) - ); + uint privKey = 2; + uint bloom; + uint ctr; + while (ctr != numberFeeds) { + LibFeed.Feed memory feed = LibFeed.newFeed({privKey: privKey}); + + // Check whether feed with id already created, if not create and + // lift. + if (bloom & (1 << feed.id) == 0) { + bloom |= 1 << feed.id; + + feeds[ctr++] = feed; + scribe.lift( + feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE) + ); + } - scribe.lift( - feeds[i].pubKey, feeds[i].signECDSA(FEED_REGISTRATION_MESSAGE) - ); + privKey++; } return feeds; @@ -137,9 +134,9 @@ abstract contract IScribeTest is Test { assertEq(scribe.bar(), 2); // Set of feeds is empty. - (address[] memory feeds_, uint[] memory feedsIndexes) = scribe.feeds(); - assertEq(feeds_.length, 0); - assertEq(feedsIndexes.length, 0); + (address[] memory feeds, uint8[] memory feedIds) = scribe.feeds(); + assertEq(feeds.length, 0); + assertEq(feedIds.length, 0); // read()(uint) fails. try scribe.read() returns (uint) { @@ -196,11 +193,10 @@ abstract contract IScribeTest is Test { // -- Test: Schnorr Verification -- function testFuzz_isAcceptableSchnorrSignatureNow(uint barSeed) public { - // Let bar ∊ [1, scribe.maxFeeds()]. - uint bar = bound(barSeed, 1, scribe.maxFeeds()); - - scribe.setBar(uint8(bar)); - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(bar); + // Let bar ∊ [1, 256). + uint8 bar = uint8(_bound(barSeed, 1, 256 - 1)); + scribe.setBar(bar); + LibFeed.Feed[] memory feeds = _liftFeeds(bar); bytes32 message = keccak256("scribe"); @@ -212,20 +208,19 @@ abstract contract IScribeTest is Test { function testFuzz_isAcceptableSchnorrSignatureNow_FailsIf_BarNotReached( uint barSeed, - uint numberSignersSeed + uint numberFeedsSeed ) public { - // Let bar ∊ [2, scribe.maxFeeds()]. - uint bar = bound(barSeed, 2, scribe.maxFeeds()); - - // Let numberSigners ∊ [1, bar). - uint numberSigners = bound(numberSignersSeed, 1, bar - 1); + // Let bar ∊ [2, 256). + uint8 bar = uint8(_bound(barSeed, 2, 256 - 1)); + scribe.setBar(bar); + LibFeed.Feed[] memory feeds = _liftFeeds(bar); - scribe.setBar(uint8(bar)); - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(bar); + // Let numberFeeds ∊ [1, bar). + uint numberFeeds = _bound(numberFeedsSeed, 1, uint(bar) - 1); assembly ("memory-safe") { - // Set length of feeds list to numberSigners. - mstore(feeds, numberSigners) + // Set length of feeds list to numberFeeds. + mstore(feeds, numberFeeds) } bytes32 message = keccak256("scribe"); @@ -236,37 +231,54 @@ abstract contract IScribeTest is Test { assertFalse(ok); } - function testFuzz_isAcceptableSchnorrSignatureNow_FailsIf_SignersNotOrdered( - uint barSeed + function testFuzz_isAcceptableSchnorrSignatureNow_FailsIf_DoubleSigningAttempted( + uint barSeed, + uint doubleSignerIndexSeed ) public { - // Let bar ∊ [3, scribe.maxFeeds()]. - uint bar = bound(barSeed, 3, scribe.maxFeeds()); + // Let bar ∊ [2, 256). + uint8 bar = uint8(_bound(barSeed, 2, 256 - 1)); + scribe.setBar(bar); + LibFeed.Feed[] memory feeds = _liftFeeds(bar); + + // Let doubleSignerIndex ∊ [1, bar). + uint doubleSignerIndex = _bound(doubleSignerIndexSeed, 1, uint(bar) - 1); - scribe.setBar(uint8(bar)); - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(bar); + // Let random feed double sign. + feeds[0] = feeds[doubleSignerIndex]; bytes32 message = keccak256("scribe"); bool ok = scribe.isAcceptableSchnorrSignatureNow( - message, feeds.signSchnorr_withoutOrderingSignerIndexes(message) + message, feeds.signSchnorr(message) ); assertFalse(ok); } - function testFuzz_isAcceptableSchnorrSignatureNow_FailsIf_SignerNotFeed( + function testFuzz_isAcceptableSchnorrSignatureNow_FailsIf_InvalidFeedId( uint barSeed, - uint nonSignerIndexSeed + uint privKeySeed, + uint indexSeed ) public { - // Let bar ∊ [1, scribe.maxFeeds()]. - uint bar = bound(barSeed, 1, scribe.maxFeeds()); + // Let bar ∊ [2, 256). + uint8 bar = uint8(_bound(barSeed, 2, 256 - 1)); + scribe.setBar(bar); + LibFeed.Feed[] memory feeds = _liftFeeds(bar); - // Let nonSignerIndex ∊ [0, bar). - uint nonSignerIndex = bound(nonSignerIndexSeed, 0, bar - 1); + // Let privKey ∊ [1, Q). + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); - scribe.setBar(uint8(bar)); - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(bar); + // Note to not lift feed. + LibFeed.Feed memory feed = LibFeed.newFeed({privKey: privKey}); - feeds[nonSignerIndex] = notFeed; + // Don't run test if bad luck and feed already lifted. + (bool isFeed, /*feedAddr*/ ) = scribe.feeds(feed.id); + if (isFeed) return; + + // Let index ∊ [0, bar). + uint index = _bound(indexSeed, 0, bar - 1); + + // Let non-lifted feed be the index's signer. + feeds[index] = feed; bytes32 message = keccak256("scribe"); @@ -279,11 +291,10 @@ abstract contract IScribeTest is Test { function testFuzz_isAcceptableSchnorrSignatureNow_FailsIf_SignatureInvalid( uint barSeed ) public { - // Let bar ∊ [1, scribe.maxFeeds()]. - uint bar = bound(barSeed, 1, scribe.maxFeeds()); - - scribe.setBar(uint8(bar)); - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(bar); + // Let bar ∊ [1, 256). + uint8 bar = uint8(_bound(barSeed, 1, 256 - 1)); + scribe.setBar(bar); + LibFeed.Feed[] memory feeds = _liftFeeds(bar); bytes32 message = keccak256("scribe"); @@ -302,17 +313,18 @@ abstract contract IScribeTest is Test { // -- Test: Poke -- function testFuzz_poke(IScribe.PokeData[] memory pokeDatas) public { - vm.assume(pokeDatas.length < 50); + LibFeed.Feed[] memory feeds = _liftFeeds(scribe.bar()); - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(scribe.bar()); + // Note to stay reasonable in favor of runtime. + vm.assume(pokeDatas.length < 50); uint32 lastPokeTimestamp = 0; IScribe.SchnorrData memory schnorrData; for (uint i; i < pokeDatas.length; i++) { pokeDatas[i].val = - uint128(bound(pokeDatas[i].val, 1, type(uint128).max)); + uint128(_bound(pokeDatas[i].val, 1, type(uint128).max)); pokeDatas[i].age = uint32( - bound(pokeDatas[i].age, lastPokeTimestamp + 1, block.timestamp) + _bound(pokeDatas[i].age, lastPokeTimestamp + 1, block.timestamp) ); schnorrData = @@ -331,7 +343,7 @@ abstract contract IScribeTest is Test { } function test_poke_Initial_FailsIf_AgeIsZero() public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(scribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(scribe.bar()); IScribe.PokeData memory pokeData; pokeData.val = 1; @@ -349,11 +361,11 @@ abstract contract IScribeTest is Test { function testFuzz_poke_FailsIf_AgeIsStale(IScribe.PokeData memory pokeData) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(scribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(scribe.bar()); vm.assume(pokeData.val != 0); // Let pokeData's age ∊ [1, block.timestamp]. - pokeData.age = uint32(bound(pokeData.age, 1, block.timestamp)); + pokeData.age = uint32(_bound(pokeData.age, 1, block.timestamp)); IScribe.SchnorrData memory schnorrData; schnorrData = feeds.signSchnorr(scribe.constructPokeMessage(pokeData)); @@ -365,7 +377,7 @@ abstract contract IScribeTest is Test { uint currentAge = uint32(block.timestamp); // Set pokeData's age ∊ [0, block.timestamp]. - pokeData.age = uint32(bound(pokeData.age, 0, block.timestamp)); + pokeData.age = uint32(_bound(pokeData.age, 0, block.timestamp)); schnorrData = feeds.signSchnorr(scribe.constructPokeMessage(pokeData)); @@ -381,10 +393,10 @@ abstract contract IScribeTest is Test { function testFuzz_poke_FailsIf_AgeIsInTheFuture( IScribe.PokeData memory pokeData ) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(scribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(scribe.bar()); vm.assume(pokeData.val != 0); - // Let pokeData's age ∊ [block.timestamp+1, type(uint32).max]. + // Let pokeData's age ∊ [block.timestamp + 1, type(uint32).max]. pokeData.age = uint32(bound(pokeData.age, block.timestamp + 1, type(uint32).max)); @@ -404,10 +416,12 @@ abstract contract IScribeTest is Test { function testFuzz_poke_FailsIf_SignatureInvalid( IScribe.PokeData memory pokeData ) public { - LibFeed.Feed[] memory feeds = _createAndLiftFeeds(scribe.bar()); + LibFeed.Feed[] memory feeds = _liftFeeds(scribe.bar()); - vm.assume(pokeData.val != 0); - vm.assume(pokeData.age != 0 && pokeData.age <= uint32(block.timestamp)); + // Let pokeData's val ∊ [1, type(uint128).max]. + // Let pokeData's age ∊ [1, block.timestamp]. + pokeData.val = uint128(_bound(pokeData.val, 1, type(uint128).max)); + pokeData.age = uint32(_bound(pokeData.age, 1, block.timestamp)); // Create schnorrData signing different message. bytes32 message = keccak256("scribe"); @@ -432,150 +446,156 @@ abstract contract IScribeTest is Test { // -- Test: Auth Protected Functions -- function testFuzz_lift_Single(uint privKey) public { - // Bound private key to secp256k1's order, i.e. scalar ∊ [1, Q). - privKey = bound(privKey, 1, LibSecp256k1.Q() - 1); + // Let privKey ∊ [1, Q). + privKey = _bound(privKey, 1, LibSecp256k1.Q() - 1); LibFeed.Feed memory feed = LibFeed.newFeed(privKey); vm.expectEmit(); - emit FeedLifted(address(this), feed.pubKey.toAddress(), 1); + emit FeedLifted(address(this), feed.pubKey.toAddress(), feed.id); - uint index = + uint feedId = scribe.lift(feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE)); - assertEq(index, 1); + assertEq(feedId, feed.id); - // Check via feeds(address)(bool,uint). + // Is idempotent. + scribe.lift(feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE)); + + // Check via feeds(address)(bool,uint8). bool ok; - (ok, index) = scribe.feeds(feed.pubKey.toAddress()); + (ok, feedId) = scribe.feeds(feed.pubKey.toAddress()); assertTrue(ok); - assertEq(index, 1); + assertEq(feedId, feed.id); - // Check via feeds(uint)(bool,address). + // Check via feeds(uint8)(bool,address). address feedAddr; - (ok, feedAddr) = scribe.feeds(1); + (ok, feedAddr) = scribe.feeds(feed.id); assertTrue(ok); assertEq(feedAddr, feed.pubKey.toAddress()); - // Check via feeds()(address[],uint[]). + // Check via feeds()(address[],uint8[]). address[] memory feeds_; - uint[] memory indexes; - (feeds_, indexes) = scribe.feeds(); - assertEq(feeds_.length, indexes.length); + uint8[] memory feedIds; + (feeds_, feedIds) = scribe.feeds(); assertEq(feeds_.length, 1); + assertEq(feedIds.length, 1); assertEq(feeds_[0], feed.pubKey.toAddress()); - assertEq(indexes[0], 1); + assertEq(feedIds[0], feed.id); } function test_lift_Single_FailsIf_ECDSADataInvalid() public { - uint privKeySigner = 1; - uint privKeyFeed = 2; - vm.expectRevert(); scribe.lift( - LibFeed.newFeed(privKeyFeed).pubKey, - LibFeed.newFeed(privKeySigner).signECDSA(FEED_REGISTRATION_MESSAGE) + LibFeed.newFeed({privKey: 1}).pubKey, + LibFeed.newFeed({privKey: 2}).signECDSA(FEED_REGISTRATION_MESSAGE) ); } - function test_lift_Single_FailsIf_MaxFeedsReached() public { - uint maxFeeds = scribe.maxFeeds(); + function test_lift_Single_FailsIf_FeedIdAlreadyLifted() public { + LibFeed.Feed memory feed1 = LibFeed.newFeed({privKey: 22171}); + LibFeed.Feed memory feed2 = LibFeed.newFeed({privKey: 38091}); - // Lift maxFeeds feeds. - LibFeed.Feed memory feed; - for (uint i; i < maxFeeds; i++) { - feed = LibFeed.newFeed(i + 1); - scribe.lift(feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE)); - } + // Both feeds have same id. + assertTrue(feed1.id == feed2.id); + + scribe.lift(feed1.pubKey, feed1.signECDSA(FEED_REGISTRATION_MESSAGE)); - feed = LibFeed.newFeed(maxFeeds + 1); vm.expectRevert(); - scribe.lift(feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE)); + scribe.lift(feed2.pubKey, feed2.signECDSA(FEED_REGISTRATION_MESSAGE)); } function testFuzz_lift_Multiple(uint[] memory privKeys) public { - // Bound private keys to secp256k1's order, i.e. scalar ∊ [1, Q). + vm.assume(privKeys.length < 50); + + // Let each privKey ∊ [1, Q). for (uint i; i < privKeys.length; i++) { - privKeys[i] = bound(privKeys[i], 1, LibSecp256k1.Q() - 1); + privKeys[i] = _bound(privKeys[i], 1, LibSecp256k1.Q() - 1); } - // Make feeds. - LibFeed.Feed[] memory feeds_ = new LibFeed.Feed[](privKeys.length); + // Make at most one feed per id. + LibFeed.Feed[] memory feeds = new LibFeed.Feed[](privKeys.length); + uint bloom; + uint ctr; for (uint i; i < privKeys.length; i++) { - feeds_[i] = LibFeed.newFeed(privKeys[i]); + LibFeed.Feed memory feed = LibFeed.newFeed(privKeys[i]); + + if (bloom & (1 << feed.id) == 0) { + bloom |= 1 << feed.id; + + feeds[ctr++] = feed; + } + } + assembly ("memory-safe") { + mstore(feeds, ctr) } // Make list of public keys. LibSecp256k1.Point[] memory pubKeys = - new LibSecp256k1.Point[](feeds_.length); - for (uint i; i < feeds_.length; i++) { - pubKeys[i] = feeds_[i].pubKey; + new LibSecp256k1.Point[](feeds.length); + for (uint i; i < feeds.length; i++) { + pubKeys[i] = feeds[i].pubKey; } // Make signatures. IScribe.ECDSAData[] memory ecdsaDatas = - new IScribe.ECDSAData[](feeds_.length); - for (uint i; i < feeds_.length; i++) { - ecdsaDatas[i] = feeds_[i].signECDSA(FEED_REGISTRATION_MESSAGE); + new IScribe.ECDSAData[](feeds.length); + for (uint i; i < feeds.length; i++) { + ecdsaDatas[i] = feeds[i].signECDSA(FEED_REGISTRATION_MESSAGE); } - uint indexCtr = 1; - for (uint i; i < feeds_.length; i++) { - // Don't expect event for duplicates. - if (!addressFilter[feeds_[i].pubKey.toAddress()]) { - vm.expectEmit(); - emit FeedLifted( - address(this), feeds_[i].pubKey.toAddress(), indexCtr++ - ); - } - addressFilter[feeds_[i].pubKey.toAddress()] = true; + // Expect events. + for (uint i; i < feeds.length; i++) { + vm.expectEmit(); + emit FeedLifted( + address(this), feeds[i].pubKey.toAddress(), feeds[i].id + ); } - uint[] memory indexes = scribe.lift(pubKeys, ecdsaDatas); - assertEq(indexes.length, pubKeys.length); - for (uint i; i < indexes.length; i++) { - assertTrue(indexes[i] != 0 && indexes[i] < pubKeys.length + 1); + // Lift feeds and verify returned feed ids. + uint8[] memory feedIds = scribe.lift(pubKeys, ecdsaDatas); + assertEq(feedIds.length, feeds.length); + for (uint i; i < feedIds.length; i++) { + assertEq(feedIds[i], feeds[i].id); } - // Check via feeds(address)(bool,uint) and feeds(uint)(bool,address). + // Check via feeds(address)(bool,uint8) and feeds(uint8)(bool,address). bool ok; - uint index; + uint8 feedId; address feedAddr; - for (uint i; i < pubKeys.length; i++) { - (ok, index) = scribe.feeds(pubKeys[i].toAddress()); + for (uint i; i < feeds.length; i++) { + (ok, feedId) = scribe.feeds(feeds[i].pubKey.toAddress()); assertTrue(ok); - // Note that the indexes are orders based on pubKeys' addresses. - assertTrue(index != 0); + assertEq(feedId, feeds[i].id); - (ok, feedAddr) = scribe.feeds(index); + (ok, feedAddr) = scribe.feeds(feedId); assertTrue(ok); - assertEq(pubKeys[i].toAddress(), feedAddr); + assertEq(feeds[i].pubKey.toAddress(), feedAddr); } - // Check via feeds()(address[],uint[]). - address[] memory addrs; - (addrs, indexes) = scribe.feeds(); - for (uint i; i < pubKeys.length; i++) { - for (uint j; j < addrs.length; j++) { - // Break inner loop if pubKey's address found in list of feeds. - if (pubKeys[i].toAddress() == addrs[j]) { + // Check via feeds()(address[],uint8[]). + address[] memory feedAddrs; + (feedAddrs, feedIds) = scribe.feeds(); + assertEq(feedAddrs.length, feedIds.length); + for (uint i; i < feeds.length; i++) { + for (uint j; j < feedAddrs.length; j++) { + // Break inner loop if feed's address found in list of feedAddrs. + if (feeds[i].pubKey.toAddress() == feedAddrs[j]) { + assertEq(feedIds[j], feeds[i].id); break; } // Fail if pubKey's address not found in list of feeds. - if (j == addrs.length - 1) { - assertTrue(false); + if (j == feedAddrs.length - 1) { + fail("Expected feed missing in feeds()(address[],uint8[])"); } } } } function test_lift_Multiple_FailsIf_ECDSADataInvalid() public { - uint privKeySigner = 1; - uint privKeyFeed = 2; - LibFeed.Feed[] memory feeds = new LibFeed.Feed[](2); - feeds[0] = LibFeed.newFeed(privKeySigner); - feeds[1] = LibFeed.newFeed(privKeyFeed); + feeds[0] = LibFeed.newFeed({privKey: 1}); + feeds[1] = LibFeed.newFeed({privKey: 2}); IScribe.ECDSAData[] memory ecdsaDatas = new IScribe.ECDSAData[](2); ecdsaDatas[0] = feeds[0].signECDSA(FEED_REGISTRATION_MESSAGE); @@ -589,33 +609,6 @@ abstract contract IScribeTest is Test { scribe.lift(pubKeys, ecdsaDatas); } - function test_lift_Multiple_FailsIf_MaxFeedsReached() public { - uint maxFeeds = scribe.maxFeeds(); - - // Make feeds. - LibFeed.Feed[] memory feeds = new LibFeed.Feed[](maxFeeds + 1); - for (uint i; i < maxFeeds + 1; i++) { - feeds[i] = LibFeed.newFeed(i + 1); - } - - // Make list of public keys. - LibSecp256k1.Point[] memory pubKeys = - new LibSecp256k1.Point[](maxFeeds + 1); - for (uint i; i < maxFeeds + 1; i++) { - pubKeys[i] = feeds[i].pubKey; - } - - // Make signatures. - IScribe.ECDSAData[] memory ecdsaDatas = - new IScribe.ECDSAData[](maxFeeds + 1); - for (uint i; i < maxFeeds + 1; i++) { - ecdsaDatas[i] = feeds[i].signECDSA(FEED_REGISTRATION_MESSAGE); - } - - vm.expectRevert(); - scribe.lift(pubKeys, ecdsaDatas); - } - function testFuzz_lift_Multiple_FailsIf_ArrayLengthMismatch( LibSecp256k1.Point[] memory pubKeys, IScribe.ECDSAData[] memory ecdsaDatas @@ -627,162 +620,127 @@ abstract contract IScribeTest is Test { } function testFuzz_drop_Single(uint privKey) public { - // Bound private key to secp256k1's order, i.e. scalar ∊ [1, Q). + // Let privKey ∊ [1, Q). privKey = bound(privKey, 1, LibSecp256k1.Q() - 1); LibFeed.Feed memory feed = LibFeed.newFeed(privKey); - uint index = + uint8 feedId = scribe.lift(feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE)); - assertEq(index, 1); vm.expectEmit(); - emit FeedDropped(address(this), feed.pubKey.toAddress(), 1); + emit FeedDropped(address(this), feed.pubKey.toAddress(), feedId); + + scribe.drop(feedId); - scribe.drop(1); + // Is idempotent. + scribe.drop(feedId); - // Check via feeds(address)(bool). + // Check via feeds(address)(bool,uint8). bool ok; - (ok, index) = scribe.feeds(feed.pubKey.toAddress()); + uint8 feedId_; + (ok, feedId_) = scribe.feeds(feed.pubKey.toAddress()); assertFalse(ok); - assertEq(index, 0); + assertEq(feedId_, feedId); // Check via feeds(uint)(bool,address). address feedAddr; - (ok, feedAddr) = scribe.feeds(1); + (ok, feedAddr) = scribe.feeds(feedId); assertFalse(ok); - assertFalse(feedAddr == feed.pubKey.toAddress()); + assertEq(feedAddr, address(0)); - // Check via feeds()(address[],uint[]). + // Check via feeds()(address[],uint8[]). address[] memory feeds_; - uint[] memory indexes; - (feeds_, indexes) = scribe.feeds(); - assertEq(feeds_.length, indexes.length); + uint8[] memory feedIds; + (feeds_, feedIds) = scribe.feeds(); assertEq(feeds_.length, 0); + assertEq(feedIds.length, 0); } function testFuzz_drop_Multiple(uint[] memory privKeys) public { - // Bound private keys to secp256k1's order, i.e. scalar ∊ [1, Q). + // Let each privKey ∊ [1, Q). for (uint i; i < privKeys.length; i++) { - privKeys[i] = bound(privKeys[i], 1, LibSecp256k1.Q() - 1); + privKeys[i] = _bound(privKeys[i], 1, LibSecp256k1.Q() - 1); } - // Make feeds. - LibFeed.Feed[] memory feeds_ = new LibFeed.Feed[](privKeys.length); + // Make at most one feed per id. + LibFeed.Feed[] memory feeds = new LibFeed.Feed[](privKeys.length); + uint bloom; + uint ctr; for (uint i; i < privKeys.length; i++) { - feeds_[i] = LibFeed.newFeed(privKeys[i]); + LibFeed.Feed memory feed = LibFeed.newFeed(privKeys[i]); + + if (bloom & (1 << feed.id) == 0) { + bloom |= 1 << feed.id; + + feeds[ctr++] = feed; + } + } + assembly ("memory-safe") { + mstore(feeds, ctr) } // Make list of public keys. LibSecp256k1.Point[] memory pubKeys = - new LibSecp256k1.Point[](feeds_.length); - for (uint i; i < feeds_.length; i++) { - pubKeys[i] = feeds_[i].pubKey; + new LibSecp256k1.Point[](feeds.length); + for (uint i; i < feeds.length; i++) { + pubKeys[i] = feeds[i].pubKey; } // Make signatures. IScribe.ECDSAData[] memory ecdsaDatas = - new IScribe.ECDSAData[](feeds_.length); - for (uint i; i < feeds_.length; i++) { - ecdsaDatas[i] = feeds_[i].signECDSA(FEED_REGISTRATION_MESSAGE); + new IScribe.ECDSAData[](feeds.length); + for (uint i; i < feeds.length; i++) { + ecdsaDatas[i] = feeds[i].signECDSA(FEED_REGISTRATION_MESSAGE); } // Lift feeds. - uint[] memory indexes = scribe.lift(pubKeys, ecdsaDatas); + uint8[] memory feedIds = scribe.lift(pubKeys, ecdsaDatas); // Expect events. - uint indexCtr = 1; - for (uint i; i < pubKeys.length; i++) { + bloom = 0; + ctr = 0; + for (uint i; i < feeds.length; i++) { // Don't expect event for duplicates. - if (!addressFilter[pubKeys[i].toAddress()]) { + if (bloom & (1 << feeds[i].id) == 0) { + bloom |= 1 << feeds[i].id; + vm.expectEmit(); emit FeedDropped( - address(this), pubKeys[i].toAddress(), indexCtr++ + address(this), feeds[i].pubKey.toAddress(), feeds[i].id ); } - - addressFilter[pubKeys[i].toAddress()] = true; } // Drop feeds. - scribe.drop(indexes); + scribe.drop(feedIds); + + // Is idempotent. + scribe.drop(feedIds); - // Check via feeds(address)(bool,uint). + // Check via feeds(address)(bool,uint8). bool ok; - uint index; - for (uint i; i < pubKeys.length; i++) { - (ok, index) = scribe.feeds(pubKeys[i].toAddress()); + uint8 feedId; + for (uint i; i < feeds.length; i++) { + (ok, feedId) = scribe.feeds(feeds[i].pubKey.toAddress()); assertFalse(ok); - assertEq(index, 0); + assertEq(feedId, feeds[i].id); } - // Check via feeds()(address[],uint[]). + // Check via feeds(uint8)(bool,address). + address feedAddr; + for (uint i; i < feeds.length; i++) { + (ok, feedAddr) = scribe.feeds(feeds[i].id); + assertFalse(ok); + assertEq(feedAddr, address(0)); + } + + // Check via feeds()(address[],uint8[]). address[] memory feedAddresses; - uint[] memory feedIndexes; - (feedAddresses, feedIndexes) = scribe.feeds(); - assertEq(feedAddresses.length, feedIndexes.length); + uint8[] memory feedIds_; + (feedAddresses, feedIds) = scribe.feeds(); assertEq(feedAddresses.length, 0); - } - - function test_drop_IndexZero() public { - // Does nothing. - scribe.drop(0); - } - - function testFuzz_drop_Single_FailsIf_IndexOutOfBounds(uint index) public { - vm.assume(index != 0); - - vm.expectRevert(); - scribe.drop(index); - } - - function testFuzz_drop_Multiple_FailsIf_IndexOutOfBounds( - uint[] memory indexes - ) public { - vm.assume(indexes.length != 0); - indexes[indexes.length - 1] = 1; - - vm.expectRevert(); - scribe.drop(indexes); - } - - function testFuzz_liftDropLift(uint privKey) public { - // Bound private key to secp256k1's order, i.e. scalar ∊ [1, Q). - privKey = bound(privKey, 1, LibSecp256k1.Q() - 1); - - LibFeed.Feed memory feed = LibFeed.newFeed(privKey); - - bool ok; - uint index; - - index = - scribe.lift(feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE)); - assertEq(index, 1); - (ok, index) = scribe.feeds(feed.pubKey.toAddress()); - assertTrue(ok); - assertEq(index, 1); - - scribe.drop(1); - (ok, index) = scribe.feeds(feed.pubKey.toAddress()); - assertFalse(ok); - assertEq(index, 0); - - // Note that lifting same feed again leads to an increased index - // nevertheless. - index = - scribe.lift(feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE)); - assertEq(index, 2); - (ok, index) = scribe.feeds(feed.pubKey.toAddress()); - assertTrue(ok); - assertEq(index, 2); - - address[] memory feedAddrs; - uint[] memory feedIndexes; - (feedAddrs, feedIndexes) = scribe.feeds(); - assertEq(feedAddrs.length, 1); - assertEq(feedIndexes.length, 1); - assertEq(feedAddrs[0], feed.pubKey.toAddress()); - assertEq(feedIndexes[0], 2); + assertEq(feedIds_.length, 0); } function testFuzz_setBar(uint8 bar) public { @@ -864,7 +822,7 @@ abstract contract IScribeTest is Test { IAuth.NotAuthorized.selector, address(0xbeef) ) ); - scribe.drop(new uint[](1)); + scribe.drop(new uint8[](1)); } function test_setBar_IsAuthProtected() public { diff --git a/test/LibBytesTest.sol b/test/LibBytesTest.sol index 6cbdc44..8fbfdb7 100644 --- a/test/LibBytesTest.sol +++ b/test/LibBytesTest.sol @@ -10,7 +10,7 @@ abstract contract LibBytesTest is Test { function testFuzz_getByteAtIndex(uint8 wantByte, uint indexSeed) public { // Let index ∊ [0, 32). - uint index = bound(indexSeed, 0, 31); + uint index = _bound(indexSeed, 0, 31); // Create word with wantByte at byte index. uint word = uint(wantByte) << (index * 8); @@ -23,8 +23,8 @@ abstract contract LibBytesTest is Test { function test_getByteAtIndex() public { uint word; uint index; - uint want; - uint got; + uint8 want; + uint8 got; // Most significant byte. word = diff --git a/test/LibSchnorrDataTest.sol b/test/LibSchnorrDataTest.sol index ef8c780..1f3a9e2 100644 --- a/test/LibSchnorrDataTest.sol +++ b/test/LibSchnorrDataTest.sol @@ -11,103 +11,98 @@ import {LibSchnorrData} from "src/libs/LibSchnorrData.sol"; abstract contract LibSchnorrDataTest is Test { using LibSchnorrData for IScribe.SchnorrData; - function testFuzz_getSignerIndex(uint lengthSeed, uint indexSeed) public { - // Let length be ∊ [1, type(uint8).max]. - uint length = bound(lengthSeed, 1, type(uint8).max); + function testFuzz_loadFeedId(uint lengthSeed, uint indexSeed) public { + // Let length be ∊ [1, 256]. + uint length = _bound(lengthSeed, 1, 256); // Let index be ∊ [0, length). - uint index = bound(indexSeed, 0, length - 1); + uint8 index = uint8(_bound(indexSeed, 0, length - 1)); - bytes memory signersBlob; + bytes memory feedIds; for (uint i; i < length; i++) { if (i == index) { - signersBlob = abi.encodePacked(signersBlob, uint8(0xFF)); + feedIds = abi.encodePacked(feedIds, uint8(0xFF)); } else { - signersBlob = abi.encodePacked(signersBlob, uint8(1)); + feedIds = abi.encodePacked(feedIds, uint8(1)); } } IScribe.SchnorrData memory schnorrData; - schnorrData.signersBlob = signersBlob; + schnorrData.feedIds = feedIds; - uint got = this.getSignerIndex(schnorrData, index); + uint got = this.loadFeedId(schnorrData, index); assertEq(got, uint8(0xFF)); } - function testFuzz_getSingerIndex_ReturnsZeroIfIndexOutOfBounds( + function testFuzz_loadFeedId_ReturnsZeroIfIndexOutOfBounds( uint lengthSeed, uint indexSeed ) public { - // Let length be ∊ [0, type(uint8).max]. - uint length = bound(lengthSeed, 0, type(uint8).max); + // Let length be ∊ [0, 256]. + uint length = _bound(lengthSeed, 0, 256); - // Let index be ∊ [length, type(uint8).max]. - // Note that index's upper limit is bounded is bounded by bar, which is - // of type uint8. - uint index = bound(indexSeed, length, type(uint8).max); + // Let index be ∊ [length, 256). + uint8 index = uint8(_bound(indexSeed, length, 256)); - bytes memory signersBlob; + // Make sure that index is actually out of bounds. + vm.assume(length <= index); + + bytes memory feedIds; for (uint i; i < length; i++) { - signersBlob = abi.encodePacked(signersBlob, uint8(1)); + feedIds = abi.encodePacked(feedIds, uint8(1)); } IScribe.SchnorrData memory schnorrData; - schnorrData.signersBlob = signersBlob; + schnorrData.feedIds = feedIds; - uint got = this.getSignerIndex(schnorrData, index); + uint got = this.loadFeedId(schnorrData, index); assertEq(got, uint8(0)); } - function testFuzz_getSignerIndexLength(uint lengthSeed) public { - // Let length be ∊ [0, type(uint8).max]. - uint length = bound(lengthSeed, 0, type(uint8).max); + function testFuzz_numberFeeds(uint lengthSeed) public { + // Let length be ∊ [0, 256]. + uint length = bound(lengthSeed, 0, 256); - bytes memory signersBlob; + bytes memory feedIds; for (uint i; i < length; i++) { - signersBlob = abi.encodePacked(signersBlob, uint8(1)); + feedIds = abi.encodePacked(feedIds, uint8(1)); } IScribe.SchnorrData memory schnorrData; - schnorrData.signersBlob = signersBlob; + schnorrData.feedIds = feedIds; - uint got = this.getSignerIndexLength(schnorrData); + uint got = this.numberFeeds(schnorrData); assertEq(got, length); } // -- Optimizations -- - /// @dev Tests correctness of a possible optimization. - /// - /// Note that the optimization is currently not implemented. - function testFuzzOptimization_getSignerIndex_WordIndexComputation( - uint index - ) public { - // Current implementation: + function testFuzzOptimization_loadFeedId_WordIndexComputation(uint index) + public + { + // Previous implementation: uint want; assembly ("memory-safe") { want := mul(div(index, 32), 32) } - // Possible optimization: + // New implementation: uint mask = type(uint).max << 5; uint got = index & mask; assertEq(want, got); } - /// @dev Tests correctness of a possible optimization. - /// - /// Note that the optimization is currently not implemented. - function testFuzzOptimization_getSignerIndex_ByteIndexComputation( - uint index - ) public { - // Current implementation: + function testFuzzOptimization_loadFeedId_ByteIndexComputation(uint index) + public + { + // Previous implementation: uint want; unchecked { want = 31 - (index % 32); } - // Possible optimization: + // New implementation: uint mask = type(uint).max >> (256 - 5); uint got = (~index) & mask; @@ -118,18 +113,19 @@ abstract contract LibSchnorrDataTest is Test { // // Used to move memory structs into calldata. - function getSignerIndex( - IScribe.SchnorrData calldata schnorrData, - uint index - ) public pure returns (uint) { - return schnorrData.getSignerIndex(index); + function loadFeedId(IScribe.SchnorrData calldata schnorrData, uint8 index) + public + pure + returns (uint) + { + return schnorrData.loadFeedId(index); } - function getSignerIndexLength(IScribe.SchnorrData calldata schnorrData) + function numberFeeds(IScribe.SchnorrData calldata schnorrData) public pure returns (uint) { - return schnorrData.getSignerIndexLength(); + return schnorrData.numberFeeds(); } } diff --git a/test/LibSchnorrTest.sol b/test/LibSchnorrTest.sol index 25afc77..11f0b44 100644 --- a/test/LibSchnorrTest.sol +++ b/test/LibSchnorrTest.sol @@ -36,7 +36,7 @@ abstract contract LibSchnorrTest is Test { // Note that we allow double signing. uint[] memory privKeys = new uint[](privKeySeeds.length); for (uint i; i < privKeySeeds.length; i++) { - privKeys[i] = bound(privKeySeeds[i], 2, LibSecp256k1.Q() - 1); + privKeys[i] = _bound(privKeySeeds[i], 2, LibSecp256k1.Q() - 1); } // Make list of public key. @@ -79,7 +79,7 @@ abstract contract LibSchnorrTest is Test { bytes32 message ) public { // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); // Compute pubKey. LibSecp256k1.Point memory pubKey = privKey.derivePublicKey(); @@ -119,7 +119,7 @@ abstract contract LibSchnorrTest is Test { // Note that we allow double signing. uint[] memory privKeys = new uint[](privKeySeeds.length); for (uint i; i < privKeySeeds.length; i++) { - privKeys[i] = bound(privKeySeeds[i], 2, LibSecp256k1.Q() - 1); + privKeys[i] = _bound(privKeySeeds[i], 2, LibSecp256k1.Q() - 1); } // Make list of public key. @@ -166,7 +166,7 @@ abstract contract LibSchnorrTest is Test { vm.assume(signatureMask != 0); // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); // Sign message. uint signature; @@ -191,7 +191,7 @@ abstract contract LibSchnorrTest is Test { vm.assume(commitmentMask != 0); // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); // Sign message. uint signature; @@ -216,7 +216,7 @@ abstract contract LibSchnorrTest is Test { vm.assume(messageMask != 0); // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); // Sign message. uint signature; @@ -242,7 +242,7 @@ abstract contract LibSchnorrTest is Test { vm.assume(pubKeyXMask != 0 || flipParity); // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); // Sign message. uint signature; @@ -268,7 +268,7 @@ abstract contract LibSchnorrTest is Test { bytes32 message ) public { // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); // Sign message. uint signature; @@ -290,7 +290,7 @@ abstract contract LibSchnorrTest is Test { bytes32 message ) public { // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); // Sign message. uint signature; diff --git a/test/LibSecp256k1Test.sol b/test/LibSecp256k1Test.sol index 1c45d24..0ea626f 100644 --- a/test/LibSecp256k1Test.sol +++ b/test/LibSecp256k1Test.sol @@ -17,7 +17,7 @@ abstract contract LibSecp256k1Test is Test { function testFuzzDifferential_toAddress(uint privKeySeed) public { // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); address want = vm.addr(privKey); address got = privKey.derivePublicKey().toAddress(); @@ -43,7 +43,7 @@ abstract contract LibSecp256k1Test is Test { function testFuzz_isOnCurve(uint privKeySeed) public { // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); assertTrue(privKey.derivePublicKey().isOnCurve()); } @@ -56,7 +56,7 @@ abstract contract LibSecp256k1Test is Test { vm.assume(maskX != 0 || maskY != 0); // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); // Compute and mutate point. LibSecp256k1.Point memory p = privKey.derivePublicKey(); @@ -93,7 +93,7 @@ abstract contract LibSecp256k1Test is Test { function testFuzz_toJacobian_toAffine(uint privKeySeed) public { // Let privKey ∊ [1, Q). - uint privKey = bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); LibSecp256k1.Point memory want = privKey.derivePublicKey(); LibSecp256k1.Point memory got = want.toJacobian().toAffine(); diff --git a/test/inspectable/ScribeInspectable.sol b/test/inspectable/ScribeInspectable.sol index ae85d99..9918cee 100644 --- a/test/inspectable/ScribeInspectable.sol +++ b/test/inspectable/ScribeInspectable.sol @@ -14,23 +14,11 @@ contract ScribeInspectable is Scribe { return _pokeData; } - function inspectable_pubKeys() - public - view - returns (LibSecp256k1.Point[] memory) - { - return _pubKeys; - } - - function inspectable_pubKeys(uint index) + function inspectable_sloadPubKey(uint8 feedId) public view returns (LibSecp256k1.Point memory) { - return _unsafeLoadPubKeyAt(index); - } - - function inspectable_feeds(address addr) public view returns (uint) { - return _feeds[addr]; + return _sloadPubKey(feedId); } } diff --git a/test/invariants/FeedSet.sol b/test/invariants/FeedSet.sol index 7beac0f..7cf2e93 100644 --- a/test/invariants/FeedSet.sol +++ b/test/invariants/FeedSet.sol @@ -71,4 +71,8 @@ library LibFeedSet { return s.feeds[seed % s.feeds.length]; } + + function count(FeedSet storage s) internal view returns (uint) { + return s.feeds.length; + } } diff --git a/test/invariants/IScribeInvariantTest.sol b/test/invariants/IScribeInvariantTest.sol index f7dd08b..27e25f2 100644 --- a/test/invariants/IScribeInvariantTest.sol +++ b/test/invariants/IScribeInvariantTest.sol @@ -57,104 +57,27 @@ abstract contract IScribeInvariantTest is Test { function invariant_poke_PokeTimestampsAreStrictlyMonotonicallyIncreasing() public { - // Get scribe's pokeData before the execution. + // Get scribe's pokeData before execution. IScribe.PokeData memory beforePokeData = handler.scribe_lastPokeData(); // Get scribe's current pokeData. IScribe.PokeData memory currentPokeData; currentPokeData = scribe.inspectable_pokeData(); - if (beforePokeData.age != currentPokeData.age) { - assertTrue(beforePokeData.age < currentPokeData.age); - } else { - assertEq(beforePokeData.age, currentPokeData.age); - } - } - - /* - function invariant_poke_PokeTimestampIsOnlyMutatedToCurrentTimestamp() - public - { - // Get scribe's pokeData before the execution. - IScribe.PokeData memory beforePokeData = handler.scribe_lastPokeData(); - - // Get scribe's current pokeData. - IScribe.PokeData memory currentPokeData; - currentPokeData = scribe.inspectable_pokeData(); - - if (beforePokeData.age != currentPokeData.age) { - assertEq(currentPokeData.age, uint32(block.timestamp)); - } + assertTrue(beforePokeData.age <= currentPokeData.age); } - */ // -- PubKeys -- - function invariant_pubKeys_AtIndexZeroIsZeroPoint() public { - assertTrue(scribe.inspectable_pubKeys(0).isZeroPoint()); - } - - mapping(bytes32 => bool) private pubKeyFilter; - - function invariant_pubKeys_NonZeroPubKeyExistsAtMostOnce() public { - LibSecp256k1.Point[] memory pubKeys = scribe.inspectable_pubKeys(); - for (uint i; i < pubKeys.length; i++) { - if (pubKeys[i].isZeroPoint()) continue; - - bytes32 id = keccak256(abi.encodePacked(pubKeys[i].x, pubKeys[i].y)); - - assertFalse(pubKeyFilter[id]); - pubKeyFilter[id] = true; - } - } - - function invariant_pubKeys_LengthIsStrictlyMonotonicallyIncreasing() - public - { - uint lastLen = handler.scribe_lastPubKeysLength(); - uint currentLen = scribe.inspectable_pubKeys().length; - - assertTrue(lastLen <= currentLen); - } - - function invariant_pubKeys_ZeroPointIsNeverAddedAsPubKey() public { - uint lastLen = handler.scribe_lastPubKeysLength(); - - LibSecp256k1.Point[] memory pubKeys; - pubKeys = scribe.inspectable_pubKeys(); - - if (lastLen != pubKeys.length) { - assertFalse(pubKeys[pubKeys.length - 1].isZeroPoint()); - } - } - - // -- Feeds -- - - function invariant_feeds_ImageIsZeroToLengthOfPubKeys() public { - address[] memory feedAddrs = handler.ghost_feedAddresses(); - uint pubKeysLen = scribe.inspectable_pubKeys().length; - - for (uint i; i < feedAddrs.length; i++) { - uint index = scribe.inspectable_feeds(feedAddrs[i]); - - assertTrue(index < pubKeysLen); - } - } - - function invariant_feeds_LinkToTheirPublicKeys() public { - address[] memory feedAddrs = handler.ghost_feedAddresses(); - - LibSecp256k1.Point[] memory pubKeys; - pubKeys = scribe.inspectable_pubKeys(); - + function invariant_pubKeys_IndexedViaFeedId() public { LibSecp256k1.Point memory pubKey; - for (uint i; i < feedAddrs.length; i++) { - uint index = scribe.inspectable_feeds(feedAddrs[i]); + uint8 feedId; + + for (uint i; i < 256; i++) { + pubKey = scribe.inspectable_sloadPubKey(uint8(i)); + feedId = uint8(uint(uint160(pubKey.toAddress())) >> 152); - pubKey = pubKeys[index]; - if (!pubKey.isZeroPoint()) { - assertEq(pubKey.toAddress(), feedAddrs[i]); - } + assertTrue(pubKey.isZeroPoint() || i == feedId); } } diff --git a/test/invariants/ScribeHandler.sol b/test/invariants/ScribeHandler.sol index 1e6da65..a3a92dd 100644 --- a/test/invariants/ScribeHandler.sol +++ b/test/invariants/ScribeHandler.sol @@ -29,16 +29,13 @@ contract ScribeHandler is CommonBase, StdUtils { IScribe public scribe; IScribe.PokeData internal _scribe_lastPokeData; - uint public scribe_lastPubKeysLength; - uint internal nextPrivKey = 2; - FeedSet internal feedSet; + uint internal _nextPrivKey = 2; + FeedSet internal _feedSet; modifier cacheScribeState() { // forgefmt: disable-next-item _scribe_lastPokeData = ScribeInspectable(address(scribe)).inspectable_pokeData(); - // forgefmt: disable-next-item - scribe_lastPubKeysLength = ScribeInspectable(address(scribe)).inspectable_pubKeys().length; _; } @@ -48,8 +45,6 @@ contract ScribeHandler is CommonBase, StdUtils { // Cache constants. WAT = scribe.wat(); FEED_REGISTRATION_MESSAGE = scribe.feedRegistrationMessage(); - - _ensureBarFeedsLifted(); } function _ensureBarFeedsLifted() internal { @@ -60,25 +55,28 @@ contract ScribeHandler is CommonBase, StdUtils { // Lift feeds until bar is reached. uint missing = bar - feeds.length; LibFeed.Feed memory feed; - for (uint i; i < missing; i++) { - feed = LibFeed.newFeed(nextPrivKey++); + while (missing != 0) { + feed = LibFeed.newFeed(_nextPrivKey++); + + // Continue if feed's id already lifted. + (bool isFeed,) = scribe.feeds(feed.id); + if (isFeed) continue; - // Lift feed and set its index. - uint index = scribe.lift( + // Otherwise lift feed and add to feedSet. + scribe.lift( feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE) ); - feed.index = uint8(index); + _feedSet.add({feed: feed, lifted: true}); - // Store feed in feedSet. - feedSet.add(feed, true); + missing--; } } } // -- Target Functions -- - function warp(uint seed) external { - uint amount = bound(seed, 1, 1 hours); + function warp(uint seed) external cacheScribeState { + uint amount = _bound(seed, 1, 1 hours); vm.warp(block.timestamp + amount); } @@ -94,7 +92,7 @@ contract ScribeHandler is CommonBase, StdUtils { } // Get set of bar many feeds from feedSet. - LibFeed.Feed[] memory feeds = feedSet.liftedFeeds(scribe.bar()); + LibFeed.Feed[] memory feeds = _feedSet.liftedFeeds(scribe.bar()); // Create pokeData. IScribe.PokeData memory pokeData = IScribe.PokeData({ @@ -122,34 +120,31 @@ contract ScribeHandler is CommonBase, StdUtils { function lift() external cacheScribeState { // Create new feed. - LibFeed.Feed memory feed = LibFeed.newFeed(nextPrivKey++); + LibFeed.Feed memory feed = LibFeed.newFeed(_nextPrivKey++); - // Lift feed and set its index. - uint index = - scribe.lift(feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE)); - feed.index = uint8(index); + // Return if feed's id already lifted. + (bool isFeed,) = scribe.feeds(feed.id); + if (isFeed) return; - // Store feed in feedSet. - feedSet.add(feed, true); + // Lift feed and add to feedSet. + scribe.lift(feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE)); + _feedSet.add({feed: feed, lifted: true}); } function drop(uint seed) external cacheScribeState { + if (_feedSet.count() == 0) return; + // Get random feed from feedSet. // Note that feed may not be lifted. - LibFeed.Feed memory feed = LibFeedSet.rand(feedSet, seed); - - // Receive index of feed. Index is zero if not lifted. - (, uint index) = scribe.feeds(feed.pubKey.toAddress()); - - // Drop feed. - scribe.drop(index); + LibFeed.Feed memory feed = _feedSet.rand(seed); - // Mark feed as non-lifted in feedSet. - feedSet.updateLifted(feed, false); + // Drop feed and mark as non-lifted in feedSet. + scribe.drop(feed.id); + _feedSet.updateLifted({feed: feed, lifted: false}); } function setBar(uint barSeed) external cacheScribeState { - uint8 newBar = uint8(bound(barSeed, 0, MAX_BAR)); + uint8 newBar = uint8(_bound(barSeed, 0, MAX_BAR)); // Should revert if newBar is 0. try scribe.setBar(newBar) {} catch {} @@ -166,22 +161,22 @@ contract ScribeHandler is CommonBase, StdUtils { } function ghost_feedAddresses() external view returns (address[] memory) { - address[] memory addrs = new address[](feedSet.feeds.length); + address[] memory addrs = new address[](_feedSet.feeds.length); for (uint i; i < addrs.length; i++) { - addrs[i] = feedSet.feeds[i].pubKey.toAddress(); + addrs[i] = _feedSet.feeds[i].pubKey.toAddress(); } return addrs; } // -- Helpers -- - function _randPokeDataVal(uint seed) internal view returns (uint128) { - uint val = bound(seed, 0, type(uint128).max); + function _randPokeDataVal(uint seed) internal pure returns (uint128) { + uint val = _bound(seed, 0, type(uint128).max); return uint128(val); } function _randPokeDataAge(uint seed) internal view returns (uint32) { - uint age = bound(seed, _scribe_lastPokeData.age + 1, block.timestamp); + uint age = _bound(seed, _scribe_lastPokeData.age + 1, block.timestamp); return uint32(age); } }