├── bin └── .gitkeep ├── assets ├── logo.png └── benchmarks.png ├── audits ├── ABDK@v1.0.0.pdf ├── Cantina@v2.0.0.pdf ├── Cantina@v2.0.0_2.pdf └── ChainSecurity@v0.1.0.pdf ├── package.json ├── slither.config.json ├── src ├── extensions │ ├── IScribeLST.sol │ ├── IScribeOptimisticLST.sol │ ├── external_ │ │ └── interfaces │ │ │ └── IRateSource.sol │ ├── ScribeLST.sol │ └── ScribeOptimisticLST.sol ├── libs │ ├── LICENSE │ └── LibSchnorr.sol ├── IScribeOptimistic.sol └── IScribe.sol ├── .gitmodules ├── script ├── dev │ ├── print-errors.sh │ ├── print-storage-layout.sh │ ├── generate-abis.sh │ ├── generate-flattened.sh │ ├── invalid-oppoker.sh │ ├── gas-estimator.sh │ ├── ScribeTester.s.sol │ └── ScribeOptimisticTester.s.sol ├── libs │ ├── LibRandom.sol │ ├── LibDissig.sol │ ├── LibFeed.sol │ ├── LibOracleSuite.sol │ ├── LibSecp256k1Extended.sol │ └── LibSchnorrExtended.sol ├── benchmarks │ ├── visualize.py │ ├── challenger.sh │ ├── relay.sh │ ├── ScribeBenchmark.s.sol │ └── ScribeOptimisticBenchmark.s.sol ├── feeds │ └── wallet-generator.sh ├── rescue │ └── Rescuer.sol └── ScribeOptimistic.s.sol ├── remappings.txt ├── .github └── workflows │ ├── lint.yml │ ├── non-via-ir-compilation.yml │ ├── unit-tests.yml │ └── solc-version-tests.yml ├── .gitignore ├── test ├── inspectable │ └── ScribeInspectable.sol ├── extensions │ ├── IScribeLSTTest.sol │ └── IScribeOptimisticLSTTest.sol ├── vectors │ └── points.js ├── EVMTest.sol ├── invariants │ ├── FeedSet.sol │ ├── IScribeInvariantTest.sol │ └── ScribeHandler.sol ├── Runner.t.sol ├── LibSecp256k1Test.sol ├── rescue │ └── RescuerTest.sol └── LibSchnorrTest.sol ├── foundry.toml ├── docs ├── Benchmarks.md ├── Invariants.md ├── Deployment.md ├── Schnorr.md ├── Scribe.md └── Management.md ├── .env.example ├── CHANGELOG.md ├── README.md ├── yarn.lock ├── LICENSE └── .gas-snapshot /bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chronicleprotocol/scribe/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/benchmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chronicleprotocol/scribe/HEAD/assets/benchmarks.png -------------------------------------------------------------------------------- /audits/ABDK@v1.0.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chronicleprotocol/scribe/HEAD/audits/ABDK@v1.0.0.pdf -------------------------------------------------------------------------------- /audits/Cantina@v2.0.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chronicleprotocol/scribe/HEAD/audits/Cantina@v2.0.0.pdf -------------------------------------------------------------------------------- /audits/Cantina@v2.0.0_2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chronicleprotocol/scribe/HEAD/audits/Cantina@v2.0.0_2.pdf -------------------------------------------------------------------------------- /audits/ChainSecurity@v0.1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chronicleprotocol/scribe/HEAD/audits/ChainSecurity@v0.1.0.pdf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@noble/curves": "^0.8.2", 4 | "bcrypto": "^5.4.0", 5 | "viem": "^0.1.16" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "filter_paths": "lib,test,script", 3 | "solc_remaps": [ 4 | "chronicle-std/=lib/chronicle-std/src", 5 | "ds-test/=lib/forge-std/lib/ds-test/src/", 6 | "forge-std/=lib/forge-std/src/" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/extensions/IScribeLST.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {IScribe} from "../IScribe.sol"; 5 | 6 | import {IRateSource} from "./external_/interfaces/IRateSource.sol"; 7 | 8 | interface IScribeLST is IScribe, IRateSource {} 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | branch = v1 5 | [submodule "lib/chronicle-std"] 6 | path = lib/chronicle-std 7 | url = https://github.com/chronicleprotocol/chronicle-std 8 | branch = v2 9 | -------------------------------------------------------------------------------- /script/dev/print-errors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script to print all error identifiers. 4 | # 5 | # Run via: 6 | # ```bash 7 | # $ script/dev/print-errors.sh 8 | # ``` 9 | 10 | echo "Scribe(Optimistic) Errors" 11 | forge inspect src/ScribeOptimistic.sol:ScribeOptimistic errors 12 | -------------------------------------------------------------------------------- /src/extensions/IScribeOptimisticLST.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {IScribeOptimistic} from "../IScribeOptimistic.sol"; 5 | 6 | import {IRateSource} from "./external_/interfaces/IRateSource.sol"; 7 | 8 | interface IScribeOptimisticLST is IScribeOptimistic, IRateSource {} 9 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/forge-std/lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | 4 | chronicle-std/=lib/chronicle-std/src/ 5 | lib/chronicle-std:src/=lib/chronicle-std/src/ 6 | lib/chronicle-std:ds-test/=lib/chronicle-std/lib/forge-std/lib/ds-test/src/ 7 | lib/chronicle-std:forge-std/=lib/chronicle-std/lib/forge-std/src/ 8 | -------------------------------------------------------------------------------- /script/dev/print-storage-layout.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to print the storage layout of Scribe and ScribeOptimistic. 4 | # 5 | # Run via: 6 | # ```bash 7 | # $ script/dev/print-storage-layout.sh 8 | # ``` 9 | 10 | echo "Scribe Storage Layout" 11 | forge inspect src/Scribe.sol:Scribe storage --pretty 12 | 13 | echo "" 14 | 15 | echo "ScribeOptimistic Storage Layout" 16 | forge inspect src/ScribeOptimistic.sol:ScribeOptimistic storage --pretty 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: recursive 16 | 17 | - name: Install Foundry 18 | uses: foundry-rs/foundry-toolchain@v1 19 | with: 20 | version: nightly 21 | 22 | - name: Check formatting 23 | run: forge fmt --check 24 | -------------------------------------------------------------------------------- /script/dev/generate-abis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to generate Scribe's and ScribeOptimistic's ABIs. 4 | # Saves the ABIs in fresh abis/ directory. 5 | # 6 | # Run via: 7 | # ```bash 8 | # $ script/dev/generate-abis.sh 9 | # ``` 10 | 11 | rm -rf abis/ 12 | mkdir abis 13 | 14 | echo "Generating Scribe's ABI" 15 | forge inspect src/Scribe.sol:Scribe abi > abis/Scribe.json 16 | 17 | echo "" 18 | 19 | echo "Generating ScribeOptimistic's ABI" 20 | forge inspect src/ScribeOptimistic.sol:ScribeOptimistic abi > abis/ScribeOptimistic.json 21 | -------------------------------------------------------------------------------- /src/extensions/external_/interfaces/IRateSource.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity >=0.8.0; 3 | 4 | /** 5 | * @dev Interest rate oracle interface from [Spark](https://spark.fi/). 6 | * 7 | * Copied from https://github.com/marsfoundation/sparklend-advanced/blob/277ea9d9ad7faf330b88198c9c6de979a2fad561/src/interfaces/IRateSource.sol. 8 | */ 9 | interface IRateSource { 10 | /// @notice Returns the oracle's current APR value. 11 | /// @return The oracle's current APR value. 12 | function getAPR() external view returns (uint); 13 | } 14 | -------------------------------------------------------------------------------- /script/dev/generate-flattened.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to generate Scribe's and ScribeOptimistic's flattened contracts. 4 | # Saves the contracts in fresh flattened/ directory. 5 | # 6 | # Run via: 7 | # ```bash 8 | # $ script/dev/generate-flattened.sh 9 | # ``` 10 | 11 | rm -rf flattened/ 12 | mkdir flattened 13 | 14 | echo "Generating flattened Scribe contract" 15 | forge flatten src/Scribe.sol > flattened/Scribe.sol 16 | 17 | echo "" 18 | 19 | echo "Generating flattened ScribeOptimistic contract" 20 | forge flatten src/ScribeOptimistic.sol > flattened/ScribeOptimistic.sol 21 | -------------------------------------------------------------------------------- /.github/workflows/non-via-ir-compilation.yml: -------------------------------------------------------------------------------- 1 | name: Non --via-ir Compilation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: recursive 16 | - run: yarn install 17 | 18 | - name: Install Foundry 19 | uses: foundry-rs/foundry-toolchain@v1 20 | with: 21 | version: nightly 22 | 23 | - name: Compile project without --via-ir 24 | run: FOUNDRY_PROFILE=no-via-ir forge build 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # VSCode settings 2 | .vscode/ 3 | 4 | # Compiler files 5 | cache/ 6 | out/ 7 | 8 | # Ignore broadcast logs 9 | broadcast/ 10 | 11 | # Ignore zkout 12 | zkout/ 13 | 14 | # Forge auto-generated docs 15 | docs_generated/ 16 | 17 | # Dotenv file 18 | .env 19 | 20 | # Coverage file 21 | lcov.info 22 | 23 | # JS dependencies 24 | node_modules/ 25 | 26 | # Binaries 27 | !/bin 28 | bin/dissig 29 | bin/schnorr 30 | 31 | # Flattened contracts 32 | flattened/ 33 | 34 | # Generated contract ABIs 35 | abis/ 36 | 37 | # Heimdall output directory 38 | # See https://github.com/Jon-Becker/heimdall-rs 39 | output/ 40 | -------------------------------------------------------------------------------- /test/inspectable/ScribeInspectable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Scribe} from "src/Scribe.sol"; 5 | 6 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 7 | 8 | contract ScribeInspectable is Scribe { 9 | constructor(address initialAuthed, bytes32 wat_) 10 | Scribe(initialAuthed, wat_) 11 | {} 12 | 13 | function inspectable_pokeData() public view returns (PokeData memory) { 14 | return _pokeData; 15 | } 16 | 17 | function inspectable_pubKeys(uint8 feedId) 18 | public 19 | view 20 | returns (LibSecp256k1.Point memory) 21 | { 22 | return _pubKeys[feedId]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: recursive 16 | - run: yarn install 17 | 18 | - name: Install Foundry 19 | uses: foundry-rs/foundry-toolchain@v1 20 | with: 21 | version: nightly 22 | 23 | - name: Run Forge build 24 | run: forge build 25 | id: build 26 | 27 | - name: Run Forge test 28 | run: FOUNDRY_PROFILE=ci forge test -vvv --nmt "FuzzDifferentialOracleSuite" 29 | id: test 30 | -------------------------------------------------------------------------------- /.github/workflows/solc-version-tests.yml: -------------------------------------------------------------------------------- 1 | name: Solc Version Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: recursive 16 | - run: yarn install 17 | 18 | - name: Install Foundry 19 | uses: foundry-rs/foundry-toolchain@v1 20 | with: 21 | version: nightly 22 | 23 | - name: Run Forge tests against lowest and highest supported solc version 24 | run: > 25 | forge test --use 0.8.16 --nmt "FuzzDifferentialOracleSuite" && 26 | forge test --use 0.8.27 --nmt "FuzzDifferentialOracleSuite" 27 | -------------------------------------------------------------------------------- /test/extensions/IScribeLSTTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {IToll} from "chronicle-std/toll/IToll.sol"; 5 | 6 | import {IScribeLST} from "src/extensions/IScribeLST.sol"; 7 | 8 | import {IScribeTest} from "../IScribeTest.sol"; 9 | 10 | abstract contract IScribeLSTTest is IScribeTest { 11 | IScribeLST private scribeLST; 12 | 13 | function setUp(address scribe_) internal override(IScribeTest) { 14 | super.setUp(scribe_); 15 | 16 | scribeLST = IScribeLST(scribe_); 17 | } 18 | 19 | function test_getAPR_DoesNotRevertIf_ValIsZero() public { 20 | uint val = scribeLST.getAPR(); 21 | assertEq(val, 0); 22 | } 23 | 24 | // -- Test: Toll Protected Functions -- 25 | 26 | function test_getAPR_isTollProtected() public { 27 | vm.prank(address(0xbeef)); 28 | vm.expectRevert( 29 | abi.encodeWithSelector(IToll.NotTolled.selector, address(0xbeef)) 30 | ); 31 | scribeLST.getAPR(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/extensions/IScribeOptimisticLSTTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {IToll} from "chronicle-std/toll/IToll.sol"; 5 | 6 | import {IScribeOptimisticLST} from "src/extensions/IScribeOptimisticLST.sol"; 7 | 8 | import {IScribeOptimisticTest} from "../IScribeOptimisticTest.sol"; 9 | 10 | abstract contract IScribeOptimisticLSTTest is IScribeOptimisticTest { 11 | IScribeOptimisticLST private opScribeLST; 12 | 13 | function setUp(address scribe_) internal override(IScribeOptimisticTest) { 14 | super.setUp(scribe_); 15 | 16 | opScribeLST = IScribeOptimisticLST(scribe_); 17 | } 18 | 19 | function test_getAPR_DoesNotRevertIf_ValIsZero() public { 20 | uint val = opScribeLST.getAPR(); 21 | assertEq(val, 0); 22 | } 23 | 24 | // -- Test: Toll Protected Functions -- 25 | 26 | function test_getAPR_isTollProtected() public { 27 | vm.prank(address(0xbeef)); 28 | vm.expectRevert( 29 | abi.encodeWithSelector(IToll.NotTolled.selector, address(0xbeef)) 30 | ); 31 | opScribeLST.getAPR(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/libs/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chronicle Association 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /script/libs/LibRandom.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | 6 | import {console2} from "forge-std/console2.sol"; 7 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 8 | 9 | /** 10 | * @title LibRandom 11 | * 12 | * @notice Library providing access to cryptographically sound randomness 13 | * 14 | * @dev Randomness is sourced from cast's `new wallet` command. 15 | */ 16 | library LibRandom { 17 | Vm private constant vm = 18 | Vm(address(uint160(uint(keccak256("hevm cheat code"))))); 19 | 20 | /// @dev Returns 256 bit of cryptographically sound randomness. 21 | function readUint() internal returns (uint) { 22 | string[] memory inputs = new string[](3); 23 | inputs[0] = "cast"; 24 | inputs[1] = "wallet"; 25 | inputs[2] = "new"; 26 | 27 | bytes memory result = vm.ffi(inputs); 28 | 29 | // Note that while parts of `cast wallet new` output is constant, it 30 | // always contains the new wallet's private key and is therefore unique. 31 | // 32 | // Note that cast is trusted to create cryptographically secure wallets. 33 | return uint(keccak256(result)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/extensions/ScribeLST.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.16; 3 | 4 | import {IScribeLST} from "./IScribeLST.sol"; 5 | import {IRateSource} from "./external_/interfaces/IRateSource.sol"; 6 | 7 | import {Scribe} from "../Scribe.sol"; 8 | 9 | /** 10 | * @title ScribeLST 11 | * 12 | * @notice Schnorr based Oracle with onchain fault resolution for Liquid 13 | * Staking Token APRs. 14 | */ 15 | contract ScribeLST is IScribeLST, Scribe { 16 | constructor(address initialAuthed_, bytes32 wat_) 17 | Scribe(initialAuthed_, wat_) 18 | {} 19 | 20 | /// @inheritdoc IRateSource 21 | /// @dev Only callable by toll'ed address. 22 | function getAPR() external view toll returns (uint) { 23 | // Note that function does not revert if val is zero. 24 | return _pokeData.val; 25 | } 26 | } 27 | 28 | /** 29 | * @dev Contract overwrite to deploy contract instances with specific naming. 30 | * 31 | * For more info, see docs/Deployment.md. 32 | */ 33 | contract Chronicle_BASE_QUOTE_COUNTER is ScribeLST { 34 | // @todo ^^^^ ^^^^^ ^^^^^^^ Adjust name of Scribe instance. 35 | constructor(address initialAuthed, bytes32 wat_) 36 | ScribeLST(initialAuthed, wat_) 37 | {} 38 | } 39 | -------------------------------------------------------------------------------- /script/benchmarks/visualize.py: -------------------------------------------------------------------------------- 1 | # Script to plot benchmark results. Result is saved in `benchmarks.png` 2 | # 3 | # Run via: 4 | # ```bash 5 | # $ python script/benchmarks/visualize.py 6 | # ``` 7 | import matplotlib.pyplot as plt 8 | 9 | # Bar configuration 10 | x = [5, 10, 15, 20, 50, 100, 200, 255] 11 | 12 | # Scribe's poke benchmark results received via `relay.sh` 13 | scribe = [80280, 105070, 132414, 156983, 314455, 574227, 1096599, 1382810] 14 | 15 | # ScribeOptimistic's opPoke benchmark results received via `relay.sh` 16 | opScribe = [68815, 68887, 68944, 69004, 69791, 71186, 73630, 74735] 17 | 18 | # Challenger's opChallenge benchmark results received via `challenger.sh` 19 | challenger = [90374, 115745, 143701, 168848, 330371, 596972, 1132141, 1424857] 20 | 21 | # Plotting the benchmark data 22 | plt.plot(x, scribe, label='Scribe') 23 | plt.plot(x, opScribe, label='ScribeOptimistic') 24 | plt.plot(x, challenger, label='Challenger') 25 | 26 | # Adjust the margins 27 | plt.subplots_adjust(left=0.2, right=0.9, bottom=0.1, top=0.9) 28 | # Add a legend 29 | plt.legend() 30 | 31 | # Adding labels and title 32 | plt.xlabel('number of bar') 33 | plt.ylabel('(op)poke()/opChallenge() gas usage') 34 | plt.title('Relay and Challenger Benchmark Results') 35 | 36 | # Save graph to file 37 | plt.savefig('benchmarks.png') 38 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | ffi = true 6 | 7 | # Compilation 8 | solc_version = "0.8.16" 9 | optimizer = true 10 | optimizer_runs = 10_000 11 | via_ir = true 12 | extra_output_files = ["metadata", "irOptimized"] 13 | 14 | # Testing 15 | fuzz = { runs = 50 } 16 | block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT 17 | 18 | [invariant] 19 | fail_on_revert = true 20 | 21 | [fmt] 22 | line_length = 80 23 | int_types = "short" 24 | number_underscore = "preserve" 25 | ignore = [] 26 | 27 | [doc] 28 | out = "docs_generated" # Note to not overwrite own docs 29 | 30 | # Profile to compile without --via-ir and optimizations 31 | # Run via `FOUNDRY_PROFILE=no-via-ir forge ...` 32 | [profile.no-via-ir] 33 | optimizer = false 34 | via_ir = false 35 | 36 | # Profile for intense testing 37 | # Run via `FOUNDRY_PROFILE=intense forge t` 38 | [profile.intense] 39 | [profile.intense.fuzz] 40 | runs = 10_000 41 | [profile.intense.invariant] 42 | runs = 10_000 43 | 44 | # Profile for CI testing 45 | # Run via `FOUNDRY_PROFILE=ci forge t` 46 | [profile.ci] 47 | [profile.ci.fuzz] 48 | runs = 100 49 | [profile.ci.invariant] 50 | runs = 100 51 | 52 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 53 | 54 | 55 | [profile.default.zksync] 56 | zksolc = "1.5.1" 57 | -------------------------------------------------------------------------------- /src/extensions/ScribeOptimisticLST.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.16; 3 | 4 | import {IScribeOptimisticLST} from "./IScribeOptimisticLST.sol"; 5 | import {IRateSource} from "./external_/interfaces/IRateSource.sol"; 6 | 7 | import {ScribeOptimistic} from "../ScribeOptimistic.sol"; 8 | 9 | /** 10 | * @title ScribeOptimisticLST 11 | * 12 | * @notice Schnorr based optimistic Oracle with onchain fault resolution for 13 | * Liquid Staking Token APRs. 14 | */ 15 | contract ScribeOptimisticLST is IScribeOptimisticLST, ScribeOptimistic { 16 | constructor(address initialAuthed_, bytes32 wat_) 17 | ScribeOptimistic(initialAuthed_, wat_) 18 | {} 19 | 20 | /// @inheritdoc IRateSource 21 | /// @dev Only callable by toll'ed address. 22 | function getAPR() external view toll returns (uint) { 23 | // Note that function does not revert if val is zero. 24 | return _currentPokeData().val; 25 | } 26 | } 27 | 28 | /** 29 | * @dev Contract overwrite to deploy contract instances with specific naming. 30 | * 31 | * For more info, see docs/Deployment.md. 32 | */ 33 | contract Chronicle_BASE_QUOTE_COUNTER is ScribeOptimisticLST { 34 | // @todo ^^^^ ^^^^^ ^^^^^^^ Adjust name of Scribe instance. 35 | constructor(address initialAuthed, bytes32 wat_) 36 | ScribeOptimisticLST(initialAuthed, wat_) 37 | {} 38 | } 39 | -------------------------------------------------------------------------------- /docs/Benchmarks.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | The benchmark for `Scribe` is based on the `poke()` function while the benchmark for `ScribeOptimistic` being based on the `opPoke()` function. The challenger's benchmark is based on the `opChallenge()` function. 4 | 5 | | `bar` | `Scribe::poke()` | `ScribeOptimistic::opPoke()` | `ScribeOptimistic::opChallenge()` | 6 | | ----- | ---------------- | ---------------------------- | --------------------------------- | 7 | | 5 | 81,025 | 68,944 | 90,374 | 8 | | 10 | 106,395 | 69,004 | 115,745 | 9 | | 15 | 134,342 | 69,061 | 143,701 | 10 | | 20 | 159,488 | 69,133 | 168,848 | 11 | | 50 | 320,473 | 69,908 | 330,371 | 12 | | 100 | 585,993 | 71,315 | 596,972 | 13 | | 200 | 1,119,535 | 73,759 | 1,132,141 | 14 | | 255 | 1,411,702 | 74,852 | 1,424,857 | 15 | 16 | The following visualization shows the gas usage for different numbers of `bar`: 17 | 18 | ![](../assets/benchmarks.png) 19 | 20 | For more info, see the `script/benchmarks/` directory. 21 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Wallet Management 2 | export KEYSTORE= 3 | export KEYSTORE_PASSWORD="" 4 | 5 | # RPC URL 6 | export RPC_URL= 7 | 8 | # Etherscan Verification 9 | export ETHERSCAN_API_URL= 10 | export ETHERSCAN_API_KEY= 11 | 12 | # Deployment+Management Configurations 13 | export SCRIBE_FLAVOUR= 14 | 15 | # Deployment Configurations 16 | export INITIAL_AUTHED= 17 | export WAT= 18 | 19 | # Management Configurations 20 | export SCRIBE= 21 | ## IScribe::setBar 22 | export BAR= 23 | ## IScribe::lift 24 | export PUBLIC_KEY_X_COORDINATE= 25 | export PUBLIC_KEY_Y_COORDINATE= 26 | export ECDSA_V= 27 | export ECDSA_R= 28 | export ECDSA_S= 29 | ## IScribe::lift multiple 30 | export PUBLIC_KEY_X_COORDINATES="[]" 31 | export PUBLIC_KEY_Y_COORDINATES="[]" 32 | export ECDSA_VS="[]" 33 | export ECDSA_RS="[]" 34 | export ECDSA_SS="[]" 35 | ## IScribe::drop 36 | export FEED_ID= 37 | ## IScribe::drop multiple 38 | export FEED_IDS="[]" 39 | ## IScribeOptimistic::setOpChallengePeriod 40 | export OP_CHALLENGE_PERIOD= 41 | ## IScribeOptimistic::setMaxChallengeReward 42 | export MAX_CHALLENGE_REWARD= 43 | ## IAuth::{rely,deny}, IToll::{kiss,diss} 44 | export WHO= 45 | 46 | 47 | # == Only for testing == 48 | # See script/dev/Scribe(Optimistic)?.s.sol 49 | export TEST_FEED_PRIVATE_KEYS="[]" 50 | export TEST_FEED_SIGNERS_PRIVATE_KEYS="[]" 51 | export TEST_POKE_VAL= 52 | export TEST_POKE_AGE= 53 | export TEST_SCHNORR_SIGNATURE= 54 | export TEST_SCHNORR_COMMITMENT= 55 | export TEST_SCHNORR_FEED_IDS= 56 | -------------------------------------------------------------------------------- /test/vectors/points.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper Script returning `points.json` vector file's test cases as Ethereum 3 | * ABI encoded uint[]. 4 | * 5 | * Usage: 6 | * ```bash 7 | * $ node test/vectors/points.js 8 | * ``` 9 | * 10 | * Outputs: 11 | * Prints an ABI encoded uint[]. The elements should be read sequentially. 12 | * The first element is the x coordinate of the first point. The second element 13 | * the y coordinate of the first point. Afterwards the x coordinate of the 14 | * second point, and so forth. 15 | * 16 | * One test case contains 3 points - p, q, and expected - for which the 17 | * following should hold: p + q = expected 18 | */ 19 | const { readFileSync } = require('fs'); 20 | const { secp256k1 } = require('@noble/curves/secp256k1'); 21 | const { encodeAbiParameters } = require("viem"); 22 | 23 | const Point = secp256k1.ProjectivePoint; 24 | 25 | function main() { 26 | const points = JSON.parse(readFileSync("test/vectors/points.json")); 27 | 28 | let out = []; 29 | for (const vector of points.vectors) { 30 | const { P, Q, expected } = vector; 31 | 32 | let p = Point.fromHex(P); 33 | let q = Point.fromHex(Q); 34 | let e = expected ? Point.fromHex(expected) : q; 35 | 36 | // Convert to Affine form. 37 | p = p.toAffine(); 38 | q = q.toAffine(); 39 | e = e.toAffine(); 40 | 41 | out.push( 42 | p.x, 43 | p.y, 44 | q.x, 45 | q.y, 46 | e.x, 47 | e.y, 48 | ); 49 | } 50 | 51 | // Encode test cases to uint[]. 52 | const encoded = encodeAbiParameters( 53 | [{ type: 'uint[]' }], 54 | [out] 55 | ) 56 | console.log(encoded); 57 | } 58 | 59 | main(); 60 | -------------------------------------------------------------------------------- /script/feeds/wallet-generator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tool to generate new encryped keystore with Ethereum address matching 4 | # a specific first byte identifier. 5 | # 6 | # Dependencies: 7 | # - cast, see https://getfoundry.sh 8 | # - unix utilities 9 | # 10 | # Usage: 11 | # ./wallet-generator.sh <0x prefixed byte> 12 | # 13 | # Example: 14 | # ./wallet-generator.sh 0xff test ./keystores 15 | 16 | if [ "$#" -ne 3 ]; then 17 | echo "Usage: $0 <0x prefixed byte> " 18 | exit 1 19 | fi 20 | 21 | assigned_id="$1" 22 | password="$2" 23 | path="$3" 24 | 25 | if [ -z "$assigned_id" ] || [ -z "$password" ] || [ -z "$path" ]; then 26 | echo "Usage: $0 <0x prefixed byte> " 27 | exit 1 28 | fi 29 | 30 | # Note to ensure assigned_id is in lower case. 31 | assigned_id=$(echo "$assigned_id" | tr '[:upper:]' '[:lower:]') 32 | 33 | ctr=0 34 | while true; do 35 | # Create new keystore and catch output. 36 | output=$(cast wallet new --unsafe-password "$password" "$path") 37 | 38 | # Get path and address of new keystore from output. 39 | keystore=$(echo "$output" | awk '/Created new encrypted keystore file:/ {print $6}') 40 | address=$(echo "$output" | awk '/Address:/ {print $2}') 41 | 42 | # Get address' id in lower case. 43 | id=$(echo "${address:0:4}" | tr '[:upper:]' '[:lower:]') 44 | 45 | # Check whether first byte matches assigned id. 46 | if [ "$id" == "$assigned_id" ]; then 47 | # Found fitting address. Print output and exit. 48 | echo "Generated new validator address with id=$id. Needed $ctr tries." 49 | echo "Keystore: $keystore" 50 | echo "Address: $address" 51 | 52 | exit 0 53 | else 54 | # If address does not fit, delete keystore again. 55 | rm "$keystore" 56 | fi 57 | 58 | ctr=$((ctr + 1)) 59 | done 60 | -------------------------------------------------------------------------------- /script/benchmarks/challenger.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to run challenger benchmarks. 4 | # 5 | # Run via: 6 | # ```bash 7 | # $ script/benchmarks/challenger.sh 8 | # ``` 9 | 10 | poke () { 11 | forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "poke()" > /dev/null 2>&1 12 | sleep 1 13 | } 14 | 15 | run () { 16 | local bar=$1 17 | echo "Benchmarking bar=$bar" 18 | # Start anvil in background 19 | anvil -b 1 > /dev/null & 20 | anvilPID=$! 21 | 22 | # Deploy ScribeOptimistic 23 | forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "deploy()" > /dev/null 2>&1 24 | # Set bar 25 | forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig $(cast calldata "setBar(uint8)" $bar) > /dev/null 2>&1 26 | # Lift feeds 27 | forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "liftFeeds()" > /dev/null 2>&1 28 | 29 | # Note to poke couple of times to have non-zero storage slots. 30 | poke 31 | poke 32 | 33 | # Make invalid opPoke, challenge it, and grep the gas usage. 34 | cost=$(forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "opPokeInvalidAndChallenge()" 2>/dev/null | grep -oE "[0-9]+ gas") 35 | 36 | # Kill anvil 37 | kill $anvilPID 38 | 39 | # Print cost of challenge tx. 40 | # Note that $cost contains 3 gas numbers, opPoke cost, opChallenge cost, 41 | # and total cost, eg `2000 gas 3000 gas 5000 gas`. 42 | # Therefore, only select the middle gas value. 43 | echo $cost | awk '{print $3, " ", $4}' 44 | echo "" 45 | } 46 | 47 | echo "=== Challenger Benchmarks (Printing cost of opChallenge())" 48 | run 5 49 | run 10 50 | run 15 51 | run 20 52 | run 50 53 | run 100 54 | run 200 55 | run 255 56 | -------------------------------------------------------------------------------- /script/libs/LibDissig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.16; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | 6 | import {console2} from "forge-std/console2.sol"; 7 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 8 | 9 | /** 10 | * @title LibDissig 11 | * 12 | * @notice Wrapper library for the `dissig` cli tool 13 | * 14 | * @dev Expects `dissig` binary to be in the `bin/` directory. 15 | * 16 | * For more info, see https://github.com/chronicleprotocol/dissig. 17 | */ 18 | library LibDissig { 19 | Vm private constant vm = 20 | Vm(address(uint160(uint(keccak256("hevm cheat code"))))); 21 | 22 | /// @dev Signs message `message` via set of private keys `privKeys`. 23 | /// 24 | /// Signed via: 25 | /// ```bash 26 | /// $ ./bin/dissig \ 27 | /// --scribe \ 28 | /// --scribe-cmd=sign \ 29 | /// --scribe-message= \ 30 | /// --scribe-privKeys= \ 31 | /// --scribe-privKeys= | 32 | /// ... 33 | /// ``` 34 | function sign(uint[] memory privKeys, bytes32 message) 35 | internal 36 | returns (uint, address) 37 | { 38 | string[] memory inputs = new string[](4 + privKeys.length); 39 | inputs[0] = "bin/dissig"; 40 | inputs[1] = "--scribe"; 41 | inputs[2] = "--scribe-cmd=sign"; 42 | inputs[3] = string.concat("--scribe-message=", vm.toString(message)); 43 | for (uint i; i < privKeys.length; i++) { 44 | inputs[4 + i] = string.concat( 45 | "--scribe-privKeys=", vm.toString(bytes32(privKeys[i])) 46 | ); 47 | } 48 | 49 | uint[2] memory result = abi.decode(vm.ffi(inputs), (uint[2])); 50 | 51 | uint signature = result[0]; 52 | address commitment = address(uint160(result[1])); 53 | 54 | return (signature, commitment); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Common Changelog](https://common-changelog.org/). 6 | 7 | [2.0.1]: https://github.com/chronicleprotocol/scribe/releases/tag/v2.0.1 8 | [2.0.0]: https://github.com/chronicleprotocol/scribe/releases/tag/v2.0.0 9 | [1.2.0]: https://github.com/chronicleprotocol/scribe/releases/tag/v1.2.0 10 | [1.1.0]: https://github.com/chronicleprotocol/scribe/releases/tag/v1.1.0 11 | [1.0.0]: https://github.com/chronicleprotocol/scribe/releases/tag/v1.0.0 12 | 13 | ## [2.0.1] - 2024-10-03 14 | 15 | ### Added 16 | 17 | - Security notice about rogue key vulnerability during lift and requirement for additional external verification ([0ef985b](https://github.com/chronicleprotocol/scribe/commit/0ef985baebc2945017bff811bb65a883f565fc4f)) 18 | 19 | ## [2.0.0] - 2023-11-27 20 | 21 | ### Changed 22 | 23 | - **Breaking** Use 1-byte identifier for feeds based on highest-order byte of their addresses instead of their storage array's index ([#23](https://github.com/chronicleprotocol/scribe/pull/23)) 24 | - **Breaking** Change `IScribe` and `IScribeOptimistic` interfaces to account for new feed identification ([#23](https://github.com/chronicleprotocol/scribe/pull/23)) 25 | 26 | ### Fixed 27 | 28 | - DOS vector in `ScribeOptimistic::opPoke` making `ScribeOptimistic::opChallenge` economically unprofitable ([#23](https://github.com/chronicleprotocol/scribe/pull/23)) 29 | - Possibility to successfully `opChallenge` a valid `opPoke` via non-default calldata encoding ([#23](https://github.com/chronicleprotocol/scribe/pull/23)) 30 | 31 | ## [1.2.0] - 2023-09-29 32 | 33 | ### Added 34 | 35 | - Chainlink compatibility function `latestAnswer()(int)` ([#24](https://github.com/chronicleprotocol/scribe/pull/24)) 36 | 37 | ## [1.1.0] - 2023-08-25 38 | 39 | ### Fixes 40 | 41 | - Broken compilation without `--via-ir` pipeline ([#13](https://github.com/chronicleprotocol/scribe/pull/13)) 42 | 43 | ### Added 44 | 45 | - MakerDAO compatibility function `peep()(uint,bool)` ([#12](https://github.com/chronicleprotocol/scribe/pull/12)) 46 | 47 | ## [1.0.0] - 2023-08-14 48 | 49 | ### Added 50 | 51 | - Initial release 52 | -------------------------------------------------------------------------------- /script/dev/invalid-oppoker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to (invalid) opPoke a ScribeOptimistic instance. 4 | # Useful to test opChallenger implementations. 5 | # 6 | # Requirements: 7 | # - Expects ScribeOptimistic instance to be deploed at `SCRIBE` 8 | # - Expects an `opChallengePeriod` of at most 10 minutes 9 | # 10 | # Run via: 11 | # ```bash 12 | # $ script dev/invalid-oppoker.sh 13 | # ``` 14 | 15 | # @todo Set constants 16 | PRIVATE_KEY= 17 | RPC_URL= 18 | SCRIBE= 19 | # Feed private keys taken from script/dev/test-feeds.json. 20 | FEED_PRIVATE_KEYS='[2,3,4,5,6,7,8,9,10,11,12,14,15,16,17,18,19,20,21,22]' 21 | 22 | while true; do 23 | # Lift feeds 24 | # Note that feeds must be lifted after each opPoke call to ensure a possibly 25 | # kicked feed, due to signing an invalid opPoke and being challenged, is 26 | # lifted again 27 | forge script \ 28 | --private-key "$PRIVATE_KEY" \ 29 | --broadcast \ 30 | --rpc-url "$RPC_URL" \ 31 | --sig $(cast calldata "lift(address,uint[])" "$SCRIBE" "$FEED_PRIVATE_KEYS") \ 32 | script/dev/ScribeTester.s.sol:ScribeTesterScript 33 | 34 | # Generate a random number between 1 and 100 35 | random_number=$((RANDOM % 100 + 1)) 36 | 37 | if [ "$random_number" -le 50 ]; then 38 | # If number is <= 50 (ie 50% chance), make invalid opPoke. 39 | forge script \ 40 | --private-key "$PRIVATE_KEY" \ 41 | --broadcast \ 42 | --rpc-url "$RPC_URL" \ 43 | --sig $(cast calldata "opPoke_invalid(address,uint[],uint128)" "$SCRIBE" "$FEED_PRIVATE_KEYS" "$random_number") \ 44 | script/dev/ScribeOptimisticTester.s.sol:ScribeOptimisticTesterScript 45 | 46 | echo "Pushed invalid opPoke" 47 | else 48 | # Otherwise make valid opPoke. 49 | forge script \ 50 | --private-key "$PRIVATE_KEY" \ 51 | --broadcast \ 52 | --rpc-url "$RPC_URL" \ 53 | --sig $(cast calldata "opPoke(address,uint[],uint128)" "$SCRIBE" "$FEED_PRIVATE_KEYS" "$random_number") \ 54 | script/dev/ScribeOptimisticTester.s.sol:ScribeOptimisticTesterScript 55 | 56 | echo "Pushed valid opPoke" 57 | fi 58 | 59 | # Sleep for 10 minutes 60 | sleep 600 61 | done 62 | -------------------------------------------------------------------------------- /test/EVMTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 7 | 8 | abstract contract EVMTest is Test { 9 | /// @dev Tests that an assembly calldataload from an out-of-bounds calldata 10 | /// index returns 0. 11 | /// Note that ScribeOptimistic::opChallenge() requires such an 12 | /// expression to _not revert_. 13 | function testFuzz_calldataload_ReadingNonExistingCalldataReturnsZero( 14 | uint index 15 | ) public { 16 | uint minIndex; 17 | assembly ("memory-safe") { 18 | minIndex := calldatasize() 19 | } 20 | vm.assume(minIndex <= index); 21 | 22 | uint got; 23 | assembly ("memory-safe") { 24 | got := calldataload(index) 25 | } 26 | assertEq(got, 0); 27 | } 28 | 29 | /// @dev Tests that: 30 | /// s ∊ [Q, type(uint).max] → ecrecover(_, _, _, s) = address(0) 31 | function testFuzz_ecrecover_ReturnsZeroAddress_If_S_IsGreaterThanOrEqualToQ( 32 | uint privKeySeed, 33 | uint sSeed 34 | ) public { 35 | // Let privKey ∊ [1, Q). 36 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 37 | 38 | // Let s ∊ [Q, type(uint).max]. 39 | bytes32 s = bytes32(_bound(sSeed, LibSecp256k1.Q(), type(uint).max)); 40 | 41 | // Create ECDSA signature. 42 | (, bytes32 r,) = vm.sign(privKey, keccak256("scribe")); 43 | 44 | assertEq(ecrecover(keccak256("scribe"), 27, r, s), address(0)); 45 | assertEq(ecrecover(keccak256("scribe"), 28, r, s), address(0)); 46 | } 47 | 48 | /// @dev Tests that: 49 | /// ecrecover(_, _, 0, _) = address(0) 50 | function testFuzz_ecrecover_ReturnsZeroAddress_If_R_IsZero(uint privKeySeed) 51 | public 52 | { 53 | // Let privKey ∊ [1, Q). 54 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 55 | 56 | // Create ECDSA signature. 57 | (,, bytes32 s) = vm.sign(privKey, keccak256("scribe")); 58 | 59 | assertEq(ecrecover(keccak256("scribe"), 27, 0, s), address(0)); 60 | assertEq(ecrecover(keccak256("scribe"), 28, 0, s), address(0)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/invariants/FeedSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 5 | import {LibFeed} from "script/libs/LibFeed.sol"; 6 | 7 | struct FeedSet { 8 | LibFeed.Feed[] feeds; 9 | mapping(address => bool) saved; 10 | mapping(address => bool) lifted; 11 | } 12 | 13 | /** 14 | * @author Inspired by horsefacts.eth's [article](https://mirror.xyz/horsefacts.eth/Jex2YVaO65dda6zEyfM_-DXlXhOWCAoSpOx5PLocYgw). 15 | */ 16 | library LibFeedSet { 17 | using LibSecp256k1 for LibSecp256k1.Point; 18 | 19 | function add(FeedSet storage s, LibFeed.Feed memory feed, bool lifted) 20 | internal 21 | { 22 | address addr = feed.pubKey.toAddress(); 23 | if (!s.saved[addr]) { 24 | s.feeds.push(feed); 25 | s.saved[addr] = true; 26 | s.lifted[addr] = lifted; 27 | } 28 | } 29 | 30 | function updateLifted( 31 | FeedSet storage s, 32 | LibFeed.Feed memory feed, 33 | bool lifted 34 | ) internal { 35 | address addr = feed.pubKey.toAddress(); 36 | require(s.saved[addr], "LibFeedSet::updateLifted: Unknown feed"); 37 | 38 | s.lifted[addr] = lifted; 39 | } 40 | 41 | function liftedFeeds(FeedSet storage s, uint amount) 42 | internal 43 | view 44 | returns (LibFeed.Feed[] memory) 45 | { 46 | LibFeed.Feed[] memory feeds = new LibFeed.Feed[](amount); 47 | uint ctr; 48 | for (uint i; i < s.feeds.length; i++) { 49 | address addr = s.feeds[i].pubKey.toAddress(); 50 | 51 | if (s.lifted[addr]) { 52 | feeds[ctr++] = s.feeds[i]; 53 | 54 | if (ctr == amount) break; 55 | } 56 | } 57 | 58 | require( 59 | ctr == amount, 60 | "LibFeedSet::liftedFeeds: Not enough lifted feeds in FeedSet" 61 | ); 62 | return feeds; 63 | } 64 | 65 | function rand(FeedSet storage s, uint seed) 66 | internal 67 | view 68 | returns (LibFeed.Feed memory) 69 | { 70 | require(s.feeds.length > 0, "LibFeedSet::rand: No feeds in FeedSet"); 71 | 72 | return s.feeds[seed % s.feeds.length]; 73 | } 74 | 75 | function count(FeedSet storage s) internal view returns (uint) { 76 | return s.feeds.length; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Unit Tests](https://github.com/chronicleprotocol/scribe/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/chronicleprotocol/scribe/actions/workflows/unit-tests.yml) 4 | 5 | Scribe is an efficient Schnorr multi-signature based Oracle. For more info, see [docs/Scribe.md](./docs/Scribe.md). 6 | 7 | ## Bug Bounty 8 | 9 | This repository is subject to _Chronicle Protocol_'s Bug Bounty program, per the terms defined [here](https://cantina.xyz/bounties/5240b7c7-6fec-4902-bec0-8cad12f14ec4). 10 | 11 | ## Installation 12 | 13 | Install module via Foundry: 14 | 15 | ```bash 16 | $ forge install chronicleprotocol/scribe 17 | ``` 18 | 19 | ## Contributing 20 | 21 | The project uses the Foundry toolchain. You can find installation instructions [here](https://getfoundry.sh/). 22 | 23 | Setup: 24 | 25 | ```bash 26 | $ git clone https://github.com/chronicleprotocol/scribe 27 | $ cd scribe/ 28 | $ forge install 29 | $ yarn install # Installs dependencies for vector-based tests 30 | ``` 31 | 32 | Run tests: 33 | 34 | ```bash 35 | $ forge test # Run all tests, including differential fuzzing tests 36 | $ forge test -vvvv # Run all tests with full stack traces 37 | $ FOUNDRY_PROFILE=intense forge test # Run all tests in intense mode 38 | $ forge test --nmt "FuzzDifferentialOracleSuite" # Run only non-differential fuzz tests 39 | ``` 40 | 41 | Note that in order to run the whole test suite, i.e. including differential fuzz tests, the oracle-suite's musig [`schnorr`](https://github.com/chronicleprotocol/musig/tree/master/cmd/schnorr) binary needs to be present inside the `bin/` directory. 42 | 43 | Lint: 44 | 45 | ```bash 46 | $ forge fmt [--check] 47 | ``` 48 | 49 | Update gas snapshots: 50 | 51 | ```bash 52 | $ forge snapshot --nmt "Fuzz" [--check] 53 | ``` 54 | 55 | ## Dependencies 56 | 57 | - [chronicleprotocol/chronicle-std@v2](https://github.com/chronicleprotocol/chronicle-std/tree/v2) 58 | 59 | ## Licensing 60 | 61 | The primary license for Scribe is the Business Source License 1.1 (`BUSL-1.1`), see [`LICENSE`](./LICENSE). However, some files are dual licensed under `MIT`: 62 | 63 | - All files in `src/libs/` may also be licensed under `MIT` (as indicated in their SPDX headers), see [`src/libs/LICENSE`](./src/libs/LICENSE) 64 | - Several Solidity interface files may also be licensed under `MIT` (as indicated in their SPDX headers) 65 | - Several files in `script/` may also be licensed under `MIT` (as indicated in their SPDX headers) 66 | -------------------------------------------------------------------------------- /test/Runner.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | // -- Test: Scribe -- 5 | 6 | import {Scribe} from "src/Scribe.sol"; 7 | import {ScribeInspectable} from "./inspectable/ScribeInspectable.sol"; 8 | import {IScribe} from "src/IScribe.sol"; 9 | 10 | import {IScribeTest} from "./IScribeTest.sol"; 11 | import {IScribeInvariantTest} from "./invariants/IScribeInvariantTest.sol"; 12 | import {ScribeHandler} from "./invariants/ScribeHandler.sol"; 13 | 14 | contract ScribeTest is IScribeTest { 15 | function setUp() public { 16 | setUp(address(new Scribe(address(this), "ETH/USD"))); 17 | } 18 | } 19 | 20 | contract ScribeInvariantTest is IScribeInvariantTest { 21 | function setUp() public { 22 | setUp( 23 | address(new ScribeInspectable(address(this), "ETH/USD")), 24 | address(new ScribeHandler()) 25 | ); 26 | } 27 | } 28 | 29 | // -- Extensions 30 | 31 | import {ScribeLST} from "src/extensions/ScribeLST.sol"; 32 | import {IScribeLSTTest} from "./extensions/IScribeLSTTest.sol"; 33 | 34 | contract ScribeLSTTest is IScribeLSTTest { 35 | function setUp() public { 36 | setUp(address(new ScribeLST(address(this), "ETH/USD"))); 37 | } 38 | } 39 | 40 | // -- Test: Optimistic Scribe -- 41 | 42 | import {ScribeOptimistic} from "src/ScribeOptimistic.sol"; 43 | import {IScribeOptimistic} from "src/IScribeOptimistic.sol"; 44 | 45 | import {IScribeOptimisticTest} from "./IScribeOptimisticTest.sol"; 46 | 47 | contract ScribeOptimisticTest is IScribeOptimisticTest { 48 | function setUp() public { 49 | setUp(address(new ScribeOptimistic(address(this), "ETH/USD"))); 50 | } 51 | } 52 | 53 | // -- Extensions 54 | 55 | import {ScribeOptimisticLST} from "src/extensions/ScribeOptimisticLST.sol"; 56 | import {IScribeOptimisticLSTTest} from 57 | "./extensions/IScribeOptimisticLSTTest.sol"; 58 | 59 | contract ScribeOptimisticLSTTest is IScribeOptimisticLSTTest { 60 | function setUp() public { 61 | setUp( 62 | payable(address(new ScribeOptimisticLST(address(this), "ETH/USD"))) 63 | ); 64 | } 65 | } 66 | 67 | // -- Test: Libraries -- 68 | 69 | import {LibSecp256k1Test as LibSecp256k1Test_} from "./LibSecp256k1Test.sol"; 70 | 71 | contract LibSecp256k1Test is LibSecp256k1Test_ {} 72 | 73 | import {LibSchnorrTest as LibSchnorrTest_} from "./LibSchnorrTest.sol"; 74 | 75 | contract LibSchnorrTest is LibSchnorrTest_ {} 76 | 77 | // -- Test: EVM Requirements -- 78 | 79 | import {EVMTest} from "./EVMTest.sol"; 80 | 81 | contract EVMTest_ is EVMTest {} 82 | -------------------------------------------------------------------------------- /script/benchmarks/relay.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to run relay benchmarks for Scribe and ScribeOptimistic. 4 | # 5 | # Run via: 6 | # ```bash 7 | # $ script/benchmarks/relay.sh 8 | # ``` 9 | 10 | poke () { 11 | forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "poke()" > /dev/null 2>&1 12 | sleep 1 13 | } 14 | 15 | run_Scribe () { 16 | local bar=$1 17 | echo "Benchmarking bar=$bar" 18 | # Start anvil in background 19 | anvil -b 1 > /dev/null & 20 | anvilPID=$! 21 | 22 | # Deploy Scribe 23 | forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "deploy()" > /dev/null 2>&1 24 | # Set bar 25 | forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig $(cast calldata "setBar(uint8)" $bar) > /dev/null 2>&1 26 | # Lift feeds 27 | forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "liftFeeds()" > /dev/null 2>&1 28 | # Poke once 29 | poke 30 | 31 | # Poke again and grep gas usage 32 | cost=$(forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "poke()" 2>/dev/null | grep -oE "[0-9]+ gas") 33 | 34 | # Kill anvil 35 | kill $anvilPID 36 | 37 | # Print cost of non-initial poke 38 | echo $cost | awk '{print $1, " ", $2}' 39 | echo "" 40 | } 41 | 42 | run_ScribeOptimistic () { 43 | local bar=$1 44 | echo "Benchmarking bar=$bar" 45 | # Start anvil in background 46 | anvil -b 1 > /dev/null & 47 | anvilPID=$! 48 | 49 | # Deploy ScribeOptimistic 50 | forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "deploy()" > /dev/null 2>&1 51 | # Set bar 52 | forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig $(cast calldata "setBar(uint8)" $bar) > /dev/null 2>&1 53 | # Lift feeds 54 | forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "liftFeeds()" > /dev/null 2>&1 55 | # Poke once 56 | poke 57 | # Poke again 58 | poke 59 | 60 | # opPoke and grep gas usage 61 | cost=$(forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "opPoke()" 2>/dev/null | grep -oE "[0-9]+ gas") 62 | 63 | # Kill anvil 64 | kill $anvilPID 65 | 66 | # Print cost of non-initial opPoke 67 | echo $cost | awk '{print $1, " ", $2}' 68 | echo "" 69 | } 70 | 71 | 72 | echo "=== Relay Scribe Benchmarks (Printing cost of non-initial poke())" 73 | run_Scribe 5 74 | run_Scribe 10 75 | run_Scribe 15 76 | run_Scribe 20 77 | run_Scribe 50 78 | run_Scribe 100 79 | run_Scribe 200 80 | run_Scribe 255 81 | 82 | echo "=== Relay Scribe Optimistic Benchmarks (Printing cost of non-initial opPoke())" 83 | run_ScribeOptimistic 5 84 | run_ScribeOptimistic 10 85 | run_ScribeOptimistic 15 86 | run_ScribeOptimistic 20 87 | run_ScribeOptimistic 50 88 | run_ScribeOptimistic 100 89 | run_ScribeOptimistic 200 90 | run_ScribeOptimistic 255 91 | -------------------------------------------------------------------------------- /docs/Invariants.md: -------------------------------------------------------------------------------- 1 | # Invariants 2 | 3 | This document specifies invariants of the Scribe and ScribeOptimistic oracle contracts. 4 | 5 | ## `Scribe::_pokeData` 6 | 7 | * Only `poke` function may mutate the `_pokeData`: 8 | ``` 9 | preTx(_pokeData) != postTx(_pokeData) 10 | → msg.sig == "poke" 11 | ``` 12 | 13 | * `_pokeData.age` may only be mutated to `block.timestamp`: 14 | ``` 15 | preTx(_pokeData.age) != postTx(_pokeData.age) 16 | → postTx(_pokeData.age) == block.timestamp 17 | ``` 18 | 19 | ## `ScribeOptimistic::_pokeData` 20 | 21 | * Only `poke`, `opPoke`, `opChallenge` and `_afterAuthedAction` protected auth'ed functions may mutate `_pokeData`: 22 | ``` 23 | preTx(_pokeData) != postTx(_pokeData) 24 | → msg.sig ∊ {"poke", "opPoke", "opChallenge", "setBar", "drop", "setOpChallengePeriod"} 25 | ``` 26 | 27 | * `poke` function may only mutate `_pokeData.age` to `block.timestamp`: 28 | ``` 29 | preTx(_pokeData) != postTx(_pokeData) ⋀ msg.sig == "poke" 30 | → postTx(_pokeData.age) == block.timestamp 31 | ``` 32 | 33 | * `opPoke`, `opChallenge` and `_afterAuthedAction` protected auth'ed functions may only mutate `_pokeData` to a finalized, non-stale `_opPokeData`: 34 | ``` 35 | preTx(_pokeData) != postTx(_pokeData) ⋀ msg.sig ∊ {"opPoke", "opChallenge", "setBar", "drop", "setOpChallengePeriod"} 36 | → postTx(_pokeData) = preTx(_opPokeData) ⋀ preTx(readWithAge()) = preTx(_opPokeData) 37 | ``` 38 | 39 | ## `{Scribe, ScribeOptimistic}::_pokeData` 40 | 41 | * `_pokeData.age` is strictly monotonically increasing: 42 | ``` 43 | preTx(_pokeData.age) != postTx(_pokeData.age) 44 | → preTx(_pokeData.age) < postTx(_pokeData.age) 45 | ``` 46 | 47 | * `_pokeData.val` can only be read by _toll'ed_ caller. 48 | 49 | ## `ScribeOptimistic::_opPokeData` 50 | 51 | * Only `opPoke`, `opChallenge` and `_afterAuthedAction` protected auth'ed functions may mutate `_opPokeData`: 52 | ``` 53 | preTx(_opPokeData) != postTx(_opPokeData) 54 | → msg.sig ∊ {"opPoke", "opChallenge", "setBar", "drop", "setOpChallengePeriod"} 55 | ``` 56 | 57 | * `opPoke` function may only set `_opPokeData.age` to `block.timestamp`: 58 | ``` 59 | preTx(_opPokeData.age) != postTx(_opPokeData.age) ⋀ msg.sig == "opPoke" 60 | → postTx(_opPokeData.age) == block.timestamp 61 | ``` 62 | 63 | * `opChallenge` and `_afterAuthedAction` protected auth'ed functions may only delete `_opPokeData`: 64 | ``` 65 | preTx(_opPokeData.age) != postTx(_opPokeData.age) ⋀ msg.sig ∊ {"opChallenge", "setBar", "drop", "setOpChallengePeriod"} 66 | → postTx(_opPokeData.val) == 0 ⋀ postTx(_opPokeData.age) == 0 67 | ``` 68 | 69 | ## `{Scribe, ScribeOptimistic}::_pubKeys` 70 | 71 | * `_pubKeys`' length is 256: 72 | ``` 73 | _pubKeys.length == 256 74 | ``` 75 | 76 | * Public keys are stored at the index of their address' first byte: 77 | ``` 78 | ∀id ∊ Uint8: _pubKeys[id].isZeroPoint() ∨ (_pubKeys[id].toAddress() >> 152) == id 79 | ``` 80 | 81 | * Only functions `lift` and `drop` may mutate the array's state: 82 | ``` 83 | ∀id ∊ Uint8: preTx(_pubKeys[id]) != postTx(_pubKeys[id]) 84 | → msg.sig ∊ {"lift", "drop"} 85 | ``` 86 | 87 | * Array's state may only be mutated by auth'ed caller: 88 | ``` 89 | ∀id ∊ Uint8: preTx(_pubKeys[id]) != postTx(_pubKeys[id]) 90 | → authed(msg.sender) 91 | ``` 92 | -------------------------------------------------------------------------------- /test/invariants/IScribeInvariantTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {console2} from "forge-std/console2.sol"; 6 | 7 | import {IAuth} from "chronicle-std/auth/IAuth.sol"; 8 | import {IToll} from "chronicle-std/toll/IToll.sol"; 9 | 10 | import {IScribe} from "src/IScribe.sol"; 11 | import {ScribeInspectable} from "../inspectable/ScribeInspectable.sol"; 12 | 13 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 14 | 15 | import {ScribeHandler} from "./ScribeHandler.sol"; 16 | 17 | abstract contract IScribeInvariantTest is Test { 18 | using LibSecp256k1 for LibSecp256k1.Point; 19 | 20 | ScribeInspectable private scribe; 21 | ScribeHandler private handler; 22 | 23 | function setUp(address scribe_, address handler_) internal virtual { 24 | scribe = ScribeInspectable(scribe_); 25 | handler = ScribeHandler(handler_); 26 | 27 | // Toll address(this). 28 | scribe.kiss(address(this)); 29 | 30 | // Make handler auth'ed. 31 | scribe.rely(address(handler)); 32 | 33 | // Finish handler initialization. 34 | // Needs to be done after handler is auth'ed. 35 | handler.init(scribe_); 36 | 37 | // Set handler as target contract. 38 | targetSelector( 39 | FuzzSelector({addr: address(handler), selectors: _targetSelectors()}) 40 | ); 41 | targetContract(address(handler)); 42 | } 43 | 44 | function _targetSelectors() internal virtual returns (bytes4[] memory) { 45 | bytes4[] memory selectors = new bytes4[](5); 46 | selectors[0] = ScribeHandler.warp.selector; 47 | selectors[1] = ScribeHandler.poke.selector; 48 | selectors[2] = ScribeHandler.setBar.selector; 49 | selectors[3] = ScribeHandler.lift.selector; 50 | selectors[4] = ScribeHandler.drop.selector; 51 | 52 | return selectors; 53 | } 54 | 55 | // -- Poke -- 56 | 57 | function invariant_poke_PokeTimestampsAreStrictlyMonotonicallyIncreasing() 58 | public 59 | { 60 | // Get scribe's pokeData before execution. 61 | IScribe.PokeData memory beforePokeData = handler.scribe_lastPokeData(); 62 | 63 | // Get scribe's current pokeData. 64 | IScribe.PokeData memory currentPokeData; 65 | currentPokeData = scribe.inspectable_pokeData(); 66 | 67 | assertTrue(beforePokeData.age <= currentPokeData.age); 68 | } 69 | 70 | // -- PubKeys -- 71 | 72 | function invariant_pubKeys_IndexedViaFeedId() public { 73 | LibSecp256k1.Point memory pubKey; 74 | uint8 feedId; 75 | 76 | for (uint i; i < 256; i++) { 77 | pubKey = scribe.inspectable_pubKeys(uint8(i)); 78 | feedId = uint8(uint(uint160(pubKey.toAddress())) >> 152); 79 | 80 | assertTrue(pubKey.isZeroPoint() || i == feedId); 81 | } 82 | } 83 | 84 | function invariant_pubKeys_CannotIndexOutOfBoundsViaUint8Index() 85 | public 86 | view 87 | { 88 | for (uint i; i <= type(uint8).max; i++) { 89 | // Should not revert. 90 | scribe.inspectable_pubKeys(uint8(i)); 91 | } 92 | } 93 | 94 | // -- Bar -- 95 | 96 | function invariant_bar_IsNeverZero() public { 97 | assertTrue(scribe.bar() != 0); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /docs/Deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | This document describes how to deploy `Scribe` and `ScribeOptimistic` instances. 4 | 5 | ## Environment Variables 6 | 7 | The following environment variables must be set: 8 | 9 | - `RPC_URL`: The RPC URL of an EVM node 10 | - `KEYSTORE`: The path to the keystore file containing the encrypted private key 11 | - Note that password can either be entered on request or set via the `KEYSTORE_PASSWORD` environment variable 12 | - `KEYSTORE_PASSWORD`: The password for the keystore file 13 | - `ETHERSCAN_API_URL`: The Etherscan API URL for the Etherscan's chain instance 14 | - Note that the API endpoint varies per Etherscan chain instance 15 | - Note to point to actual API endpoint (e.g. `/api`) and not just host 16 | - `ETHERSCAN_API_KEY`: The Etherscan API key for the Etherscan's chain instance 17 | - `SCRIBE_FLAVOUR`: The `Scribe` flavour to deploy 18 | - Note that value must be either `Scribe` or `ScribeOptimistic` 19 | - `INITIAL_AUTHED`: The address being auth'ed on the newly deployed `Scribe` instance 20 | - `WAT`: The wat for `Scribe` 21 | - Note to use the wat's string representation 22 | - Note that the wat must not exceed 32 bytes in length 23 | 24 | Note that an `.env.example` file is provided in the project root. To set all environment variables at once, create a copy of the file and rename the copy to `.env`, adjust the variables' values, and run `source .env`. 25 | 26 | To easily check the environment variables, run: 27 | 28 | ```bash 29 | $ env | grep -e "RPC_URL" -e "KEYSTORE" -e "KEYSTORE_PASSWORD" -e "ETHERSCAN_API_URL" -e "ETHERSCAN_API_KEY" -e "SCRIBE_FLAVOUR" -e "INITIAL_AUTHED" -e "WAT" 30 | ``` 31 | 32 | ## Code Adjustments 33 | 34 | Two code adjustments are necessary to give each deployed contract instance a unique name: 35 | 36 | 1. Adjust the `Chronicle_BASE_QUOTE_COUNTER`'s name in `src/${SCRIBE_FLAVOUR}.sol` and remove the `@todo` comment 37 | 2. Adjust the import of the `Chronicle_BASE_QUOTE_COUNTER` in `script/${SCRIBE_FLAVOUR}.s.sol` and remove the `@todo` comment 38 | 39 | ## Execution 40 | 41 | The deployment process consists of two steps - the actual deployment and the subsequent Etherscan verification. 42 | 43 | Deployment: 44 | 45 | ```bash 46 | $ WAT_BYTES32=$(cast format-bytes32-string $WAT) && \ 47 | forge script \ 48 | --keystore "$KEYSTORE" \ 49 | --password "$KEYSTORE_PASSWORD" \ 50 | --sender "$INITIAL_AUTHED" \ 51 | --broadcast \ 52 | --rpc-url "$RPC_URL" \ 53 | --sig "$(cast calldata "deploy(address,bytes32)" "$INITIAL_AUTHED" "$WAT_BYTES32")" \ 54 | -vvv \ 55 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 56 | ``` 57 | 58 | In the case of zksync add this final flag: 59 | 60 | ``` 61 | --zksync 62 | ``` 63 | 64 | The deployment command will log the address of the newly deployed contract address. Store this address in the `$SCRIBE` environment variable and continue with the verification. 65 | 66 | Verification: 67 | 68 | ```bash 69 | $ WAT_BYTES32=$(cast format-bytes32-string $WAT) && \ 70 | forge verify-contract \ 71 | "$SCRIBE" \ 72 | --verifier-url "$ETHERSCAN_API_URL" \ 73 | --etherscan-api-key "$ETHERSCAN_API_KEY" \ 74 | --watch \ 75 | --constructor-args $(cast abi-encode "constructor(address,bytes32)" "$INITIAL_AUTHED" "$WAT_BYTES32") \ 76 | src/${SCRIBE_FLAVOUR}.sol:${SCRIBE_FLAVOUR}_1 77 | ``` 78 | 79 | 80 | In the case of zksync add these final two flags: 81 | 82 | ``` 83 | --zksync --evm-version london 84 | ``` 85 | -------------------------------------------------------------------------------- /script/dev/gas-estimator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script to estimate gas usage of an (op)Poke using an RPC connected client. 4 | # 5 | # Run via: 6 | # ```bash 7 | # $ script/dev/gas-estimator \ 8 | # --scribe
\ 9 | # --op-poke \ # Set to use opPoke, omit for normal poke 10 | # --poke-val \ 11 | # --poke-age \ 12 | # --schnorr-signature \ 13 | # --schnorr-commitment
\ 14 | # --schnorr-feed-ids \ 15 | # --ecdsa-v \ # ECDSA args only necessary if opPoke 16 | # --ecdsa-r \ # ECDSA args only necessary if opPoke 17 | # --ecdsa-s \ # ECDSA args only necessary if opPoke 18 | # --rpc-url 19 | # ``` 20 | 21 | # Scribe argument 22 | scribe="" 23 | 24 | # (op)Poke arguments 25 | op_poke=false 26 | ## Poke data 27 | poke_val="" 28 | poke_age="" 29 | ## Schnorr data 30 | schnorr_signature="" 31 | schnorr_commitment="" 32 | schnorr_feed_ids="" 33 | ## ECDSA data 34 | ecdsa_v="" 35 | ecdsa_r="" 36 | ecdsa_s="" 37 | 38 | # Other arguments 39 | rpc_url="" 40 | 41 | # Parse arguments: 42 | while [[ $# -gt 0 ]]; do 43 | case "$1" in 44 | --scribe) 45 | scribe="$2" 46 | shift 2 47 | ;; 48 | --op-poke) 49 | op_poke=true 50 | shift 51 | ;; 52 | --poke-val) 53 | poke_val="$2" 54 | shift 2 55 | ;; 56 | --poke-age) 57 | poke_age="$2" 58 | shift 2 59 | ;; 60 | --schnorr-signature) 61 | schnorr_signature="$2" 62 | shift 2 63 | ;; 64 | --schnorr-commitment) 65 | schnorr_commitment="$2" 66 | shift 2 67 | ;; 68 | --schnorr-feed-ids) 69 | schnorr_feed_ids="$2" 70 | shift 2 71 | ;; 72 | --ecdsa-v) 73 | ecdsa_v="$2" 74 | shift 2 75 | ;; 76 | --ecdsa-r) 77 | ecdsa_r="$2" 78 | shift 2 79 | ;; 80 | --ecdsa-s) 81 | ecdsa_s="$2" 82 | shift 2 83 | ;; 84 | --rpc-url) 85 | rpc_url="$2" 86 | shift 2 87 | ;; 88 | *) 89 | echo "Unknown option: $1" 90 | exit 1 91 | ;; 92 | esac 93 | done 94 | 95 | # Create calldata. 96 | calldata="" 97 | if [[ -n $op_poke ]]; then 98 | calldata=$(cast calldata "opPoke((uint128,uint32),(bytes32,address,bytes),(uint8,bytes32,bytes32))" \ 99 | "($poke_val,$poke_age)" \ 100 | "($schnorr_signature,$schnorr_commitment,$schnorr_feed_ids)" \ 101 | "($ecdsa_v,$ecdsa_r,$ecdsa_s)" \ 102 | ) 103 | else 104 | calldata=$(cast calldata "poke((uint128,uint32),(bytes32,address,bytes))" \ 105 | "($poke_val,$poke_age)" \ 106 | "($schnorr_signature,$schnorr_commitment,$schnorr_feed_ids)" 107 | ) 108 | fi 109 | 110 | if [[ $calldata == "" ]]; then 111 | echo -e "Error creating calldata" 112 | exit 1 113 | fi 114 | 115 | # Use `cast estimate` on given RPC to estimate gas usage. 116 | result=$(cast estimate $scribe $calldata --rpc-url $rpc_url) 117 | echo -e "$result" 118 | -------------------------------------------------------------------------------- /script/libs/LibFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.16; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | 6 | import {IScribe} from "src/IScribe.sol"; 7 | 8 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 9 | 10 | import {LibSchnorrExtended} from "./LibSchnorrExtended.sol"; 11 | import {LibSecp256k1Extended} from "./LibSecp256k1Extended.sol"; 12 | 13 | /** 14 | * @title LibFeed 15 | * 16 | * @notice Solidity library for feeds 17 | */ 18 | library LibFeed { 19 | using LibSchnorrExtended for LibSecp256k1.Point; 20 | using LibSchnorrExtended for uint; 21 | using LibSchnorrExtended for uint[]; 22 | using LibSecp256k1Extended for uint; 23 | using LibSecp256k1 for LibSecp256k1.Point; 24 | using LibFeed for Feed; 25 | using LibFeed for Feed[]; 26 | 27 | Vm internal constant vm = 28 | Vm(address(uint160(uint(keccak256("hevm cheat code"))))); 29 | 30 | /// @dev Feed encapsulates a private key, derived public key, and the 31 | /// corresponding feed id. 32 | struct Feed { 33 | uint privKey; 34 | LibSecp256k1.Point pubKey; 35 | uint8 id; 36 | } 37 | 38 | /// @dev Returns a new feed instance with private key `privKey`. 39 | function newFeed(uint privKey) internal returns (Feed memory) { 40 | LibSecp256k1.Point memory pubKey = privKey.derivePublicKey(); 41 | 42 | return Feed({ 43 | privKey: privKey, 44 | pubKey: pubKey, 45 | id: uint8(uint(uint160(pubKey.toAddress())) >> 152) 46 | }); 47 | } 48 | 49 | /// @dev Returns a ECDSA signature of type IScribe.ECDSAData 50 | /// signing `message` via `self`'s private key. 51 | function signECDSA(Feed memory self, bytes32 message) 52 | internal 53 | pure 54 | returns (IScribe.ECDSAData memory) 55 | { 56 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(self.privKey, message); 57 | 58 | return IScribe.ECDSAData(v, r, s); 59 | } 60 | 61 | /// @dev Returns a Schnorr signature of type IScribe.SchnorrData 62 | /// signing `message` via `self`'s private key. 63 | function signSchnorr(Feed memory self, bytes32 message) 64 | internal 65 | returns (IScribe.SchnorrData memory) 66 | { 67 | (uint signature, address commitment) = self.privKey.signMessage(message); 68 | 69 | return IScribe.SchnorrData({ 70 | signature: bytes32(signature), 71 | commitment: commitment, 72 | feedIds: abi.encodePacked(self.id) 73 | }); 74 | } 75 | 76 | /// @dev Returns a Schnorr multi-signature (aggregated signature) of type 77 | /// IScribe.SchnorrData signing `message` via `selfs`' private keys. 78 | function signSchnorr(Feed[] memory selfs, bytes32 message) 79 | internal 80 | returns (IScribe.SchnorrData memory) 81 | { 82 | // Create multi-signature. 83 | uint[] memory privKeys = new uint[](selfs.length); 84 | for (uint i; i < selfs.length; i++) { 85 | privKeys[i] = selfs[i].privKey; 86 | } 87 | (uint signature, address commitment) = privKeys.signMessage(message); 88 | 89 | // Create blob of feedIds. 90 | bytes memory feedIds; 91 | for (uint i; i < selfs.length; i++) { 92 | feedIds = abi.encodePacked(feedIds, selfs[i].id); 93 | } 94 | 95 | return IScribe.SchnorrData({ 96 | signature: bytes32(signature), 97 | commitment: commitment, 98 | feedIds: feedIds 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/libs/LibSchnorr.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {LibSecp256k1} from "./LibSecp256k1.sol"; 5 | 6 | /** 7 | * @title LibSchnorr 8 | * 9 | * @notice Custom-purpose library for Schnorr signature verification on the 10 | * secp256k1 curve 11 | */ 12 | library LibSchnorr { 13 | using LibSecp256k1 for LibSecp256k1.Point; 14 | 15 | /// @dev Returns whether `signature` and `commitment` sign via `pubKey` 16 | /// message `message`. 17 | /// 18 | /// @custom:invariant Reverts iff out of gas. 19 | /// @custom:invariant Uses constant amount of gas. 20 | function verifySignature( 21 | LibSecp256k1.Point memory pubKey, 22 | bytes32 message, 23 | bytes32 signature, 24 | address commitment 25 | ) internal pure returns (bool) { 26 | // Return false if signature or commitment is zero. 27 | if (signature == 0 || commitment == address(0)) { 28 | return false; 29 | } 30 | 31 | // Note to enforce pubKey is valid secp256k1 point. 32 | // 33 | // While the Scribe contract ensures to only verify signatures for valid 34 | // public keys, this check is enabled as an additional defense 35 | // mechanism. 36 | if (!pubKey.isOnCurve()) { 37 | return false; 38 | } 39 | 40 | // Note to enforce signature is less than Q to prevent signature 41 | // malleability. 42 | // 43 | // While the Scribe contract only accepts messages with strictly 44 | // monotonically increasing timestamps, circumventing replay attack 45 | // vectors and therefore also signature malleability issues at a higher 46 | // level, this check is enabled as an additional defense mechanism. 47 | if (uint(signature) >= LibSecp256k1.Q()) { 48 | return false; 49 | } 50 | 51 | // Construct challenge = H(Pₓ ‖ Pₚ ‖ m ‖ Rₑ) mod Q 52 | uint challenge = uint( 53 | keccak256( 54 | abi.encodePacked( 55 | pubKey.x, uint8(pubKey.yParity()), message, commitment 56 | ) 57 | ) 58 | ) % LibSecp256k1.Q(); 59 | 60 | // Compute msgHash = -sig * Pₓ (mod Q) 61 | // = Q - (sig * Pₓ) (mod Q) 62 | // 63 | // Unchecked because the only protected operation performed is the 64 | // subtraction from Q where the subtrahend is the result of a (mod Q) 65 | // computation, i.e. the subtrahend is guaranteed to be less than Q. 66 | uint msgHash; 67 | unchecked { 68 | msgHash = LibSecp256k1.Q() 69 | - mulmod(uint(signature), pubKey.x, LibSecp256k1.Q()); 70 | } 71 | 72 | // Compute v = Pₚ + 27 73 | // 74 | // Unchecked because pubKey.yParity() ∊ {0, 1} which cannot overflow 75 | // by adding 27. 76 | uint v; 77 | unchecked { 78 | v = pubKey.yParity() + 27; 79 | } 80 | 81 | // Set r = Pₓ 82 | uint r = pubKey.x; 83 | 84 | // Compute s = Q - (e * Pₓ) (mod Q) 85 | // 86 | // Unchecked because the only protected operation performed is the 87 | // subtraction from Q where the subtrahend is the result of a (mod Q) 88 | // computation, i.e. the subtrahend is guaranteed to be less than Q. 89 | uint s; 90 | unchecked { 91 | s = LibSecp256k1.Q() - mulmod(challenge, pubKey.x, LibSecp256k1.Q()); 92 | } 93 | 94 | // Compute ([s]G - [e]P)ₑ via ecrecover. 95 | address recovered = 96 | ecrecover(bytes32(msgHash), uint8(v), bytes32(r), bytes32(s)); 97 | 98 | // Verification succeeds iff ([s]G - [e]P)ₑ = Rₑ. 99 | // 100 | // Note that commitment is guaranteed to not be zero. 101 | return commitment == recovered; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@noble/curves@^0.8.2": 6 | version "0.8.3" 7 | resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-0.8.3.tgz#ad6d48baf2599cf1d58dcb734c14d5225c8996e0" 8 | integrity sha512-OqaOf4RWDaCRuBKJLDURrgVxjLmneGsiCXGuzYB5y95YithZMA6w4uk34DHSm0rKMrrYiaeZj48/81EvaAScLQ== 9 | dependencies: 10 | "@noble/hashes" "1.3.0" 11 | 12 | "@noble/hashes@1.3.0", "@noble/hashes@^1.1.2": 13 | version "1.3.0" 14 | resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" 15 | integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== 16 | 17 | "@noble/secp256k1@^1.7.1": 18 | version "1.7.1" 19 | resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" 20 | integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== 21 | 22 | "@wagmi/chains@~0.2.11": 23 | version "0.2.20" 24 | resolved "https://registry.yarnpkg.com/@wagmi/chains/-/chains-0.2.20.tgz#f8370d3266a86c2fdc8e3b9fb8d5d960bc89f30f" 25 | integrity sha512-VdsyZrukVkDbQQBAfQel/Ro7EMBzlO/xYMSblad+v+RMA0CbXYtSx0obiZhfVmK+8IR/5XeSkvv6gC95UReFjA== 26 | 27 | abitype@~0.7.1: 28 | version "0.7.1" 29 | resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.7.1.tgz#16db20abe67de80f6183cf75f3de1ff86453b745" 30 | integrity sha512-VBkRHTDZf9Myaek/dO3yMmOzB/y2s3Zo6nVU7yaw1G+TvCHAjwaJzNGN9yo4K5D8bU/VZXKP1EJpRhFr862PlQ== 31 | 32 | bcrypto@^5.4.0: 33 | version "5.4.0" 34 | resolved "https://registry.yarnpkg.com/bcrypto/-/bcrypto-5.4.0.tgz#4046f0c44a4b301eff84de593b4f86fce8d91db2" 35 | integrity sha512-KDX2CR29o6ZoqpQndcCxFZAtYA1jDMnXU3jmCfzP44g++Cu7AHHtZN/JbrN/MXAg9SLvtQ8XISG+eVD9zH1+Jg== 36 | dependencies: 37 | bufio "~1.0.7" 38 | loady "~0.0.5" 39 | 40 | bufio@~1.0.7: 41 | version "1.0.7" 42 | resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.0.7.tgz#b7f63a1369a0829ed64cc14edf0573b3e382a33e" 43 | integrity sha512-bd1dDQhiC+bEbEfg56IdBv7faWa6OipMs/AFFFvtFnB3wAYjlwQpQRZ0pm6ZkgtfL0pILRXhKxOiQj6UzoMR7A== 44 | 45 | idna-uts46-hx@^4.1.2: 46 | version "4.1.2" 47 | resolved "https://registry.yarnpkg.com/idna-uts46-hx/-/idna-uts46-hx-4.1.2.tgz#b7ecf6b603abf1d81972d0a9a1335d7bc1eda553" 48 | integrity sha512-EAB3egrcalcTQHcjA7yzXXkE4E09TIFerR//4yUYGYCeCfXmkU0LgsGJgYSIQA1lQunfsn4ZCWJhbelgl3cdiQ== 49 | dependencies: 50 | punycode "^2.1.1" 51 | 52 | isomorphic-ws@^5.0.0: 53 | version "5.0.0" 54 | resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" 55 | integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== 56 | 57 | loady@~0.0.5: 58 | version "0.0.5" 59 | resolved "https://registry.yarnpkg.com/loady/-/loady-0.0.5.tgz#b17adb52d2fb7e743f107b0928ba0b591da5d881" 60 | integrity sha512-uxKD2HIj042/HBx77NBcmEPsD+hxCgAtjEWlYNScuUjIsh/62Uyu39GOR68TBR68v+jqDL9zfftCWoUo4y03sQ== 61 | 62 | punycode@^2.1.1: 63 | version "2.3.0" 64 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" 65 | integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== 66 | 67 | viem@^0.1.16: 68 | version "0.1.26" 69 | resolved "https://registry.yarnpkg.com/viem/-/viem-0.1.26.tgz#2ce12547c7137025795e3bcaeb99f33eafebe1bf" 70 | integrity sha512-6oSGhDtgb64hjhBxOu5TPy9WT5rMww2iFuDASIOAB6kHJQ2NIVJabrnA/BQoBXbv3SJyyX/622enh5YX8MYTYg== 71 | dependencies: 72 | "@noble/hashes" "^1.1.2" 73 | "@noble/secp256k1" "^1.7.1" 74 | "@wagmi/chains" "~0.2.11" 75 | abitype "~0.7.1" 76 | idna-uts46-hx "^4.1.2" 77 | isomorphic-ws "^5.0.0" 78 | ws "^8.12.0" 79 | 80 | ws@^8.12.0: 81 | version "8.13.0" 82 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" 83 | integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== 84 | -------------------------------------------------------------------------------- /script/libs/LibOracleSuite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.16; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | 6 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 7 | 8 | /** 9 | * @title LibOracleSuite 10 | * 11 | * @notice Wrapper library for oracle-suite's `schnorr` cli tool 12 | * 13 | * @dev Expects `schnorr` binary to be in the `bin/` directory. 14 | * 15 | * For more info, see https://github.com/chronicleprotocol/oracle-suite. 16 | */ 17 | library LibOracleSuite { 18 | Vm private constant vm = 19 | Vm(address(uint160(uint(keccak256("hevm cheat code"))))); 20 | 21 | /// @dev Signs message `message` via set of private keys `privKeys`. 22 | /// 23 | /// Signed via: 24 | /// ```bash 25 | /// $ ./bin/schnorr sign 26 | /// ``` 27 | function sign(uint[] memory privKeys, bytes32 message) 28 | internal 29 | returns (uint, address) 30 | { 31 | string[] memory inputs = new string[](3 + privKeys.length); 32 | inputs[0] = "bin/schnorr"; 33 | inputs[1] = "sign"; 34 | inputs[2] = vm.toString(message); 35 | for (uint i; i < privKeys.length; i++) { 36 | inputs[3 + i] = vm.toString(bytes32(privKeys[i])); 37 | } 38 | 39 | uint[2] memory result = abi.decode(vm.ffi(inputs), (uint[2])); 40 | 41 | uint signature = result[0]; 42 | address commitment = address(uint160(result[1])); 43 | 44 | return (signature, commitment); 45 | } 46 | 47 | /// @dev Verifies public key `pubKey` signs via `signature` and `commitment` 48 | /// message `message`. 49 | /// 50 | /// Verified via: 51 | /// ```bash 52 | /// $ ./bin/schnorr verify \ 53 | /// \ 54 | /// \ 55 | /// \ 56 | /// \ 57 | /// 58 | /// ``` 59 | function verify( 60 | LibSecp256k1.Point memory pubKey, 61 | bytes32 message, 62 | bytes32 signature, 63 | address commitment 64 | ) internal returns (bool) { 65 | string[] memory inputs = new string[](7); 66 | inputs[0] = "bin/schnorr"; 67 | inputs[1] = "verify"; 68 | inputs[2] = vm.toString(message); 69 | inputs[3] = vm.toString(pubKey.x); 70 | inputs[4] = vm.toString(pubKey.y); 71 | inputs[5] = vm.toString(signature); 72 | inputs[6] = vm.toString(commitment); 73 | 74 | uint result = abi.decode(vm.ffi(inputs), (uint)); 75 | 76 | return result == 1; 77 | } 78 | 79 | /// @dev Constructs poke message for `wat` with value `val` and age `age`. 80 | /// 81 | /// Constructed via: 82 | /// ```bash 83 | /// $ ./bin/schnorr construct-poke-message 84 | /// ``` 85 | function constructPokeMessage(bytes32 wat, uint128 val, uint32 age) 86 | internal 87 | returns (bytes32) 88 | { 89 | string[] memory inputs = new string[](5); 90 | inputs[0] = "bin/schnorr"; 91 | inputs[1] = "construct-poke-message"; 92 | inputs[2] = _bytes32ToString(wat); 93 | inputs[3] = vm.toString(val); 94 | inputs[4] = vm.toString(age); 95 | 96 | return abi.decode(vm.ffi(inputs), (bytes32)); 97 | } 98 | 99 | // -- Private Helpers -- 100 | 101 | // Copied from https://ethereum.stackexchange.com/a/59335/114758. 102 | function _bytes32ToString(bytes32 _bytes32) 103 | private 104 | pure 105 | returns (string memory) 106 | { 107 | uint8 i = 0; 108 | while (i < 32 && _bytes32[i] != 0) { 109 | i++; 110 | } 111 | bytes memory bytesArray = new bytes(i); 112 | for (i = 0; i < 32 && _bytes32[i] != 0; i++) { 113 | bytesArray[i] = _bytes32[i]; 114 | } 115 | return string(bytesArray); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /script/rescue/Rescuer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {IAuth} from "chronicle-std/auth/IAuth.sol"; 5 | import {Auth} from "chronicle-std/auth/Auth.sol"; 6 | 7 | import {IScribe} from "src/IScribe.sol"; 8 | import {IScribeOptimistic} from "src/IScribeOptimistic.sol"; 9 | 10 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 11 | 12 | /** 13 | * @title Rescuer 14 | * 15 | * @notice Contract to recover ETH from offboarded ScribeOptimistic instances 16 | * 17 | * @dev Deployment: 18 | * ```bash 19 | * $ forge create script/rescue/Rescuer.sol:Rescuer \ 20 | * --constructor-args $INITIAL_AUTHED \ 21 | * --keystore $KEYSTORE \ 22 | * --password $KEYSTORE_PASSWORD \ 23 | * --rpc-url $RPC_URL \ 24 | * --verifier-url $ETHERSCAN_API_URL \ 25 | * --etherscan-api-key $ETHERSCAN_API_KEY 26 | * ``` 27 | * 28 | * @author Chronicle Labs, Inc 29 | * @custom:security-contact security@chroniclelabs.org 30 | */ 31 | contract Rescuer is Auth { 32 | using LibSecp256k1 for LibSecp256k1.Point; 33 | 34 | /// @notice Emitted when successfully recovered ETH funds. 35 | /// @param caller The caller's address. 36 | /// @param opScribe The ScribeOptimistic instance the ETH got recovered 37 | /// from. 38 | /// @param amount The amount of ETH recovered. 39 | event Recovered( 40 | address indexed caller, address indexed opScribe, uint amount 41 | ); 42 | 43 | /// @notice Emitted when successfully withdrawed ETH from this contract. 44 | /// @param caller The caller's address. 45 | /// @param receiver The receiver 46 | /// from. 47 | /// @param amount The amount of ETH recovered. 48 | event Withdrawed( 49 | address indexed caller, address indexed receiver, uint amount 50 | ); 51 | 52 | constructor(address initialAuthed) Auth(initialAuthed) {} 53 | 54 | receive() external payable {} 55 | 56 | /// @notice Withdraws `amount` ETH held in contract to `receiver`. 57 | /// 58 | /// @dev Only callable by auth'ed address. 59 | function withdraw(address payable receiver, uint amount) external auth { 60 | (bool ok,) = receiver.call{value: amount}(""); 61 | require(ok); 62 | 63 | emit Withdrawed(msg.sender, receiver, amount); 64 | } 65 | 66 | /// @notice Rescues ETH from ScribeOptimistic instance `opScribe`. 67 | /// 68 | /// @dev Note that `opScribe` MUST be deactivated. 69 | /// @dev Note that validator key pair SHALL be only used once and generated 70 | /// via a CSPRNG. 71 | /// 72 | /// @dev Only callable by auth'ed address. 73 | function suck( 74 | address opScribe, 75 | LibSecp256k1.Point memory pubKey, 76 | IScribe.ECDSAData memory registrationSig, 77 | uint32 pokeDataAge, 78 | IScribe.ECDSAData memory opPokeSig 79 | ) external auth { 80 | require(IAuth(opScribe).authed(address(this))); 81 | 82 | uint balanceBefore = address(this).balance; 83 | 84 | // Fail if instance has feeds lifted, ie is not deactivated. 85 | require(IScribe(opScribe).feeds().length == 0); 86 | 87 | // Construct pokeData. 88 | IScribe.PokeData memory pokeData = 89 | IScribe.PokeData({val: uint128(0), age: pokeDataAge}); 90 | 91 | // Construct invalid Schnorr signature. 92 | IScribe.SchnorrData memory schnorrSig = IScribe.SchnorrData({ 93 | signature: bytes32(0), 94 | commitment: address(0), 95 | feedIds: hex"" 96 | }); 97 | 98 | // Lift validator. 99 | IScribe(opScribe).lift(pubKey, registrationSig); 100 | 101 | // Perform opPoke. 102 | IScribeOptimistic(opScribe).opPoke(pokeData, schnorrSig, opPokeSig); 103 | 104 | // Perform opChallenge. 105 | IScribeOptimistic(opScribe).opChallenge(schnorrSig); 106 | 107 | // Compute amount of ETH received as challenge reward. 108 | uint amount = address(this).balance - balanceBefore; 109 | 110 | // Emit event. 111 | emit Recovered(msg.sender, opScribe, amount); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. 4 | "Business Source License" is a trademark of MariaDB Corporation Ab. 5 | 6 | ----------------------------------------------------------------------------- 7 | 8 | Parameters 9 | 10 | Licensor: Chronicle Association 11 | 12 | Licensed Work: Scribe 13 | The Licensed Work is (c) 2023 Chronicle Association 14 | 15 | Additional Use Grant: Any uses listed and defined at 16 | scribe-license-grants.chronicleassociation.eth 17 | 18 | Change Date: The earlier of 2026-09-12 or a date specified at 19 | scribe-license-date.chronicleassociation.eth 20 | 21 | Change License: MIT 22 | 23 | ----------------------------------------------------------------------------- 24 | 25 | Terms 26 | 27 | The Licensor hereby grants you the right to copy, modify, create derivative 28 | works, redistribute, and make non-production use of the Licensed Work. The 29 | Licensor may make an Additional Use Grant, above, permitting limited 30 | production use. 31 | 32 | Effective on the Change Date, or the fourth anniversary of the first publicly 33 | available distribution of a specific version of the Licensed Work under this 34 | License, whichever comes first, the Licensor hereby grants you rights under 35 | the terms of the Change License, and the rights granted in the paragraph 36 | above terminate. 37 | 38 | If your use of the Licensed Work does not comply with the requirements 39 | currently in effect as described in this License, you must purchase a 40 | commercial license from the Licensor, its affiliated entities, or authorized 41 | resellers, or you must refrain from using the Licensed Work. 42 | 43 | All copies of the original and modified Licensed Work, and derivative works 44 | of the Licensed Work, are subject to this License. This License applies 45 | separately for each version of the Licensed Work and the Change Date may vary 46 | for each version of the Licensed Work released by Licensor. 47 | 48 | You must conspicuously display this License on each original or modified copy 49 | of the Licensed Work. If you receive the Licensed Work in original or 50 | modified form from a third party, the terms and conditions set forth in this 51 | License apply to your use of that work. 52 | 53 | Any use of the Licensed Work in violation of this License will automatically 54 | terminate your rights under this License for the current and all other 55 | versions of the Licensed Work. 56 | 57 | This License does not grant you any right in any trademark or logo of 58 | Licensor or its affiliates (provided that you may use a trademark or logo of 59 | Licensor as expressly required by this License). 60 | 61 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 62 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 63 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 64 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 65 | TITLE. 66 | 67 | MariaDB hereby grants you permission to use this License’s text to license 68 | your works, and to refer to it using the trademark "Business Source License", 69 | as long as you comply with the Covenants of Licensor below. 70 | 71 | ----------------------------------------------------------------------------- 72 | 73 | Covenants of Licensor 74 | 75 | In consideration of the right to use this License’s text and the "Business 76 | Source License" name and trademark, Licensor covenants to MariaDB, and to all 77 | other recipients of the licensed work to be provided by Licensor: 78 | 79 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 80 | or a license that is compatible with GPL Version 2.0 or a later version, 81 | where "compatible" means that software provided under the Change License can 82 | be included in a program with software provided under GPL Version 2.0 or a 83 | later version. Licensor may specify additional Change Licenses without 84 | limitation. 85 | 86 | 2. To either: (a) specify an additional grant of rights to use that does not 87 | impose any additional restriction on the right granted in this License, as 88 | the Additional Use Grant; or (b) insert the text "None". 89 | 90 | 3. To specify a Change Date. 91 | 92 | 4. Not to modify this License in any other way. 93 | 94 | ----------------------------------------------------------------------------- 95 | 96 | Notice 97 | 98 | The Business Source License (this document, or the "License") is not an Open 99 | Source license. However, the Licensed Work will eventually be made available 100 | under an Open Source License, as stated in this License. 101 | -------------------------------------------------------------------------------- /.gas-snapshot: -------------------------------------------------------------------------------- 1 | LibSecp256k1Test:testVectors_addAffinePoint() (gas: 2502307) 2 | LibSecp256k1Test:test_isZeroPoint() (gas: 465) 3 | LibSecp256k1Test:test_yParity() (gas: 530) 4 | ScribeInvariantTest:invariant_bar_IsNeverZero() (runs: 256, calls: 3840, reverts: 0) 5 | ScribeInvariantTest:invariant_poke_PokeTimestampsAreStrictlyMonotonicallyIncreasing() (runs: 256, calls: 3840, reverts: 0) 6 | ScribeInvariantTest:invariant_pubKeys_CannotIndexOutOfBoundsViaUint8Index() (runs: 256, calls: 3840, reverts: 0) 7 | ScribeInvariantTest:invariant_pubKeys_IndexedViaFeedId() (runs: 256, calls: 3840, reverts: 0) 8 | ScribeOptimisticTest:test_Deployment() (gas: 1204337) 9 | ScribeOptimisticTest:test_afterAuthedAction_1_drop() (gas: 202463) 10 | ScribeOptimisticTest:test_afterAuthedAction_1_setBar() (gas: 238545) 11 | ScribeOptimisticTest:test_afterAuthedAction_1_setChallengePeriod() (gas: 237041) 12 | ScribeOptimisticTest:test_afterAuthedAction_2_drop() (gas: 285432) 13 | ScribeOptimisticTest:test_afterAuthedAction_2_setBar() (gas: 325402) 14 | ScribeOptimisticTest:test_afterAuthedAction_2_setChallengePeriod() (gas: 324955) 15 | ScribeOptimisticTest:test_afterAuthedAction_3_drop() (gas: 205138) 16 | ScribeOptimisticTest:test_afterAuthedAction_3_setBar() (gas: 241782) 17 | ScribeOptimisticTest:test_afterAuthedAction_3_setChallengePeriod() (gas: 241184) 18 | ScribeOptimisticTest:test_afterAuthedAction_4_drop() (gas: 286761) 19 | ScribeOptimisticTest:test_afterAuthedAction_4_setBar() (gas: 326467) 20 | ScribeOptimisticTest:test_afterAuthedAction_4_setChallengePeriod() (gas: 325228) 21 | ScribeOptimisticTest:test_drop_Multiple_IsAuthProtected() (gas: 14650) 22 | ScribeOptimisticTest:test_drop_Single_IsAuthProtected() (gas: 13533) 23 | ScribeOptimisticTest:test_latestAnswer_isTollProtected() (gas: 12518) 24 | ScribeOptimisticTest:test_latestRoundData_isTollProtected() (gas: 14030) 25 | ScribeOptimisticTest:test_lift_Multiple_FailsIf_ECDSADataInvalid() (gas: 78843) 26 | ScribeOptimisticTest:test_lift_Multiple_IsAuthProtected() (gas: 16509) 27 | ScribeOptimisticTest:test_lift_Single_FailsIf_ECDSADataInvalid() (gas: 21864) 28 | ScribeOptimisticTest:test_lift_Single_FailsIf_FeedIdAlreadyLifted() (gas: 73859) 29 | ScribeOptimisticTest:test_lift_Single_IsAuthProtected() (gas: 13053) 30 | ScribeOptimisticTest:test_opChallenge_CalldataEncodingAttack() (gas: 1528876) 31 | ScribeOptimisticTest:test_opChallenge_FailsIf_CalledSubsequently() (gas: 256890) 32 | ScribeOptimisticTest:test_opChallenge_FailsIf_InvalidSchnorrDataGiven() (gas: 226291) 33 | ScribeOptimisticTest:test_opChallenge_FailsIf_NoOpPokeToChallenge() (gas: 14879) 34 | ScribeOptimisticTest:test_peek_isTollProtected() (gas: 12886) 35 | ScribeOptimisticTest:test_peep_isTollProtected() (gas: 13216) 36 | ScribeOptimisticTest:test_poke_Initial_FailsIf_AgeIsZero() (gas: 185331) 37 | ScribeOptimisticTest:test_readWithAge_isTollProtected() (gas: 12397) 38 | ScribeOptimisticTest:test_read_isTollProtected() (gas: 11836) 39 | ScribeOptimisticTest:test_setBar_FailsIf_BarIsZero() (gas: 11908) 40 | ScribeOptimisticTest:test_setBar_IsAuthProtected() (gas: 13803) 41 | ScribeOptimisticTest:test_setMaxChallengeReward_IsAuthProtected() (gas: 13698) 42 | ScribeOptimisticTest:test_setOpChallengePeriod_DropsFinalizedOpPoke_If_NonFinalizedAfterUpdate() (gas: 242619) 43 | ScribeOptimisticTest:test_setOpChallengePeriod_FailsIf_OpChallengePeriodIsZero() (gas: 11633) 44 | ScribeOptimisticTest:test_setOpChallengePeriod_IsAuthProtected() (gas: 12393) 45 | ScribeOptimisticTest:test_toll_diss_IsAuthProtected() (gas: 13268) 46 | ScribeOptimisticTest:test_toll_kiss_IsAuthProtected() (gas: 12517) 47 | ScribeOptimisticTest:test_tryReadWithAge_isTollProtected() (gas: 13786) 48 | ScribeOptimisticTest:test_tryRead_isTollProtected() (gas: 13047) 49 | ScribeTest:test_Deployment() (gas: 1185225) 50 | ScribeTest:test_drop_Multiple_IsAuthProtected() (gas: 13643) 51 | ScribeTest:test_drop_Single_IsAuthProtected() (gas: 12648) 52 | ScribeTest:test_latestAnswer_isTollProtected() (gas: 12053) 53 | ScribeTest:test_latestRoundData_isTollProtected() (gas: 13080) 54 | ScribeTest:test_lift_Multiple_FailsIf_ECDSADataInvalid() (gas: 78456) 55 | ScribeTest:test_lift_Multiple_IsAuthProtected() (gas: 15556) 56 | ScribeTest:test_lift_Single_FailsIf_ECDSADataInvalid() (gas: 21117) 57 | ScribeTest:test_lift_Single_FailsIf_FeedIdAlreadyLifted() (gas: 73352) 58 | ScribeTest:test_lift_Single_IsAuthProtected() (gas: 12631) 59 | ScribeTest:test_peek_isTollProtected() (gas: 12337) 60 | ScribeTest:test_peep_isTollProtected() (gas: 12425) 61 | ScribeTest:test_poke_Initial_FailsIf_AgeIsZero() (gas: 181717) 62 | ScribeTest:test_readWithAge_isTollProtected() (gas: 11957) 63 | ScribeTest:test_read_isTollProtected() (gas: 11678) 64 | ScribeTest:test_setBar_FailsIf_BarIsZero() (gas: 11457) 65 | ScribeTest:test_setBar_IsAuthProtected() (gas: 12846) 66 | ScribeTest:test_toll_diss_IsAuthProtected() (gas: 12489) 67 | ScribeTest:test_toll_kiss_IsAuthProtected() (gas: 12156) 68 | ScribeTest:test_tryReadWithAge_isTollProtected() (gas: 12838) 69 | ScribeTest:test_tryRead_isTollProtected() (gas: 12298) -------------------------------------------------------------------------------- /script/dev/ScribeTester.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {console2} from "forge-std/console2.sol"; 6 | 7 | import {IScribe} from "src/IScribe.sol"; 8 | 9 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 10 | 11 | import {LibFeed} from "../libs/LibFeed.sol"; 12 | 13 | /** 14 | * @notice Scribe Tester Script 15 | * 16 | * @dev !!! IMPORTANT !!! 17 | * 18 | * This script may only be used for dev deployments! 19 | */ 20 | contract ScribeTesterScript is Script { 21 | using LibSecp256k1 for LibSecp256k1.Point; 22 | using LibFeed for LibFeed.Feed; 23 | using LibFeed for LibFeed.Feed[]; 24 | 25 | /// @dev Lifts set of private keys `privKeys` on `self`. 26 | /// 27 | /// @dev Call via: 28 | /// 29 | /// ```bash 30 | /// $ forge script \ 31 | /// --private-key $PRIVATE_KEY \ 32 | /// --broadcast \ 33 | /// --rpc-url $RPC_URL \ 34 | /// --sig $(cast calldata "lift(address,uint[])" $SCRIBE $TEST_FEED_PRIVATE_KEYS) \ 35 | /// -vvv \ 36 | /// script/dev/ScribeTester.s.sol:ScribeTesterScript 37 | /// ``` 38 | function lift(address self, uint[] memory privKeys) public { 39 | require(privKeys.length != 0, "No private keys given"); 40 | 41 | // Setup feeds. 42 | LibFeed.Feed[] memory feeds = new LibFeed.Feed[](privKeys.length); 43 | for (uint i; i < feeds.length; i++) { 44 | feeds[i] = LibFeed.newFeed({privKey: privKeys[i]}); 45 | 46 | vm.label( 47 | feeds[i].pubKey.toAddress(), 48 | string.concat("Feed #", vm.toString(i + 1)) 49 | ); 50 | } 51 | 52 | // Let feeds sign the feed registration message. 53 | IScribe.ECDSAData[] memory ecdsaDatas; 54 | ecdsaDatas = new IScribe.ECDSAData[](feeds.length); 55 | bytes32 message = IScribe(self).feedRegistrationMessage(); 56 | for (uint i; i < feeds.length; i++) { 57 | ecdsaDatas[i] = feeds[i].signECDSA(message); 58 | } 59 | 60 | // Create list of public keys. 61 | LibSecp256k1.Point[] memory pubKeys; 62 | pubKeys = new LibSecp256k1.Point[](feeds.length); 63 | for (uint i; i < pubKeys.length; i++) { 64 | pubKeys[i] = feeds[i].pubKey; 65 | } 66 | 67 | // Lift feeds. 68 | vm.startBroadcast(); 69 | IScribe(self).lift(pubKeys, ecdsaDatas); 70 | vm.stopBroadcast(); 71 | 72 | console2.log("Lifted feeds"); 73 | } 74 | 75 | /// @dev Pokes `self` with val `val` and current timestamp signed by set of 76 | /// private keys `privKeys`. 77 | /// 78 | /// @dev Call via: 79 | /// 80 | /// ```bash 81 | /// $ forge script \ 82 | /// --private-key $PRIVATE_KEY \ 83 | /// --broadcast \ 84 | /// --rpc-url $RPC_URL \ 85 | /// --sig $(cast calldata "poke(address,uint[],uint128)" $SCRIBE $TEST_FEED_SIGNERS_PRIVATE_KEYS $TEST_POKE_VAL) \ 86 | /// -vvv \ 87 | /// script/dev/ScribeTester.s.sol:ScribeTesterScript 88 | /// ``` 89 | function poke(address self, uint[] memory privKeys, uint128 val) public { 90 | require(privKeys.length != 0, "No private keys given"); 91 | 92 | // Setup feeds. 93 | LibFeed.Feed[] memory feeds = new LibFeed.Feed[](privKeys.length); 94 | for (uint i; i < feeds.length; i++) { 95 | feeds[i] = LibFeed.newFeed({privKey: privKeys[i]}); 96 | 97 | vm.label( 98 | feeds[i].pubKey.toAddress(), 99 | string.concat("Feed #", vm.toString(i + 1)) 100 | ); 101 | 102 | // Verify feed is lifted. 103 | bool isFeed = IScribe(self).feeds(feeds[i].pubKey.toAddress()); 104 | require( 105 | isFeed, 106 | string.concat( 107 | "Private key not feed, privKey=", vm.toString(privKeys[i]) 108 | ) 109 | ); 110 | } 111 | 112 | // Create poke data. 113 | IScribe.PokeData memory pokeData; 114 | pokeData.val = val; 115 | pokeData.age = uint32(block.timestamp); 116 | 117 | // Construct poke message. 118 | bytes32 message = IScribe(self).constructPokeMessage(pokeData); 119 | 120 | // Create Schnorr data proving poke message's integrity. 121 | IScribe.SchnorrData memory schnorrData = feeds.signSchnorr(message); 122 | 123 | // Poke scribe. 124 | vm.startBroadcast(); 125 | IScribe(self).poke(pokeData, schnorrData); 126 | vm.stopBroadcast(); 127 | 128 | console2.log( 129 | string.concat( 130 | "Poked, val=", 131 | vm.toString(pokeData.val), 132 | ", age=", 133 | vm.toString(pokeData.age) 134 | ) 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /script/benchmarks/ScribeBenchmark.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {console2} from "forge-std/console2.sol"; 6 | 7 | import {IScribe} from "src/IScribe.sol"; 8 | import {Scribe} from "src/Scribe.sol"; 9 | 10 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 11 | 12 | import {LibFeed} from "script/libs/LibFeed.sol"; 13 | 14 | /** 15 | * @notice Scribe Benchmark Script 16 | * 17 | * @dev Usage: 18 | * 1. Open new terminal and start anvil via: 19 | * $ anvil -b 1 20 | * 21 | * 2. Deploy contract via: 22 | * $ forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "deploy()" 23 | * 24 | * 3. Set bar via: 25 | * $ BAR=10 # Note to update to appropriate value 26 | * $ forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig $(cast calldata "setBar(uint8)" $BAR) 27 | * 28 | * 4. Lift feeds via: 29 | * $ forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "liftFeeds()" 30 | * 31 | * 5. Poke via: 32 | * $ forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "poke()" 33 | * 34 | * Note to poke more than once to get realistic gas costs. 35 | * During the first execution the storage slots are empty. 36 | */ 37 | contract ScribeBenchmark is Script { 38 | using LibFeed for LibFeed.Feed; 39 | using LibFeed for LibFeed.Feed[]; 40 | 41 | /// @dev Anvil's default mnemonic. 42 | string internal constant ANVIL_MNEMONIC = 43 | "test test test test test test test test test test test junk"; 44 | 45 | Scribe scribe = Scribe(address(0x5FbDB2315678afecb367f032d93F642f64180aa3)); 46 | 47 | function deploy() public { 48 | uint deployer = vm.deriveKey(ANVIL_MNEMONIC, uint32(0)); 49 | 50 | vm.broadcast(deployer); 51 | scribe = new Scribe(vm.addr(deployer), "ETH/USD"); 52 | } 53 | 54 | function setBar(uint8 bar) public { 55 | uint deployer = vm.deriveKey(ANVIL_MNEMONIC, uint32(0)); 56 | 57 | vm.broadcast(deployer); 58 | scribe.setBar(bar); 59 | } 60 | 61 | function liftFeeds() public { 62 | uint deployer = vm.deriveKey(ANVIL_MNEMONIC, uint32(0)); 63 | 64 | // Create bar many feeds. 65 | LibFeed.Feed[] memory feeds = _createFeeds(scribe.bar()); 66 | 67 | // Create list of feeds' public keys and ECDSA signatures. 68 | LibSecp256k1.Point[] memory pubKeys = 69 | new LibSecp256k1.Point[](feeds.length); 70 | IScribe.ECDSAData[] memory sigs = new IScribe.ECDSAData[](feeds.length); 71 | for (uint i; i < feeds.length; i++) { 72 | pubKeys[i] = feeds[i].pubKey; 73 | sigs[i] = feeds[i].signECDSA(scribe.feedRegistrationMessage()); 74 | } 75 | 76 | // Lift feeds. 77 | vm.broadcast(deployer); 78 | scribe.lift(pubKeys, sigs); 79 | } 80 | 81 | function poke() public { 82 | uint relay = vm.deriveKey(ANVIL_MNEMONIC, uint32(1)); 83 | 84 | // Create bar many feeds. 85 | LibFeed.Feed[] memory feeds = _createFeeds(scribe.bar()); 86 | 87 | // Create list of feeds' public keys. 88 | LibSecp256k1.Point[] memory pubKeys = 89 | new LibSecp256k1.Point[](feeds.length); 90 | for (uint i; i < feeds.length; i++) { 91 | pubKeys[i] = feeds[i].pubKey; 92 | } 93 | 94 | // Create pokeData. 95 | // Note to use max value for val to have highest possible gas costs. 96 | IScribe.PokeData memory pokeData = IScribe.PokeData({ 97 | val: type(uint128).max, 98 | age: uint32(block.timestamp) 99 | }); 100 | 101 | // Create schnorrData. 102 | IScribe.SchnorrData memory schnorrData; 103 | schnorrData = feeds.signSchnorr(scribe.constructPokeMessage(pokeData)); 104 | 105 | // Execute poke. 106 | vm.broadcast(relay); 107 | scribe.poke(pokeData, schnorrData); 108 | } 109 | 110 | function _createFeeds(uint numberFeeds) 111 | internal 112 | returns (LibFeed.Feed[] memory) 113 | { 114 | LibFeed.Feed[] memory feeds = new LibFeed.Feed[](numberFeeds); 115 | 116 | // Note to not start with privKey=1. This is because the sum of public 117 | // keys would evaluate to: 118 | // pubKeyOf(1) + pubKeyOf(2) + pubKeyOf(3) + ... 119 | // = pubKeyOf(3) + pubKeyOf(3) + ... 120 | // Note that pubKeyOf(3) would be doubled. Doubling is not supported by 121 | // LibSecp256k1 as this would indicate a double-signing attack. 122 | uint privKey = 2; 123 | uint bloom; 124 | uint ctr; 125 | while (ctr != numberFeeds) { 126 | LibFeed.Feed memory feed = LibFeed.newFeed({privKey: privKey}); 127 | 128 | // Check whether feed with id already created, if not create. 129 | if (bloom & (1 << feed.id) == 0) { 130 | bloom |= 1 << feed.id; 131 | 132 | feeds[ctr++] = feed; 133 | } 134 | 135 | privKey++; 136 | } 137 | 138 | return feeds; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /test/invariants/ScribeHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {console2} from "forge-std/console2.sol"; 5 | import {CommonBase} from "forge-std/Base.sol"; 6 | import {StdUtils} from "forge-std/StdUtils.sol"; 7 | import {StdStyle} from "forge-std/StdStyle.sol"; 8 | 9 | import {IScribe} from "src/IScribe.sol"; 10 | import {ScribeInspectable} from "../inspectable/ScribeInspectable.sol"; 11 | 12 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 13 | 14 | import {LibFeed} from "script/libs/LibFeed.sol"; 15 | 16 | import {FeedSet, LibFeedSet} from "./FeedSet.sol"; 17 | 18 | contract ScribeHandler is CommonBase, StdUtils { 19 | using LibSecp256k1 for LibSecp256k1.Point; 20 | using LibFeed for LibFeed.Feed; 21 | using LibFeed for LibFeed.Feed[]; 22 | using LibFeedSet for FeedSet; 23 | 24 | uint public constant MAX_BAR = 10; 25 | 26 | bytes32 public WAT; 27 | bytes32 public FEED_REGISTRATION_MESSAGE; 28 | 29 | IScribe public scribe; 30 | 31 | IScribe.PokeData internal _scribe_lastPokeData; 32 | 33 | uint internal _nextPrivKey = 2; 34 | FeedSet internal _feedSet; 35 | 36 | modifier cacheScribeState() { 37 | // forgefmt: disable-next-item 38 | _scribe_lastPokeData = ScribeInspectable(address(scribe)).inspectable_pokeData(); 39 | _; 40 | } 41 | 42 | function init(address scribe_) public virtual { 43 | scribe = IScribe(scribe_); 44 | 45 | // Cache constants. 46 | WAT = scribe.wat(); 47 | FEED_REGISTRATION_MESSAGE = scribe.feedRegistrationMessage(); 48 | } 49 | 50 | function _ensureBarFeedsLifted() internal { 51 | uint bar = scribe.bar(); 52 | address[] memory feeds = scribe.feeds(); 53 | 54 | if (feeds.length < bar) { 55 | // Lift feeds until bar is reached. 56 | uint missing = bar - feeds.length; 57 | LibFeed.Feed memory feed; 58 | while (missing != 0) { 59 | feed = LibFeed.newFeed(_nextPrivKey++); 60 | 61 | // Continue if feed's id already lifted. 62 | (bool isFeed,) = scribe.feeds(feed.id); 63 | if (isFeed) continue; 64 | 65 | // Otherwise lift feed and add to feedSet. 66 | scribe.lift( 67 | feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE) 68 | ); 69 | _feedSet.add({feed: feed, lifted: true}); 70 | 71 | missing--; 72 | } 73 | } 74 | } 75 | 76 | // -- Target Functions -- 77 | 78 | function warp(uint seed) external cacheScribeState { 79 | uint amount = _bound(seed, 1, 1 hours); 80 | vm.warp(block.timestamp + amount); 81 | } 82 | 83 | function poke(uint valSeed, uint ageSeed) external cacheScribeState { 84 | _ensureBarFeedsLifted(); 85 | 86 | // Wait some time if executed in same timestamp as last poke. 87 | if (_scribe_lastPokeData.age + 1 >= block.timestamp) { 88 | vm.warp( 89 | block.timestamp 90 | + ((_scribe_lastPokeData.age + 1) - block.timestamp) + 1 91 | ); 92 | } 93 | 94 | // Get set of bar many feeds from feedSet. 95 | LibFeed.Feed[] memory feeds = _feedSet.liftedFeeds(scribe.bar()); 96 | 97 | // Create pokeData. 98 | IScribe.PokeData memory pokeData = IScribe.PokeData({ 99 | val: _randPokeDataVal(valSeed), 100 | age: _randPokeDataAge(ageSeed) 101 | }); 102 | 103 | bytes32 pokeMessage = scribe.constructPokeMessage(pokeData); 104 | IScribe.SchnorrData memory schnorrData = feeds.signSchnorr(pokeMessage); 105 | 106 | // Note to not poke if schnorr signature is valid, but not acceptable. 107 | bool ok = 108 | scribe.isAcceptableSchnorrSignatureNow(pokeMessage, schnorrData); 109 | if (ok) { 110 | // Execute poke. 111 | scribe.poke(pokeData, feeds.signSchnorr(pokeMessage)); 112 | } else { 113 | console2.log( 114 | StdStyle.yellow( 115 | "ScribeHandler::poke: Skipping because Schnorr cannot be verified" 116 | ) 117 | ); 118 | } 119 | } 120 | 121 | function lift() external cacheScribeState { 122 | // Create new feed. 123 | LibFeed.Feed memory feed = LibFeed.newFeed(_nextPrivKey++); 124 | 125 | // Return if feed's id already lifted. 126 | (bool isFeed,) = scribe.feeds(feed.id); 127 | if (isFeed) return; 128 | 129 | // Lift feed and add to feedSet. 130 | scribe.lift(feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE)); 131 | _feedSet.add({feed: feed, lifted: true}); 132 | } 133 | 134 | function drop(uint seed) external cacheScribeState { 135 | if (_feedSet.count() == 0) return; 136 | 137 | // Get random feed from feedSet. 138 | // Note that feed may not be lifted. 139 | LibFeed.Feed memory feed = _feedSet.rand(seed); 140 | 141 | // Drop feed and mark as non-lifted in feedSet. 142 | scribe.drop(feed.id); 143 | _feedSet.updateLifted({feed: feed, lifted: false}); 144 | } 145 | 146 | function setBar(uint barSeed) external cacheScribeState { 147 | uint8 newBar = uint8(_bound(barSeed, 0, MAX_BAR)); 148 | 149 | // Should revert if newBar is 0. 150 | try scribe.setBar(newBar) {} catch {} 151 | } 152 | 153 | // -- Ghost View Functions -- 154 | 155 | function scribe_lastPokeData() 156 | external 157 | view 158 | returns (IScribe.PokeData memory) 159 | { 160 | return _scribe_lastPokeData; 161 | } 162 | 163 | function ghost_feedAddresses() external view returns (address[] memory) { 164 | address[] memory addrs = new address[](_feedSet.feeds.length); 165 | for (uint i; i < addrs.length; i++) { 166 | addrs[i] = _feedSet.feeds[i].pubKey.toAddress(); 167 | } 168 | return addrs; 169 | } 170 | 171 | // -- Helpers -- 172 | 173 | function _randPokeDataVal(uint seed) internal pure returns (uint128) { 174 | uint val = _bound(seed, 0, type(uint128).max); 175 | return uint128(val); 176 | } 177 | 178 | function _randPokeDataAge(uint seed) internal view returns (uint32) { 179 | uint age = _bound(seed, _scribe_lastPokeData.age + 1, block.timestamp); 180 | return uint32(age); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /script/libs/LibSecp256k1Extended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | 6 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 7 | 8 | /** 9 | * @title LibSecp256k1Extended 10 | * 11 | * @author Modified from Jordi Baylina's [ecsol](https://github.com/jbaylina/ecsol/blob/c2256afad126b7500e6f879a9369b100e47d435d/ec.sol). 12 | */ 13 | library LibSecp256k1Extended { 14 | using LibSecp256k1 for LibSecp256k1.Point; 15 | using LibSecp256k1 for LibSecp256k1.JacobianPoint; 16 | using LibSecp256k1Extended for LibSecp256k1.JacobianPoint; 17 | 18 | Vm private constant vm = 19 | Vm(address(uint160(uint(keccak256("hevm cheat code"))))); 20 | 21 | // -- Secp256k1 Constants -- 22 | // 23 | // Taken from https://www.secg.org/sec2-v2.pdf. 24 | // See section 2.4.1 "Recommended Parameters secp256k1". 25 | 26 | uint internal constant A = 0; 27 | uint internal constant B = 7; 28 | uint internal constant P = 29 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F; 30 | 31 | // -- API Functions -- 32 | 33 | function derivePublicKey(uint privKey) 34 | internal 35 | returns (LibSecp256k1.Point memory) 36 | { 37 | Vm.Wallet memory wallet = vm.createWallet(privKey); 38 | 39 | return LibSecp256k1.Point({x: wallet.publicKeyX, y: wallet.publicKeyY}); 40 | 41 | // Note that the public key can also be computed manually. 42 | // 43 | // Manual computation was used before forge-std supported public key 44 | // derivation, see https://github.com/foundry-rs/foundry/issues/4790. 45 | // 46 | // Note that using the vm cheatcode increases the test suite's 47 | // performance by ~5x. 48 | // 49 | // The code is kept as documentation. Note that all other functions in 50 | // this library are now unused. 51 | // 52 | // Manual computation of public key: 53 | // 54 | // LibSecp256k1.JacobianPoint memory jacResult; 55 | // jacResult = LibSecp256k1.G().toJacobian().mul(privKey); 56 | // 57 | // uint z = invMod(jacResult.z); 58 | // 59 | // return LibSecp256k1.Point({ 60 | // x: mulmod(jacResult.x, z, P), 61 | // y: mulmod(jacResult.y, z, P) 62 | // }); 63 | } 64 | 65 | function mul(LibSecp256k1.JacobianPoint memory self, uint scalar) 66 | internal 67 | pure 68 | returns (LibSecp256k1.JacobianPoint memory) 69 | { 70 | if (scalar == 0) { 71 | return LibSecp256k1.ZERO_POINT().toJacobian(); 72 | } 73 | 74 | LibSecp256k1.JacobianPoint memory copy; 75 | copy = self; 76 | 77 | LibSecp256k1.JacobianPoint memory result; 78 | result = LibSecp256k1.ZERO_POINT().toJacobian(); 79 | 80 | while (scalar != 0) { 81 | if (scalar % 2 == 1) { 82 | result = result.add(copy); 83 | } 84 | scalar /= 2; 85 | copy = copy.double(); 86 | } 87 | 88 | return result; 89 | } 90 | 91 | function double(LibSecp256k1.JacobianPoint memory self) 92 | internal 93 | pure 94 | returns (LibSecp256k1.JacobianPoint memory) 95 | { 96 | return self.add(self); 97 | } 98 | 99 | function add( 100 | LibSecp256k1.JacobianPoint memory self, 101 | LibSecp256k1.JacobianPoint memory p 102 | ) internal pure returns (LibSecp256k1.JacobianPoint memory) { 103 | if (self.x == 0 && self.y == 0) { 104 | return p; 105 | } 106 | if (p.x == 0 && p.y == 0) { 107 | return self; 108 | } 109 | 110 | uint l; 111 | uint lz; 112 | uint da; 113 | uint db; 114 | LibSecp256k1.JacobianPoint memory result; 115 | 116 | if (self.x == p.x && self.y == p.y) { 117 | (l, lz) = _mul(self.x, self.z, self.x, self.z); 118 | (l, lz) = _mul(l, lz, 3, 1); 119 | (l, lz) = _add(l, lz, A, 1); 120 | 121 | (da, db) = _mul(self.y, self.z, 2, 1); 122 | } else { 123 | (l, lz) = _sub(p.y, p.z, self.y, self.z); 124 | (da, db) = _sub(p.x, p.z, self.x, self.z); 125 | } 126 | 127 | (l, lz) = _div(l, lz, da, db); 128 | 129 | (result.x, da) = _mul(l, lz, l, lz); 130 | (result.x, da) = _sub(result.x, da, self.x, self.z); 131 | (result.x, da) = _sub(result.x, da, p.x, p.z); 132 | 133 | (result.y, db) = _sub(self.x, self.z, result.x, da); 134 | (result.y, db) = _mul(result.y, db, l, lz); 135 | (result.y, db) = _sub(result.y, db, self.y, self.z); 136 | 137 | if (da != db) { 138 | result.x = mulmod(result.x, db, P); 139 | result.y = mulmod(result.y, da, P); 140 | result.z = mulmod(da, db, P); 141 | } else { 142 | result.z = da; 143 | } 144 | 145 | return result; 146 | } 147 | 148 | function invMod(uint x) internal pure returns (uint) { 149 | uint t; 150 | uint q; 151 | uint newT = 1; 152 | uint r = P; 153 | 154 | while (x != 0) { 155 | q = r / x; 156 | (t, newT) = (newT, addmod(t, (P - mulmod(q, newT, P)), P)); 157 | (r, x) = (x, r - (q * x)); 158 | } 159 | 160 | return t; 161 | } 162 | 163 | // -- Private Helpers -- 164 | 165 | function _add(uint x1, uint z1, uint x2, uint z2) 166 | private 167 | pure 168 | returns (uint, uint) 169 | { 170 | uint x3 = addmod(mulmod(z2, x1, P), mulmod(x2, z1, P), P); 171 | uint z3 = mulmod(z1, z2, P); 172 | 173 | return (x3, z3); 174 | } 175 | 176 | function _sub(uint x1, uint z1, uint x2, uint z2) 177 | private 178 | pure 179 | returns (uint, uint) 180 | { 181 | uint x3 = addmod(mulmod(z2, x1, P), mulmod(P - x2, z1, P), P); 182 | uint z3 = mulmod(z1, z2, P); 183 | 184 | return (x3, z3); 185 | } 186 | 187 | function _mul(uint x1, uint z1, uint x2, uint z2) 188 | private 189 | pure 190 | returns (uint, uint) 191 | { 192 | uint x3 = mulmod(x1, x2, P); 193 | uint z3 = mulmod(z1, z2, P); 194 | 195 | return (x3, z3); 196 | } 197 | 198 | function _div(uint x1, uint z1, uint x2, uint z2) 199 | private 200 | pure 201 | returns (uint, uint) 202 | { 203 | uint x3 = mulmod(x1, z2, P); 204 | uint z3 = mulmod(z1, x2, P); 205 | 206 | return (x3, z3); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /test/LibSecp256k1Test.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {console2} from "forge-std/console2.sol"; 6 | 7 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 8 | 9 | import {LibSecp256k1Extended} from "script/libs/LibSecp256k1Extended.sol"; 10 | 11 | abstract contract LibSecp256k1Test is Test { 12 | using LibSecp256k1 for LibSecp256k1.Point; 13 | using LibSecp256k1 for LibSecp256k1.JacobianPoint; 14 | using LibSecp256k1Extended for uint; 15 | 16 | // -- toAddress -- 17 | 18 | function testFuzzDifferential_toAddress(uint privKeySeed) public { 19 | // Let privKey ∊ [1, Q). 20 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 21 | 22 | address want = vm.addr(privKey); 23 | address got = privKey.derivePublicKey().toAddress(); 24 | 25 | assertEq(want, got); 26 | } 27 | 28 | // -- isZeroPoint -- 29 | 30 | function test_isZeroPoint() public { 31 | assertTrue(LibSecp256k1.Point(0, 0).isZeroPoint()); 32 | assertFalse(LibSecp256k1.Point(1, 0).isZeroPoint()); 33 | } 34 | 35 | function testFuzz_isZeroPoint(LibSecp256k1.Point memory p) public { 36 | bool want = p.x == 0 && p.y == 0; 37 | bool got = p.isZeroPoint(); 38 | 39 | assertEq(want, got); 40 | } 41 | 42 | // -- isOnCurve -- 43 | 44 | function testFuzz_isOnCurve(uint privKeySeed) public { 45 | // Let privKey ∊ [1, Q). 46 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 47 | 48 | assertTrue(privKey.derivePublicKey().isOnCurve()); 49 | } 50 | 51 | function testFuzz_isOnCurve_FailsIf_PointNotOnCurve( 52 | uint privKeySeed, 53 | uint maskX, 54 | uint maskY 55 | ) public { 56 | vm.assume(maskX != 0 || maskY != 0); 57 | 58 | // Let privKey ∊ [1, Q). 59 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 60 | 61 | // Compute and mutate point. 62 | LibSecp256k1.Point memory p = privKey.derivePublicKey(); 63 | LibSecp256k1.Point memory pMutated = privKey.derivePublicKey(); 64 | pMutated.x ^= maskX; 65 | pMutated.y ^= maskY; 66 | vm.assume(pMutated.x != p.x || pMutated.y != p.y); 67 | 68 | assertFalse(pMutated.isOnCurve()); 69 | } 70 | 71 | // -- yParity -- 72 | 73 | function test_yParity() public { 74 | assertEq(LibSecp256k1.Point(1, 0).yParity(), 0); 75 | assertEq(LibSecp256k1.Point(1, 1).yParity(), 1); 76 | assertEq(LibSecp256k1.Point(1, 2).yParity(), 0); 77 | } 78 | 79 | function testFuzz_yParity(LibSecp256k1.Point memory p) public { 80 | uint want = p.y % 2; 81 | uint got = p.yParity(); 82 | 83 | assertEq(want, got); 84 | } 85 | 86 | // -- toAffine -- 87 | 88 | function testFuzz_toAffine_DoesNotRevert( 89 | LibSecp256k1.JacobianPoint memory jacPoint 90 | ) public pure { 91 | jacPoint.toAffine(); 92 | } 93 | 94 | function testFuzz_toJacobian_toAffine(uint privKeySeed) public { 95 | // Let privKey ∊ [1, Q). 96 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 97 | 98 | LibSecp256k1.Point memory want = privKey.derivePublicKey(); 99 | LibSecp256k1.Point memory got = want.toJacobian().toAffine(); 100 | 101 | assertEq(want.x, got.x); 102 | assertEq(want.y, got.y); 103 | } 104 | 105 | // -- addAffinePoint -- 106 | 107 | function testFuzz_addAffinePoint_UsesConstantAmountOfGas( 108 | LibSecp256k1.JacobianPoint memory jacPoint1, 109 | LibSecp256k1.Point memory p1, 110 | LibSecp256k1.JacobianPoint memory jacPoint2, 111 | LibSecp256k1.Point memory p2 112 | ) public { 113 | // Benchmark jacPoint1 + p1. 114 | uint gasBefore = gasleft(); 115 | jacPoint1.addAffinePoint(p1); 116 | uint gasAfter = gasleft(); 117 | uint first = gasBefore - gasAfter; 118 | 119 | // Benchmark jacPoint2 + p2. 120 | gasBefore = gasleft(); 121 | jacPoint2.addAffinePoint(p2); 122 | gasAfter = gasleft(); 123 | uint second = gasBefore - gasAfter; 124 | 125 | // @todo Not using --via-ir, the second computation uses 3 gas less. 126 | assertApproxEqAbs(first, second, 3); 127 | } 128 | 129 | function testFuzz_addAffinePoint_DoesNotRevert( 130 | LibSecp256k1.JacobianPoint memory jacPoint, 131 | LibSecp256k1.Point memory p 132 | ) public pure { 133 | jacPoint.addAffinePoint(p); 134 | } 135 | 136 | struct VectorTestCase { 137 | // Test: p + q = expected 138 | LibSecp256k1.Point p; 139 | LibSecp256k1.Point q; 140 | LibSecp256k1.Point expected; 141 | } 142 | 143 | function testVectors_addAffinePoint() public { 144 | string[] memory inputs = new string[](2); 145 | inputs[0] = "node"; 146 | inputs[1] = "test/vectors/points.js"; 147 | 148 | uint[] memory rawCoordinates = abi.decode(vm.ffi(inputs), (uint[])); 149 | 150 | // Parse raw coordinates to VectorTestCases. 151 | VectorTestCase[] memory testCases = 152 | new VectorTestCase[](rawCoordinates.length / 2 / 3); 153 | uint rawCoordinatesCtr; 154 | for (uint i; i < testCases.length; i++) { 155 | VectorTestCase memory cur = testCases[i]; 156 | 157 | cur.p.x = rawCoordinates[rawCoordinatesCtr++]; 158 | cur.p.y = rawCoordinates[rawCoordinatesCtr++]; 159 | cur.q.x = rawCoordinates[rawCoordinatesCtr++]; 160 | cur.q.y = rawCoordinates[rawCoordinatesCtr++]; 161 | cur.expected.x = rawCoordinates[rawCoordinatesCtr++]; 162 | cur.expected.y = rawCoordinates[rawCoordinatesCtr++]; 163 | } 164 | 165 | // Execute test cases. 166 | VectorTestCase memory curTestCase; 167 | LibSecp256k1.JacobianPoint memory jacPoint; 168 | LibSecp256k1.Point memory result; 169 | for (uint i; i < testCases.length; i++) { 170 | curTestCase = testCases[i]; 171 | 172 | jacPoint = curTestCase.p.toJacobian(); 173 | jacPoint.addAffinePoint(curTestCase.q); 174 | 175 | result = jacPoint.toAffine(); 176 | 177 | if (curTestCase.p.x == curTestCase.q.x) { 178 | console2.log( 179 | string.concat( 180 | "Note: Test case #", 181 | vm.toString(i), 182 | " has same x coordinates:" 183 | ), 184 | "Expecting zero point as result" 185 | ); 186 | 187 | assertTrue(result.isZeroPoint()); 188 | } else { 189 | assertEq(result.x, curTestCase.expected.x); 190 | assertEq(result.y, curTestCase.expected.y); 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /docs/Schnorr.md: -------------------------------------------------------------------------------- 1 | # Schnorr Signature Scheme Specification 2 | 3 | This document specifies a custom Schnorr-based signature scheme on the secp256k1 4 | elliptic curve. The scheme is used by _Chronicle Protocol_'s Scribe oracle contract. 5 | 6 | ## Terminology 7 | 8 | * `H()` - Keccak256 hash function 9 | * `‖` - Concatenation operator, defined as `abi.encodePacked()` 10 | 11 | * `G` - Generator of secp256k1 12 | * `Q` - Order of secp256k1 13 | 14 | * `x` - The signer's private key as type `uint256` 15 | * `P` - The signer's public key, i.e. `[x]G`, as type `(uint256, uint256)` 16 | * `Pₓ` - P's x coordinate as type `uint256` 17 | * `Pₚ` - Parity of `P`'s `y` coordinate, i.e. `0` if even, `1` if odd, as type `uint8` 18 | 19 | * `m` - Message as type `bytes32`. Note that the message **SHOULD** be a keccak256 digest 20 | * `k` - Nonce as type `uint256` 21 | 22 | 23 | ## Signing 24 | 25 | 1. Select a _cryptographically secure random_ `k ∊ [1, Q)` 26 | 27 | 2. Compute `R = [k]G` 28 | 29 | 3. Derive `Rₑ` being the Ethereum address of `R` 30 | 31 | Let `Rₑ` be the _commitment_ 32 | 33 | 4. Construct `e = H(Pₓ ‖ Pₚ ‖ m ‖ Rₑ) mod Q` 34 | 35 | Let `e` be the _challenge_ 36 | 37 | 5. Compute `s = k + (e * x) mod Q` 38 | 39 | Let `s` be the _signature_ 40 | 41 | => The public key `P` signs via the signature `s` and the commitment `Rₑ` the 42 | message `m` 43 | 44 | 45 | ## Verification 46 | 47 | - Input : `(P, m, s, Rₑ)` 48 | - Output: `True` if signature verification succeeds, `false` otherwise 49 | 50 | 1. Compute _challenge_ `e = H(Pₓ ‖ Pₚ ‖ m ‖ Rₑ) mod Q` 51 | 52 | 2. Compute _commitment_: 53 | ``` 54 | [s]G - [e]P | s = k + (e * x) 55 | = [k + (e * x)]G - [e]P | P = [x]G 56 | = [k + (e * x)]G - [e * x]G | Distributive Law 57 | = [k + (e * x) - (e * x)]G | (e * x) - (e * x) = 0 58 | = [k]G | R = [k]G 59 | = R | Let ()ₑ be the Ethereum address of a Point 60 | → Rₑ 61 | ``` 62 | 63 | 3. Verification succeeds iff `([s]G - [e]P)ₑ = Rₑ` 64 | 65 | 66 | ## Key Aggregation for Multisignatures 67 | 68 | In order to efficiently aggregate public keys onchain, the key aggregation 69 | mechanism for aggregated signatures is specified as the sum of the public 70 | keys: 71 | 72 | ``` 73 | Let the signers' public keys be: 74 | signers = [pubKey₁, pubKey₂, ..., pubKeyₙ] 75 | 76 | Let the aggregated public key be: 77 | aggPubKey = sum(signers) 78 | = pubKey₁ + pubKey₂ + ... + pubKeyₙ 79 | = [privKey₁]G + [privKey₂]G + ... + [privKeyₙ]G 80 | = [privKey₁ + privKey₂ + ... + privKeyₙ]G 81 | ``` 82 | 83 | Note that this aggregation scheme is vulnerable to rogue-key attacks[^musig2-paper]! 84 | In order to prevent such attacks a separate public key validation step, called a 85 | proof of possession, must be performed. This proof of possession can be 86 | implemented via an ECDSA signature, however, the message signed **MUST** be 87 | derived from the respective public key[^bls-proof-of-possession]. 88 | 89 | Note further that this aggregation scheme is vulnerable to public keys with 90 | linear relationships. A set of public keys `A` leaking the sum of their private 91 | keys would allow the creation of a second set of public keys `B` with 92 | `aggPubKey(A) = aggPubKey(B)`. This would make signatures created by set `A` 93 | indistinguishable from signatures created by set `B`. 94 | However, this specification assumes that participants do not share private key 95 | material leading to negligible probability for such cases to happen. 96 | 97 | 98 | ## Other Security Considerations 99 | 100 | Note that the signing scheme deviates slightly from the classical Schnorr 101 | signature scheme. 102 | 103 | Instead of using the secp256k1 point `R = [k]G` directly, this scheme uses the 104 | Ethereum address of the point `R`. This decreases the difficulty of 105 | brute-forcing the signature from `256 bits` (trying random secp256k1 points) 106 | to `160 bits` (trying random Ethereum addresses). 107 | 108 | However, the difficulty of cracking a secp256k1 public key using the 109 | baby-step giant-step algorithm is `O(√Q)`, with `Q` being the order of the group[^baby-step-giant-step-wikipedia]. 110 | Note that `√Q ~ 3.4e38 < 128 bit`. 111 | 112 | Therefore, this signing scheme does not weaken the overall security. 113 | 114 | 115 | ## Implementation Optimizations 116 | 117 | This implementation uses the ecrecover precompile to perform the necessary 118 | elliptic curve multiplication in secp256k1 during the verification process. 119 | 120 | The ecrecover precompile can roughly be implemented in python via[^vitalik-ethresearch-post]: 121 | ```python 122 | def ecdsa_raw_recover(msghash, vrs): 123 | v, r, s = vrs 124 | y = # (get y coordinate for EC point with x=r, with same parity as v) 125 | Gz = jacobian_multiply((Gx, Gy, 1), (Q - hash_to_int(msghash)) % Q) 126 | XY = jacobian_multiply((r, y, 1), s) 127 | Qr = jacobian_add(Gz, XY) 128 | N = jacobian_multiply(Qr, inv(r, Q)) 129 | return from_jacobian(N) 130 | ``` 131 | 132 | Note that ecrecover also uses `s` as variable. From this point forward, let 133 | the Schnorr signature's `s` be `sig`. 134 | 135 | A single ecrecover call can compute `([sig]G - [e]P)ₑ = ([k]G)ₑ = Rₑ` via the 136 | following inputs: 137 | ``` 138 | msghash = -sig * Pₓ 139 | v = Pₚ + 27 140 | r = Pₓ 141 | s = Q - (e * Pₓ) 142 | ``` 143 | 144 | Note that ecrecover returns the Ethereum address of `R` and not `R` itself. 145 | 146 | The ecrecover call then digests to: 147 | ``` 148 | Gz = [Q - (-sig * Pₓ)]G | Double negation 149 | = [Q + (sig * Pₓ)]G | Addition with Q can be removed in (mod Q) 150 | = [sig * Pₓ]G | sig = k + (e * x) 151 | = [(k + (e * x)) * Pₓ]G 152 | 153 | XY = [Q - (e * Pₓ)]P | P = [x]G 154 | = [(Q - (e * Pₓ)) * x]G 155 | 156 | Qr = Gz + XY | Gz = [(k + (e * x)) * Pₓ]G 157 | = [(k + (e * x)) * Pₓ]G + XY | XY = [(Q - (e * Pₓ)) * x]G 158 | = [(k + (e * x)) * Pₓ]G + [(Q - (e * Pₓ)) * x]G 159 | 160 | N = Qr * Pₓ⁻¹ | Qr = [(k + (e * x)) * Pₓ]G + [(Q - (e * Pₓ)) * x]G 161 | = [(k + (e * x)) * Pₓ]G + [(Q - (e * Pₓ)) * x]G * Pₓ⁻¹ | Distributive law 162 | = [(k + (e * x)) * Pₓ * Pₓ⁻¹]G + [(Q - (e * Pₓ)) * x * Pₓ⁻¹]G | Pₓ * Pₓ⁻¹ = 1 163 | = [(k + (e * x))]G + [Q - e * x]G | sig = k + (e * x) 164 | = [sig]G + [Q - e * x]G | Q - (e * x) = -(e * x) in (mod Q) 165 | = [sig]G - [e * x]G | P = [x]G 166 | = [sig]G - [e]P 167 | ``` 168 | 169 | 170 | ## Resources 171 | 172 | - [github.com/sipa/secp256k1](https://github.com/sipa/secp256k1/blob/968e2f415a5e764d159ee03e95815ea11460854e/src/modules/schnorr/schnorr.md) 173 | - [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) 174 | - [Analysis of Bitcoin Improvement Proposal 340](https://courses.csail.mit.edu/6.857/2020/projects/4-Elbahrawy-Lovejoy-Ouyang-Perez.pdf) 175 | 176 | [^musig2-paper]:[MuSig2 Paper](https://eprint.iacr.org/2020/1261.pdf) 177 | [^bls-proof-of-possession]:[BLSBLS Signatures](https://www.ietf.org/archive/id/draft-irtf-cfrg-bls-signature-05.html#name-proof-of-possession) 178 | [^baby-step-giant-step-wikipedia]:[Baby-step giant-step Wikipedia](https://en.wikipedia.org/wiki/Baby-step_giant-step) 179 | [^vitalik-ethresearch-post]:[Vitalik's ethresearch post](https://ethresear.ch/t/you-can-kinda-abuse-ecrecover-to-do-ecmul-in-secp256k1-today/2384) 180 | -------------------------------------------------------------------------------- /script/ScribeOptimistic.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {console2} from "forge-std/console2.sol"; 5 | 6 | import {IToll} from "chronicle-std/toll/IToll.sol"; 7 | 8 | import {IScribe} from "src/IScribe.sol"; 9 | import {IScribeOptimistic} from "src/IScribeOptimistic.sol"; 10 | import {Chronicle_BASE_QUOTE_COUNTER as ScribeOptimistic} from 11 | "src/ScribeOptimistic.sol"; 12 | // @todo ^^^^ ^^^^^ ^^^^^^^ Adjust name of Scribe instance. 13 | 14 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 15 | 16 | import {ScribeScript} from "./Scribe.s.sol"; 17 | 18 | import {LibRandom} from "./libs/LibRandom.sol"; 19 | import {LibFeed} from "./libs/LibFeed.sol"; 20 | 21 | import {Rescuer} from "./rescue/Rescuer.sol"; 22 | 23 | /** 24 | * @title ScribeOptimistic Management Script 25 | */ 26 | contract ScribeOptimisticScript is ScribeScript { 27 | using LibSecp256k1 for LibSecp256k1.Point; 28 | using LibFeed for LibFeed.Feed; 29 | 30 | /// @dev Deploys a new ScribeOptimistic instance with `initialAuthed` being 31 | /// the address initially auth'ed. Note that zero address is kissed 32 | /// directly after deployment. 33 | function deploy(address initialAuthed, bytes32 wat) 34 | public 35 | override(ScribeScript) 36 | { 37 | vm.startBroadcast(); 38 | require(msg.sender == initialAuthed, "Deployer must be initial auth'ed"); 39 | address deployed = address(new ScribeOptimistic(initialAuthed, wat)); 40 | IToll(deployed).kiss(address(0)); 41 | vm.stopBroadcast(); 42 | 43 | console2.log("Deployed at", deployed); 44 | } 45 | 46 | // -- IScribeOptimistic Functions -- 47 | 48 | /// @dev Sets the opChallengePeriod of `self` to `opChallengePeriod`. 49 | function setOpChallengePeriod(address self, uint16 opChallengePeriod) 50 | public 51 | { 52 | vm.startBroadcast(); 53 | IScribeOptimistic(self).setOpChallengePeriod(opChallengePeriod); 54 | vm.stopBroadcast(); 55 | 56 | console2.log("OpChallengePeriod set to", opChallengePeriod); 57 | } 58 | 59 | /// @dev Sets the maxChallengeReward of `self` to `maxChallengeReward`. 60 | function setMaxChallengeReward(address self, uint maxChallengeReward) 61 | public 62 | { 63 | vm.startBroadcast(); 64 | IScribeOptimistic(self).setMaxChallengeReward(maxChallengeReward); 65 | vm.stopBroadcast(); 66 | 67 | console2.log("MaxChallengeReward set to", maxChallengeReward); 68 | } 69 | 70 | /// @dev opPokes `self` with arguments given via calldata payload `payload`. 71 | /// 72 | /// @dev Note that this function can be used to simulate - or execute - 73 | /// opPokes with an already fully constructed payload. 74 | /// 75 | /// @dev Call via: 76 | /// ```bash 77 | /// $ forge script \ 78 | /// --keystore $KEYSTORE \ 79 | /// --password $KEYSTORE_PASSWORD \ 80 | /// --broadcast \ 81 | /// --rpc-url $RPC_URL \ 82 | /// --sig $(cast calldata "opPokeRaw(address,bytes)" $SCRIBE $PAYLOAD) \ 83 | /// -vvvvv \ 84 | /// script/dev/ScribeOptimistic.s.sol:ScribeOptimisticScript 85 | /// ``` 86 | /// 87 | /// Note to remove `--broadcast` to just simulate the opPoke. 88 | function opPokeRaw(address self, bytes calldata payload) public { 89 | // Note to remove first 4 bytes, ie the function selector, from the 90 | // payload to receive the arguments. 91 | bytes calldata args = payload[4:]; 92 | 93 | // Decode arguments into opPoke argument types. 94 | IScribe.PokeData memory pokeData; 95 | IScribe.SchnorrData memory schnorrData; 96 | IScribe.ECDSAData memory ecdsaData; 97 | (pokeData, schnorrData, ecdsaData) = abi.decode( 98 | args, (IScribe.PokeData, IScribe.SchnorrData, IScribe.ECDSAData) 99 | ); 100 | 101 | // Print arguments. 102 | console2.log("PokeData"); 103 | console2.log("- val :", pokeData.val); 104 | console2.log("- age :", pokeData.age); 105 | console2.log("SchnorrData"); 106 | console2.log("- signature :", uint(schnorrData.signature)); 107 | console2.log("- commitment :", schnorrData.commitment); 108 | console2.log("- feedIds :", vm.toString(schnorrData.feedIds)); 109 | console2.log("ECDSAData"); 110 | console2.log("- v :", ecdsaData.v); 111 | console2.log("- r :", uint(ecdsaData.r)); 112 | console2.log("- s :", uint(ecdsaData.s)); 113 | 114 | // Execute opPoke. 115 | vm.startBroadcast(); 116 | IScribeOptimistic(self).opPoke(pokeData, schnorrData, ecdsaData); 117 | vm.stopBroadcast(); 118 | } 119 | 120 | /// @dev Rescues ETH held in deactivated `self`. 121 | /// 122 | /// @dev Call via: 123 | /// ```bash 124 | /// $ forge script \ 125 | /// --keystore $KEYSTORE \ 126 | /// --password $KEYSTORE_PASSWORD \ 127 | /// --broadcast \ 128 | /// --rpc-url $RPC_URL \ 129 | /// --sig $(cast calldata "rescueETH(address,address)" $SCRIBE $RESCUER) \ 130 | /// -vvvvv \ 131 | /// script/dev/ScribeOptimistic.s.sol:ScribeOptimisticScript 132 | /// ``` 133 | function rescueETH(address self, address rescuer) public { 134 | // Require self to be deactivated. 135 | { 136 | vm.prank(address(0)); 137 | (bool ok, /*val*/ ) = IScribe(self).tryRead(); 138 | require(!ok, "Instance not deactivated: read() does not fail"); 139 | 140 | require( 141 | IScribe(self).feeds().length == 0, 142 | "Instance not deactivated: Feeds still lifted" 143 | ); 144 | require( 145 | IScribe(self).bar() == 255, 146 | "Instance not deactivated: Bar not type(uint8).max" 147 | ); 148 | } 149 | 150 | // Ensure challenge reward is total balance. 151 | uint challengeReward = IScribeOptimistic(self).challengeReward(); 152 | uint total = self.balance; 153 | if (challengeReward < total) { 154 | IScribeOptimistic(self).setMaxChallengeReward(type(uint).max); 155 | } 156 | 157 | // Create new random private key. 158 | uint privKeySeed = LibRandom.readUint(); 159 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 160 | 161 | // Create feed instance from private key. 162 | LibFeed.Feed memory feed = LibFeed.newFeed(privKey); 163 | 164 | // Let feed sign feed registration message. 165 | IScribe.ECDSAData memory registrationSig; 166 | registrationSig = 167 | feed.signECDSA(IScribe(self).feedRegistrationMessage()); 168 | 169 | // Construct pokeData and invalid Schnorr signature. 170 | uint32 pokeDataAge = uint32(block.timestamp); 171 | IScribe.PokeData memory pokeData = IScribe.PokeData(0, pokeDataAge); 172 | IScribe.SchnorrData memory schnorrData = 173 | IScribe.SchnorrData(bytes32(0), address(0), hex""); 174 | 175 | // Construct opPokeMessage. 176 | bytes32 opPokeMessage = IScribeOptimistic(self).constructOpPokeMessage( 177 | pokeData, schnorrData 178 | ); 179 | 180 | // Let feed sign opPokeMessage. 181 | IScribe.ECDSAData memory opPokeSig = feed.signECDSA(opPokeMessage); 182 | 183 | // Rescue ETH via rescuer contract. 184 | Rescuer(payable(rescuer)).suck( 185 | self, feed.pubKey, registrationSig, pokeDataAge, opPokeSig 186 | ); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /docs/Scribe.md: -------------------------------------------------------------------------------- 1 | # Scribe 2 | 3 | This document provides technical documentation for _Chronicle Protocol_'s Scribe oracle system. 4 | 5 | 6 | ## Table of Contents 7 | 8 | - [Scribe](#scribe) 9 | - [Table of Contents](#table-of-contents) 10 | - [Overview](#overview) 11 | - [Schnorr Signature Scheme](#schnorr-signature-scheme) 12 | - [Elliptic Curve Computations](#elliptic-curve-computations) 13 | - [Encoding of Participating Public Keys](#encoding-participating-public-keys) 14 | - [Lifting Feeds](#lifting-feeds) 15 | - [Chainlink Compatibility](#chainlink-compatibility) 16 | - [Optimistic-Flavored Scribe](#optimistic-flavored-scribe) 17 | - [About Bounded Gas Usage](#about-bounded-gas-usage) 18 | - [Verifying Optimistic Pokes](#verifying-optimistic-pokes) 19 | - [Benchmarks](#benchmarks) 20 | 21 | ## Overview 22 | 23 | Scribe is an efficient Schnorr multi-signature based oracle allowing a subset of feeds to multi-sign a `(value, age)` tuple via a custom Schnorr scheme. The oracle advances to a new `(value, age)` tuple - via the public callable `poke()` function - if the given tuple is signed by exactly `IScribe::bar()` many feeds. 24 | 25 | The Scribe contract also allows the creation of an _optimistic-flavored_ oracle instance with onchain fault resolution called _ScribeOptimistic_. 26 | 27 | Scribe implements _Chronicle Protocol_'s [`IChronicle`](https://github.com/chronicleprotocol/chronicle-std/blob/v1/src/IChronicle.sol) interface for reading the oracle's value. 28 | 29 | To protect authorized functions, Scribe uses `chronicle-std`'s [`Auth`](https://github.com/chronicleprotocol/chronicle-std/blob/v2/src/auth/Auth.sol) module. Functions to read the oracle's value are protected via `chronicle-std`'s [`Toll`](https://github.com/chronicleprotocol/chronicle-std/blob/v2/src/toll/Toll.sol) module. 30 | 31 | ## Schnorr Signature Scheme 32 | 33 | Scribe uses a custom Schnorr signature scheme. The scheme is specified in [docs/Schnorr.md](./Schnorr.md). 34 | 35 | The verification logic is implemented in [`LibSchnorr.sol`](../src/libs/LibSchnorr.sol). A Solidity library to (multi-) sign data is provided via [`script/libs/LibSchnorrExtended.sol`](../script/libs/LibSchnorrExtended.sol). 36 | 37 | ## Elliptic Curve Computations 38 | 39 | Scribe needs to perform elliptic curve computations on the secp256k1 curve to verify aggregated/multi signatures. 40 | 41 | The [`LibSecp256k1.sol`](../src/libs/LibSecp256k1.sol) library provides the necessary addition and point conversion (Affine coordinates <-> Jacobian coordinates) functions. 42 | 43 | In order to save computation-heavy conversions from Jacobian coordinates - which are used for point addition - back to Affine coordinates - which are used to store public keys -, `LibSecp256k1` uses an addition formula expecting one point's `z` coordinate to be 1. Effectively allowing to add a point in Affine coordinates to a point in Jacobian coordinates. 44 | 45 | This optimization allows Scribe to aggregate public keys, i.e. compute the sum of secp256k1 points, in an efficient manner by only having to convert the end result from Jacobian coordinates to Affine coordinates. 46 | 47 | For more info, see [`LibSecp256k1::addAffinePoint()`](../src/libs/LibSecp256k1.sol). 48 | 49 | ## Encoding Participating Public Keys 50 | 51 | The `poke()` function has to receive the set of feeds, i.e. public keys, that participated in the Schnorr multi-signature. 52 | 53 | 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`. 54 | 55 | 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)`. 56 | 57 | ## Lifting Feeds 58 | 59 | Feeds _must_ prove the integrity of their public key by proving the ownership of the corresponding private key. The `lift()` function therefore expects an ECDSA signed message, for more info see [`IScribe.feedRegistrationMessage()`](../src/IScribe.sol). 60 | 61 | > [!WARNING] 62 | > 63 | > The proof of possession implemented in Scribe is insufficient to defend against rogue-key attacks. In order to sufficiently verify a public key the message being signed MUST be derived from the public key itself. 64 | > 65 | > In order to keep Scribe backwards compatible the extended proof of possession is implemented in the external [ValidatorRegistry](https://github.com/chronicleprotocol/validator-registry) contract. 66 | > 67 | > For more info, see [audits/Cantina@v2.0.0_2.pdf](../audits/Cantina@v2.0.0_2.pdf). 68 | 69 | 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). 70 | 71 | ## Chainlink Compatibility 72 | 73 | Scribe aims to be partially Chainlink compatible by implementing the most widely, and not deprecated, used functions of the `IChainlinkAggregatorV3` interface. 74 | 75 | The following `IChainlinkAggregatorV3` functions are provided: 76 | - `latestRoundData()` 77 | - `decimals()` 78 | - `latestAnswer()` 79 | 80 | ## Optimistic-Flavored Scribe 81 | 82 | _ScribeOptimistic_ is a contract inheriting from Scribe and providing an _optimistic-flavored_ Scribe version. This version is intended to only be used on Layer 1s with expensive computation. 83 | 84 | To circumvent verifying Schnorr signatures onchain, `ScribeOptimistic` provides an additional `opPoke()` function. This function expects the `(value, age)` tuple and corresponding Schnorr signature to be signed via ECDSA by a single feed. 85 | 86 | The `opPoke()` function binds the feed to the data they signed. A public callable `opChallenge()` function can be called at any time. The function verifies the current optimistically poked data and, if the Schnorr signature verification succeeds, finalizes the data. However, if the Schnorr signature verification fails, the feed bound to the data is automatically `diss`'ed, i.e. removed from the whitelist, and the data deleted. 87 | 88 | If an `opPoke()` is not challenged, its value finalizes after a specified period. For more info, see [`IScribeOptimistic::opChallengePeriod()`](../src/IScribeOptimistic.sol). 89 | 90 | Monitoring optimistic pokes and, if necessary, challenging them can be incentivized via ETH rewards. For more info, see [`IScribeOptimistic::maxChallengeReward()`](../src/IScribeOptimistic.sol). 91 | 92 | ### About Bounded Gas Usage 93 | 94 | For all functions being executed during `opChallenge()`, it is of utmost importance to have bounded gas usage. These functions are marked with `@custom:invariant` specifications documenting their gas usage. 95 | 96 | The gas usage _must_ be bounded to ensure an invalid `opPoke()` can always be successfully challenged. 97 | 98 | Two loops are executed during an `opChallenge()`: 99 | 1. Inside `Scribe::_verifySchnorrSignature` - bounded by `bar` 100 | 2. Inside `LibSecp256k1::_invMod` - computing the modular inverse of a Jacobian `z` coordinate of a secp256k1 point 101 | 102 | ### Verifying Optimistic Pokes 103 | 104 | 1. Listen to `opPoked` events: 105 | ```solidity 106 | event OpPoked( 107 | address indexed caller, 108 | address indexed opFeed, 109 | IScribe.SchnorrData schnorrData, 110 | IScribe.PokeData pokeData 111 | ); 112 | ``` 113 | 114 | 2. Construct message from `pokeData`: 115 | ```solidity 116 | function constructPokeMessage(PokeData calldata pokeData) 117 | external 118 | view 119 | returns (bytes32); 120 | ``` 121 | 122 | 3. Verify Schnorr signature is acceptable: 123 | ```solidity 124 | function isAcceptableSchnorrSignatureNow( 125 | bytes32 message, 126 | SchnorrData calldata schnorrData 127 | ) external view returns (bool ok); 128 | ``` 129 | 130 | 4. If Schnorr signature is not acceptable: 131 | ```solidity 132 | function opChallenge(SchnorrData calldata schnorrData) 133 | external 134 | returns (bool ok); 135 | ``` 136 | 137 | 5. ETH Challenge reward can be checked beforehand: 138 | ```solidity 139 | function challengeReward() external view returns (uint challengeReward); 140 | ``` 141 | 142 | ## Benchmarks 143 | 144 | Benchmarks can be found in [`./Benchmarks.md`](./Benchmarks.md). 145 | -------------------------------------------------------------------------------- /src/IScribeOptimistic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {IScribe} from "./IScribe.sol"; 5 | 6 | interface IScribeOptimistic is IScribe { 7 | /// @notice Thrown if attempted to opPoke while a previous opPoke is still 8 | /// in challenge period. 9 | error InChallengePeriod(); 10 | 11 | /// @notice Thrown if opChallenge called while no challengeable opPoke exists. 12 | error NoOpPokeToChallenge(); 13 | 14 | /// @notice Thrown if opChallenge called with SchnorrData not matching 15 | /// opPoke's SchnorrData. 16 | /// @param gotHash The truncated keccak256 hash of the SchnorrData argument. 17 | /// @param wantHash The truncated expected keccak256 hash of the SchnorrData 18 | /// argument. 19 | error SchnorrDataMismatch(uint160 gotHash, uint160 wantHash); 20 | 21 | /// @notice Thrown if opPoke called with non-feed ECDSA signature. 22 | /// @param signer The ECDSA signature's signer. 23 | error SignerNotFeed(address signer); 24 | 25 | /// @notice Emitted when oracles was successfully opPoked. 26 | /// @param caller The caller's address. 27 | /// @param opFeed The feed that signed the opPoke. 28 | /// @param schnorrData The schnorrData opPoked. 29 | /// @param pokeData The pokeData opPoked. 30 | event OpPoked( 31 | address indexed caller, 32 | address indexed opFeed, 33 | IScribe.SchnorrData schnorrData, 34 | IScribe.PokeData pokeData 35 | ); 36 | 37 | /// @notice Emitted when successfully challenged an opPoke. 38 | /// @param caller The caller's address. 39 | /// @param schnorrData The schnorrData challenged. 40 | /// @param schnorrErr The abi-encoded custom error returned from the failed 41 | /// Schnorr signature verification. 42 | event OpPokeChallengedSuccessfully( 43 | address indexed caller, 44 | IScribe.SchnorrData schnorrData, 45 | bytes schnorrErr 46 | ); 47 | 48 | /// @notice Emitted when unsuccessfully challenged an opPoke. 49 | /// @param caller The caller's address. 50 | /// @param schnorrData The schnorrData challenged. 51 | event OpPokeChallengedUnsuccessfully( 52 | address indexed caller, IScribe.SchnorrData schnorrData 53 | ); 54 | 55 | /// @notice Emitted when ETH reward paid for successfully challenging an 56 | /// opPoke. 57 | /// @param challenger The challenger to which the reward was send. 58 | /// @param schnorrData The schnorrData challenged. 59 | /// @param reward The ETH rewards paid. 60 | event OpChallengeRewardPaid( 61 | address indexed challenger, IScribe.SchnorrData schnorrData, uint reward 62 | ); 63 | 64 | /// @notice Emitted when an opPoke dropped. 65 | /// @dev opPoke's are dropped if security parameters are updated that could 66 | /// lead to an initially valid opPoke becoming invalid or if an opPoke 67 | /// was successfully challenged. 68 | /// @param caller The caller's address. 69 | /// @param pokeData The pokeData dropped. 70 | event OpPokeDataDropped(address indexed caller, IScribe.PokeData pokeData); 71 | 72 | /// @notice Emitted when length of opChallengePeriod updated. 73 | /// @param caller The caller's address. 74 | /// @param oldOpChallengePeriod The old opChallengePeriod's length. 75 | /// @param newOpChallengePeriod The new opChallengePeriod's length. 76 | event OpChallengePeriodUpdated( 77 | address indexed caller, 78 | uint16 oldOpChallengePeriod, 79 | uint16 newOpChallengePeriod 80 | ); 81 | 82 | /// @notice Emitted when maxChallengeReward updated. 83 | /// @param caller The caller's address. 84 | /// @param oldMaxChallengeReward The old maxChallengeReward. 85 | /// @param newMaxChallengeReward The new maxChallengeReward. 86 | event MaxChallengeRewardUpdated( 87 | address indexed caller, 88 | uint oldMaxChallengeReward, 89 | uint newMaxChallengeReward 90 | ); 91 | 92 | /// @notice Optimistically pokes the oracle. 93 | /// @dev Expects `pokeData`'s age to be greater than the timestamp of the 94 | /// last successful poke. 95 | /// @dev Expects `pokeData`'s age to not be greater than the current time. 96 | /// @dev Expects `ecdsaData` to be a signature from a feed. 97 | /// @dev Expects `ecdsaData` to prove the integrity of the `pokeData` and 98 | /// `schnorrData`. 99 | /// @dev If the `schnorrData` is proven to be invalid via the opChallenge 100 | /// function, the `ecdsaData` signing feed will be dropped. 101 | /// @param pokeData The PokeData being poked. 102 | /// @param schnorrData The SchnorrData optimistically assumed to be 103 | /// proving the `pokeData`'s integrity. 104 | /// @param ecdsaData The ECDSAData proving the integrity of the 105 | /// `pokeData` and `schnorrData`. 106 | function opPoke( 107 | PokeData calldata pokeData, 108 | SchnorrData calldata schnorrData, 109 | ECDSAData calldata ecdsaData 110 | ) external; 111 | 112 | /// @notice Challenges the current challengeable opPoke. 113 | /// @dev If opPoke is determined to be invalid, the caller receives an ETH 114 | /// bounty. The bounty is defined via the `challengeReward()(uint)` 115 | /// function. 116 | /// @dev If opPoke is determined to be invalid, the corresponding feed is 117 | /// dropped. 118 | /// @param schnorrData The SchnorrData initially provided via 119 | /// opPoke. 120 | /// @return ok True if opPoke declared invalid, false otherwise. 121 | function opChallenge(SchnorrData calldata schnorrData) 122 | external 123 | returns (bool ok); 124 | 125 | /// @notice Returns the message expected to be signed via ECDSA for calling 126 | /// opPoke. 127 | /// @dev The message is defined as: 128 | /// H(tag ‖ H(wat ‖ pokeData ‖ schnorrData)), where H() is the keccak256 function. 129 | /// @param pokeData The pokeData being optimistically poked. 130 | /// @param schnorrData The schnorrData proving `pokeData`'s integrity. 131 | /// @return opPokeMessage Message to be signed for an opPoke for `pokeData` 132 | /// and `schnorrData`. 133 | function constructOpPokeMessage( 134 | PokeData calldata pokeData, 135 | SchnorrData calldata schnorrData 136 | ) external view returns (bytes32 opPokeMessage); 137 | 138 | /// @notice Returns the feed id of the feed last opPoke'd. 139 | /// @return opFeedId Feed id of the feed last opPoke'd. 140 | function opFeedId() external view returns (uint8 opFeedId); 141 | 142 | /// @notice Returns the opChallengePeriod security parameter. 143 | /// @return opChallengePeriod The opChallengePeriod security parameter. 144 | function opChallengePeriod() 145 | external 146 | view 147 | returns (uint16 opChallengePeriod); 148 | 149 | /// @notice Returns the maxChallengeRewards parameter. 150 | /// @return maxChallengeReward The maxChallengeReward parameter. 151 | function maxChallengeReward() 152 | external 153 | view 154 | returns (uint maxChallengeReward); 155 | 156 | /// @notice Returns the ETH rewards being paid for successfully challenging 157 | /// an opPoke. 158 | /// @return challengeReward The ETH reward for successfully challenging an 159 | /// opPoke. 160 | function challengeReward() external view returns (uint challengeReward); 161 | 162 | /// @notice Updates the opChallengePeriod security parameter. 163 | /// @dev Only callable by auth'ed address. 164 | /// @dev Reverts if opChallengePeriod is zero. 165 | /// @dev Note that evaluating whether an opPoke is finalized happens via the 166 | /// _current_ opChallengePeriod. 167 | /// This means a finalized opPoke is dropped if opChallengePeriod is 168 | /// decreased to a value less than opPoke's age. 169 | /// @param opChallengePeriod The value to update opChallengePeriod to. 170 | function setOpChallengePeriod(uint16 opChallengePeriod) external; 171 | 172 | /// @notice Updates the maxChallengeReward parameter. 173 | /// @dev Only callable by auth'ed address. 174 | /// @param maxChallengeReward The value to update maxChallengeReward to. 175 | function setMaxChallengeReward(uint maxChallengeReward) external; 176 | } 177 | -------------------------------------------------------------------------------- /test/rescue/RescuerTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {console2} from "forge-std/console2.sol"; 6 | 7 | import {IAuth} from "chronicle-std/auth/IAuth.sol"; 8 | 9 | import {Rescuer} from "script/rescue/Rescuer.sol"; 10 | 11 | import {IScribe} from "src/IScribe.sol"; 12 | import {IScribeOptimistic} from "src/IScribeOptimistic.sol"; 13 | import {ScribeOptimistic} from "src/ScribeOptimistic.sol"; 14 | 15 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 16 | 17 | import {LibFeed} from "script/libs/LibFeed.sol"; 18 | 19 | contract RescuerTest is Test { 20 | using LibSecp256k1 for LibSecp256k1.Point; 21 | using LibFeed for LibFeed.Feed; 22 | 23 | // Events copied from Rescuer. 24 | event Recovered( 25 | address indexed caller, address indexed opScribe, uint amount 26 | ); 27 | event Withdrawed( 28 | address indexed caller, address indexed receiver, uint amount 29 | ); 30 | 31 | Rescuer private rescuer; 32 | IScribeOptimistic private opScribe; 33 | 34 | bytes32 internal FEED_REGISTRATION_MESSAGE; 35 | 36 | function setUp() public { 37 | opScribe = new ScribeOptimistic(address(this), bytes32("TEST/TEST")); 38 | 39 | rescuer = new Rescuer(address(this)); 40 | 41 | // Note to auth rescuer on opScribe. 42 | IAuth(address(opScribe)).rely(address(rescuer)); 43 | 44 | // Note to let opScribe have a non-zero ETH balance. 45 | vm.deal(address(opScribe), 1 ether); 46 | 47 | // Cache constants. 48 | FEED_REGISTRATION_MESSAGE = opScribe.feedRegistrationMessage(); 49 | } 50 | 51 | // -- Test: Suck -- 52 | 53 | function testFuzz_suck(uint privKeySeed) public { 54 | // Create new feed from privKeySeed. 55 | LibFeed.Feed memory feed = 56 | LibFeed.newFeed(_bound(privKeySeed, 1, LibSecp256k1.Q() - 1)); 57 | 58 | // Construct opPoke signature with invalid Schnorr signature. 59 | uint32 pokeDataAge = uint32(block.timestamp); 60 | IScribe.ECDSAData memory opPokeSig = 61 | _constructOpPokeSig(feed, pokeDataAge); 62 | 63 | vm.expectEmit(); 64 | emit Recovered(address(this), address(opScribe), 1 ether); 65 | 66 | rescuer.suck( 67 | address(opScribe), 68 | feed.pubKey, 69 | feed.signECDSA(FEED_REGISTRATION_MESSAGE), 70 | pokeDataAge, 71 | opPokeSig 72 | ); 73 | 74 | // Verify balances. 75 | assertEq(address(opScribe).balance, 0); 76 | assertEq(address(rescuer).balance, 1 ether); 77 | 78 | // Verify feed got kicked. 79 | assertFalse(opScribe.feeds(feed.pubKey.toAddress())); 80 | } 81 | 82 | function testFuzz_suck_FailsIf_RescuerNotAuthedOnOpScribe(uint privKeySeed) 83 | public 84 | { 85 | // Create new feed from privKeySeed. 86 | LibFeed.Feed memory feed = 87 | LibFeed.newFeed(_bound(privKeySeed, 1, LibSecp256k1.Q() - 1)); 88 | 89 | // Construct opPoke signature with invalid Schnorr signature. 90 | uint32 pokeDataAge = uint32(block.timestamp); 91 | IScribe.ECDSAData memory opPokeSig = 92 | _constructOpPokeSig(feed, pokeDataAge); 93 | 94 | // Deny rescuer on opScribe. 95 | IAuth(address(opScribe)).deny(address(rescuer)); 96 | 97 | // Expect rescue to fail. 98 | vm.expectRevert(); 99 | rescuer.suck( 100 | address(opScribe), 101 | feed.pubKey, 102 | feed.signECDSA(FEED_REGISTRATION_MESSAGE), 103 | pokeDataAge, 104 | opPokeSig 105 | ); 106 | } 107 | 108 | function testFuzz_suck_FailsIf_OpScribeNotDeactivated( 109 | uint privKeySeed, 110 | uint privKeyLiftedSeed 111 | ) public { 112 | // Create new feeds from seeds 113 | LibFeed.Feed memory feed = 114 | LibFeed.newFeed(_bound(privKeySeed, 1, LibSecp256k1.Q() - 1)); 115 | LibFeed.Feed memory feedLifted = 116 | LibFeed.newFeed(_bound(privKeyLiftedSeed, 1, LibSecp256k1.Q() - 1)); 117 | 118 | // Lift feedLifted. 119 | opScribe.lift( 120 | feedLifted.pubKey, 121 | feedLifted.signECDSA(opScribe.feedRegistrationMessage()) 122 | ); 123 | 124 | // Construct opPoke signature with invalid Schnorr signature. 125 | uint32 pokeDataAge = uint32(block.timestamp); 126 | IScribe.ECDSAData memory opPokeSig = 127 | _constructOpPokeSig(feed, pokeDataAge); 128 | 129 | // Expect rescue to fail. 130 | vm.expectRevert(); 131 | rescuer.suck( 132 | address(opScribe), 133 | feed.pubKey, 134 | feed.signECDSA(FEED_REGISTRATION_MESSAGE), 135 | pokeDataAge, 136 | opPokeSig 137 | ); 138 | } 139 | 140 | function test_suck_isAuthProtected() public { 141 | vm.prank(address(0xbeef)); 142 | vm.expectRevert( 143 | abi.encodeWithSelector( 144 | IAuth.NotAuthorized.selector, address(0xbeef) 145 | ) 146 | ); 147 | rescuer.suck( 148 | address(opScribe), 149 | LibSecp256k1.ZERO_POINT(), 150 | IScribe.ECDSAData(uint8(0), bytes32(0), bytes32(0)), 151 | uint32(0), 152 | IScribe.ECDSAData(uint8(0), bytes32(0), bytes32(0)) 153 | ); 154 | } 155 | 156 | // -- Test: Withdraw -- 157 | 158 | function testFuzz_withdraw_ToEOA( 159 | address payable receiver, 160 | uint balance, 161 | uint withdrawal 162 | ) public { 163 | vm.assume(receiver.code.length == 0); 164 | vm.assume(balance >= withdrawal); 165 | 166 | // Let rescuer have ETH balance. 167 | vm.deal(address(rescuer), balance); 168 | 169 | vm.expectEmit(); 170 | emit Withdrawed(address(this), receiver, withdrawal); 171 | 172 | rescuer.withdraw(receiver, withdrawal); 173 | 174 | assertEq(address(rescuer).balance, balance - withdrawal); 175 | assertEq(receiver.balance, withdrawal); 176 | } 177 | 178 | function test_withdraw_ToContract(uint balance, uint withdrawal) public { 179 | vm.assume(balance >= withdrawal); 180 | 181 | // Let rescuer have ETH balance. 182 | vm.deal(address(rescuer), balance); 183 | 184 | // Deploy ETH receiver. 185 | ETHReceiver receiver = new ETHReceiver(); 186 | 187 | vm.expectEmit(); 188 | emit Withdrawed(address(this), address(receiver), withdrawal); 189 | 190 | rescuer.withdraw(payable(address(receiver)), withdrawal); 191 | 192 | assertEq(address(rescuer).balance, balance - withdrawal); 193 | assertEq(address(receiver).balance, withdrawal); 194 | } 195 | 196 | function test_withdraw_FailsIf_ETHTransferFails( 197 | uint balance, 198 | uint withdrawal 199 | ) public { 200 | vm.assume(balance >= withdrawal); 201 | 202 | // Let rescuer have ETH balance. 203 | vm.deal(address(rescuer), balance); 204 | 205 | // Deploy non ETH receiver. 206 | NotETHReceiver receiver = new NotETHReceiver(); 207 | 208 | vm.expectRevert(); 209 | rescuer.withdraw(payable(address(receiver)), withdrawal); 210 | } 211 | 212 | function test_withdraw_isAuthProtected() public { 213 | vm.prank(address(0xbeef)); 214 | vm.expectRevert( 215 | abi.encodeWithSelector( 216 | IAuth.NotAuthorized.selector, address(0xbeef) 217 | ) 218 | ); 219 | rescuer.withdraw(payable(address(this)), 0); 220 | } 221 | 222 | // -- Helpers -- 223 | 224 | function _constructOpPokeSig(LibFeed.Feed memory feed, uint32 pokeDataAge) 225 | internal 226 | view 227 | returns (IScribe.ECDSAData memory) 228 | { 229 | // Construct pokeData with zero val and given age. 230 | IScribe.PokeData memory pokeData = IScribe.PokeData(0, pokeDataAge); 231 | 232 | // Construct invalid Schnorr signature. 233 | IScribe.SchnorrData memory schnorrData = 234 | IScribe.SchnorrData(bytes32(0), address(0), hex""); 235 | 236 | // Construct opPokeMessage. 237 | bytes32 opPokeMessage = 238 | opScribe.constructOpPokeMessage(pokeData, schnorrData); 239 | 240 | // Let feed sign opPokeMessage. 241 | return feed.signECDSA(opPokeMessage); 242 | } 243 | } 244 | 245 | contract NotETHReceiver {} 246 | 247 | contract ETHReceiver { 248 | receive() external payable {} 249 | } 250 | -------------------------------------------------------------------------------- /script/benchmarks/ScribeOptimisticBenchmark.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {console2} from "forge-std/console2.sol"; 6 | 7 | import {IScribe} from "src/IScribe.sol"; 8 | import {Scribe} from "src/Scribe.sol"; 9 | 10 | import {ScribeOptimistic} from "src/ScribeOptimistic.sol"; 11 | 12 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 13 | 14 | import {LibFeed} from "script/libs/LibFeed.sol"; 15 | 16 | /** 17 | * @notice Scribe Optimistic Benchmark Script 18 | * 19 | * @dev Usage: 20 | * 1. Open new terminal and start anvil via: 21 | * $ anvil -b 1 22 | * 23 | * 2. Deploy contract via: 24 | * $ forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "deploy()" 25 | * 26 | * 3. Set bar via: 27 | * $ BAR=10 # Note to update to appropriate value 28 | * $ forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig $(cast calldata "setBar(uint8)" $BAR) 29 | * 30 | * 4. Lift feeds via: 31 | * $ forge script script/benchmarks/ScribeBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "liftFeeds()" 32 | * 33 | * 5. Set opChallengePeriod via: 34 | * $ OP_CHALLENGE_PERIOD=1 # Note to update to appropriate value 35 | * $ forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig $(cast calldata "setOpChallengePeriod(uint16)" $OP_CHALLENGE_PERIOD)" 36 | * 37 | * 6. Poke via: 38 | * $ forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "poke()" 39 | * 40 | * 7. opPoke via: 41 | * $ forge script script/benchmarks/ScribeOptimisticBenchmark.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig "opPoke()" 42 | * 43 | * Note to (op)Poke more than once to get realistic gas costs. 44 | * During the first execution the storage slots are empty. 45 | */ 46 | contract ScribeOptimisticBenchmark is Script { 47 | using LibFeed for LibFeed.Feed; 48 | using LibFeed for LibFeed.Feed[]; 49 | 50 | /// @dev Anvil's default mnemonic. 51 | string internal constant ANVIL_MNEMONIC = 52 | "test test test test test test test test test test test junk"; 53 | 54 | ScribeOptimistic opScribe = ScribeOptimistic( 55 | payable(address(0x5FbDB2315678afecb367f032d93F642f64180aa3)) 56 | ); 57 | 58 | function deploy() public { 59 | uint deployer = vm.deriveKey(ANVIL_MNEMONIC, uint32(0)); 60 | 61 | vm.broadcast(deployer); 62 | opScribe = new ScribeOptimistic(vm.addr(deployer), "ETH/USD"); 63 | } 64 | 65 | function setBar(uint8 bar) public { 66 | uint deployer = vm.deriveKey(ANVIL_MNEMONIC, uint32(0)); 67 | 68 | vm.broadcast(deployer); 69 | opScribe.setBar(bar); 70 | } 71 | 72 | function setOpChallengePeriod(uint16 opChallengePeriod) public { 73 | uint deployer = vm.deriveKey(ANVIL_MNEMONIC, uint32(0)); 74 | 75 | // Note to set opChallengePeriod to small value. 76 | vm.broadcast(deployer); 77 | opScribe.setOpChallengePeriod(opChallengePeriod); 78 | } 79 | 80 | function liftFeeds() public { 81 | uint deployer = vm.deriveKey(ANVIL_MNEMONIC, uint32(0)); 82 | 83 | // Create bar many feeds. 84 | LibFeed.Feed[] memory feeds = _createFeeds(opScribe.bar()); 85 | 86 | // Create list of feeds' public keys and ECDSA signatures. 87 | LibSecp256k1.Point[] memory pubKeys = 88 | new LibSecp256k1.Point[](feeds.length); 89 | IScribe.ECDSAData[] memory sigs = new IScribe.ECDSAData[](feeds.length); 90 | for (uint i; i < feeds.length; i++) { 91 | pubKeys[i] = feeds[i].pubKey; 92 | sigs[i] = feeds[i].signECDSA(opScribe.feedRegistrationMessage()); 93 | } 94 | 95 | // Lift feeds. 96 | vm.broadcast(deployer); 97 | opScribe.lift(pubKeys, sigs); 98 | } 99 | 100 | function poke() public { 101 | uint relay = vm.deriveKey(ANVIL_MNEMONIC, uint32(1)); 102 | 103 | // Create bar many feeds. 104 | LibFeed.Feed[] memory feeds = _createFeeds(opScribe.bar()); 105 | 106 | // Create list of feeds' public keys. 107 | LibSecp256k1.Point[] memory pubKeys = 108 | new LibSecp256k1.Point[](feeds.length); 109 | for (uint i; i < feeds.length; i++) { 110 | pubKeys[i] = feeds[i].pubKey; 111 | } 112 | 113 | // Create pokeData. 114 | // Note to use max value for val to have highest possible gas costs. 115 | IScribe.PokeData memory pokeData = IScribe.PokeData({ 116 | val: type(uint128).max, 117 | age: uint32(block.timestamp) 118 | }); 119 | 120 | // Create schnorrData. 121 | IScribe.SchnorrData memory schnorrData; 122 | schnorrData = feeds.signSchnorr(opScribe.constructPokeMessage(pokeData)); 123 | 124 | // Execute poke. 125 | vm.broadcast(relay); 126 | opScribe.poke(pokeData, schnorrData); 127 | } 128 | 129 | function opPoke() public { 130 | uint relay = vm.deriveKey(ANVIL_MNEMONIC, uint32(1)); 131 | 132 | // Create bar many feeds. 133 | LibFeed.Feed[] memory feeds = _createFeeds(opScribe.bar()); 134 | 135 | // Create pokeData. 136 | // Note to use max value for val to have highest possible gas costs. 137 | IScribe.PokeData memory pokeData = IScribe.PokeData({ 138 | val: type(uint128).max, 139 | age: uint32(block.timestamp) 140 | }); 141 | 142 | // Create schnorrData. 143 | IScribe.SchnorrData memory schnorrData; 144 | schnorrData = feeds.signSchnorr(opScribe.constructPokeMessage(pokeData)); 145 | 146 | // Create ecdsaData. 147 | IScribe.ECDSAData memory ecdsaData; 148 | ecdsaData = feeds[0].signECDSA( 149 | opScribe.constructOpPokeMessage(pokeData, schnorrData) 150 | ); 151 | 152 | // Execute opPoke. 153 | vm.broadcast(relay); 154 | opScribe.opPoke(pokeData, schnorrData, ecdsaData); 155 | } 156 | 157 | function opPokeInvalidAndChallenge() public { 158 | uint relay = vm.deriveKey(ANVIL_MNEMONIC, uint32(1)); 159 | uint challenger = vm.deriveKey(ANVIL_MNEMONIC, uint32(2)); 160 | 161 | // Create bar many feeds. 162 | LibFeed.Feed[] memory feeds = _createFeeds(opScribe.bar()); 163 | 164 | // Create pokeData. 165 | // Note to use max value for val to have highest possible gas costs. 166 | IScribe.PokeData memory pokeData = IScribe.PokeData({ 167 | val: type(uint128).max, 168 | age: uint32(block.timestamp) 169 | }); 170 | 171 | // Create schnorrData. 172 | IScribe.SchnorrData memory schnorrData; 173 | schnorrData = feeds.signSchnorr(opScribe.constructPokeMessage(pokeData)); 174 | 175 | // Mutate pokeData to make Schnorr signature invalid. 176 | // Note to mutate before creating ECDSA signature. 177 | pokeData.val -= 1; 178 | 179 | // Create ecdsaData. 180 | IScribe.ECDSAData memory ecdsaData; 181 | ecdsaData = feeds[0].signECDSA( 182 | opScribe.constructOpPokeMessage(pokeData, schnorrData) 183 | ); 184 | 185 | // Execute opPoke. 186 | vm.broadcast(relay); 187 | opScribe.opPoke(pokeData, schnorrData, ecdsaData); 188 | 189 | // Execute opChallenge. 190 | vm.broadcast(challenger); 191 | opScribe.opChallenge(schnorrData); 192 | } 193 | 194 | function _createFeeds(uint numberFeeds) 195 | internal 196 | returns (LibFeed.Feed[] memory) 197 | { 198 | LibFeed.Feed[] memory feeds = new LibFeed.Feed[](numberFeeds); 199 | 200 | // Note to not start with privKey=1. This is because the sum of public 201 | // keys would evaluate to: 202 | // pubKeyOf(1) + pubKeyOf(2) + pubKeyOf(3) + ... 203 | // = pubKeyOf(3) + pubKeyOf(3) + ... 204 | // Note that pubKeyOf(3) would be doubled. Doubling is not supported by 205 | // LibSecp256k1 as this would indicate a double-signing attack. 206 | uint privKey = 2; 207 | uint bloom; 208 | uint ctr; 209 | while (ctr != numberFeeds) { 210 | LibFeed.Feed memory feed = LibFeed.newFeed({privKey: privKey}); 211 | 212 | // Check whether feed with id already created, if not create. 213 | if (bloom & (1 << feed.id) == 0) { 214 | bloom |= 1 << feed.id; 215 | 216 | feeds[ctr++] = feed; 217 | } 218 | 219 | privKey++; 220 | } 221 | 222 | return feeds; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /script/libs/LibSchnorrExtended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {console2} from "forge-std/console2.sol"; 5 | import {StdStyle} from "forge-std/StdStyle.sol"; 6 | 7 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 8 | 9 | import {LibSecp256k1Extended} from "script/libs/LibSecp256k1Extended.sol"; 10 | 11 | /** 12 | * @title LibSchnorrExtended 13 | * 14 | * @notice Extended library for Schnorr signatures as specified in 15 | * `docs/Schnorr.md` 16 | */ 17 | library LibSchnorrExtended { 18 | using LibSecp256k1 for LibSecp256k1.Point; 19 | using LibSecp256k1 for LibSecp256k1.JacobianPoint; 20 | using LibSecp256k1Extended for uint; 21 | 22 | /// @dev Returns a Schnorr signature of type (signature, commitment) signing 23 | /// `message` via `privKey`. 24 | function signMessage(uint privKey, bytes32 message) 25 | internal 26 | returns (uint, address) 27 | { 28 | LibSecp256k1.Point memory pubKey = privKey.derivePublicKey(); 29 | 30 | // 1. Select secure nonce. 31 | uint nonce = deriveNonce(privKey, message); 32 | 33 | // 2. Compute noncePubKey. 34 | LibSecp256k1.Point memory noncePubKey = computeNoncePublicKey(nonce); 35 | 36 | // 3. Derive commitment from noncePubKey. 37 | address commitment = deriveCommitment(noncePubKey); 38 | 39 | // 4. Construct challenge. 40 | bytes32 challenge = constructChallenge(pubKey, message, commitment); 41 | 42 | // 5. Compute signature. 43 | uint signature = computeSignature(privKey, nonce, challenge); 44 | 45 | // BONUS: Make sure signature can be verified. 46 | bool ok = 47 | verifySignature(pubKey, message, bytes32(signature), commitment); 48 | if (!ok) { 49 | console2.log( 50 | StdStyle.red( 51 | "[INTERNAL ERROR] LibSchnorrExtended: could not verify own signature" 52 | ) 53 | ); 54 | } 55 | 56 | // => The public key signs the message via the signature and 57 | // commitment. 58 | return (signature, commitment); 59 | } 60 | 61 | /// @dev Returns a Schnorr multi-signature (aggregated signature) of type 62 | /// (signature, commitment) signing `message` via `privKeys`. 63 | function signMessage(uint[] memory privKeys, bytes32 message) 64 | internal 65 | returns (uint, address) 66 | { 67 | // 1. Collect list of pubKeys of signers. 68 | LibSecp256k1.Point[] memory pubKeys = 69 | new LibSecp256k1.Point[](privKeys.length); 70 | for (uint i; i < privKeys.length; i++) { 71 | pubKeys[i] = privKeys[i].derivePublicKey(); 72 | } 73 | 74 | // 2. Compute aggPubKey. 75 | LibSecp256k1.Point memory aggPubKey; 76 | aggPubKey = aggregatePublicKeys(pubKeys); 77 | 78 | // 3. Collect list of noncePubKeys from signers. 79 | LibSecp256k1.Point[] memory noncePubKeys = 80 | new LibSecp256k1.Point[](privKeys.length); 81 | for (uint i; i < privKeys.length; i++) { 82 | // 3.1. Derive secure nonce. 83 | uint nonce = deriveNonce(privKeys[i], message); 84 | 85 | // 3.2. Compute noncePubKey and append to list of noncePubKeys. 86 | noncePubKeys[i] = computeNoncePublicKey(nonce); 87 | } 88 | 89 | // 4. Compute aggNoncePubKey. 90 | LibSecp256k1.Point memory aggNoncePubKey; 91 | aggNoncePubKey = aggregatePublicKeys(noncePubKeys); 92 | 93 | // 5. Derive commitment from aggNoncePubKey. 94 | address commitment = deriveCommitment(aggNoncePubKey); 95 | 96 | // 6. Construct challenge. 97 | bytes32 challenge = constructChallenge(aggPubKey, message, commitment); 98 | 99 | // 7. Collect signatures from signers. 100 | uint[] memory signatures = new uint[](privKeys.length); 101 | for (uint i; i < privKeys.length; i++) { 102 | // 7.1 Derive secure nonce. 103 | uint nonce = deriveNonce(privKeys[i], message); 104 | 105 | // 7.2 Compute signature. 106 | signatures[i] = computeSignature(privKeys[i], nonce, challenge); 107 | } 108 | 109 | // 8. Compute aggSignature. 110 | uint aggSignature; 111 | for (uint i; i < privKeys.length; i++) { 112 | // Note to keep aggSignature ∊ [0, Q). 113 | aggSignature = addmod(aggSignature, signatures[i], LibSecp256k1.Q()); 114 | } 115 | 116 | // BONUS: Make sure signature can be verified. 117 | bool ok = verifySignature( 118 | aggPubKey, message, bytes32(aggSignature), commitment 119 | ); 120 | if (!ok) { 121 | console2.log( 122 | StdStyle.red( 123 | "[INTERNAL ERROR] LibSchnorrExtended: could not verify own signature" 124 | ) 125 | ); 126 | } 127 | 128 | // => The aggregated public key signs the message via the aggregated 129 | // signature and commitment. 130 | return (aggSignature, commitment); 131 | } 132 | 133 | function verifySignature( 134 | LibSecp256k1.Point memory pubKey, 135 | bytes32 message, 136 | bytes32 signature, 137 | address commitment 138 | ) internal pure returns (bool) { 139 | if (commitment == address(0) || signature == 0) { 140 | return false; 141 | } 142 | 143 | uint challenge = uint(constructChallenge(pubKey, message, commitment)); 144 | 145 | // Compute msgHash = -sig * Pₓ (mod Q) 146 | // = Q - (sig * Pₓ) (mod Q) 147 | uint msgHash = LibSecp256k1.Q() 148 | - mulmod(uint(signature), pubKey.x, LibSecp256k1.Q()); 149 | 150 | // Compute v = Pₚ + 27 151 | uint v = pubKey.yParity() + 27; 152 | 153 | // Set r = Pₓ 154 | uint r = pubKey.x; 155 | 156 | // Compute s = Q - (e * Pₓ) (mod Q) 157 | uint s = 158 | LibSecp256k1.Q() - mulmod(challenge, pubKey.x, LibSecp256k1.Q()); 159 | 160 | // Perform ecrecover call. 161 | // Note to perform necessary castings. 162 | address recovered = 163 | ecrecover(bytes32(msgHash), uint8(v), bytes32(r), bytes32(s)); 164 | 165 | // Verification succeeds iff the ecrecover'ed address equals Rₑ, i.e. 166 | // the commitment. 167 | return commitment == recovered; 168 | } 169 | 170 | // -- Low-Level Functions -- 171 | 172 | function deriveNonce(uint privKey, bytes32 message) 173 | internal 174 | pure 175 | returns (uint) 176 | { 177 | // k = H(x ‖ m) mod Q 178 | return uint(keccak256(abi.encodePacked(privKey, message))) 179 | % LibSecp256k1.Q(); 180 | } 181 | 182 | function computeNoncePublicKey(uint nonce) 183 | internal 184 | returns (LibSecp256k1.Point memory) 185 | { 186 | // R = [k]G 187 | return nonce.derivePublicKey(); 188 | } 189 | 190 | function deriveCommitment(LibSecp256k1.Point memory noncePubKey) 191 | internal 192 | pure 193 | returns (address) 194 | { 195 | // Rₑ = Ethereum address of R. 196 | return noncePubKey.toAddress(); 197 | } 198 | 199 | function aggregatePublicKeys(LibSecp256k1.Point[] memory pubKeys) 200 | internal 201 | pure 202 | returns (LibSecp256k1.Point memory) 203 | { 204 | // aggPubKey = sum(signers) 205 | // = pubKey₁ + pubKey₂ + ... + pubKeyₙ 206 | require(pubKeys.length != 0); 207 | 208 | LibSecp256k1.JacobianPoint memory aggPubKey = pubKeys[0].toJacobian(); 209 | 210 | for (uint i = 1; i < pubKeys.length; i++) { 211 | aggPubKey.addAffinePoint(pubKeys[i]); 212 | } 213 | 214 | return aggPubKey.toAffine(); 215 | } 216 | 217 | function constructChallenge( 218 | LibSecp256k1.Point memory pubKey, 219 | bytes32 message, 220 | address commitment 221 | ) internal pure returns (bytes32) { 222 | // e = H(Pₓ ‖ Pₚ ‖ m ‖ Rₑ) mod Q 223 | return bytes32( 224 | uint( 225 | keccak256( 226 | abi.encodePacked( 227 | pubKey.x, uint8(pubKey.yParity()), message, commitment 228 | ) 229 | ) 230 | ) % LibSecp256k1.Q() 231 | ); 232 | } 233 | 234 | function computeSignature(uint privKey, uint nonce, bytes32 challenge) 235 | internal 236 | pure 237 | returns (uint) 238 | { 239 | // s = k + (e * x) (mod Q) 240 | return addmod( 241 | nonce, 242 | mulmod(uint(challenge), privKey, LibSecp256k1.Q()), 243 | LibSecp256k1.Q() 244 | ); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /script/dev/ScribeOptimisticTester.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {console2} from "forge-std/console2.sol"; 5 | 6 | import {IScribe} from "src/IScribe.sol"; 7 | import {IScribeOptimistic} from "src/IScribeOptimistic.sol"; 8 | 9 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 10 | 11 | import {ScribeTesterScript} from "./ScribeTester.s.sol"; 12 | 13 | import {LibFeed} from "../libs/LibFeed.sol"; 14 | 15 | /** 16 | * @notice ScribeOptimistic Tester Script 17 | * 18 | * @dev !!! IMPORTANT !!! 19 | * 20 | * This script may only be used for dev deployments! 21 | */ 22 | contract ScribeOptimisticTesterScript is ScribeTesterScript { 23 | using LibSecp256k1 for LibSecp256k1.Point; 24 | using LibFeed for LibFeed.Feed; 25 | using LibFeed for LibFeed.Feed[]; 26 | 27 | /// @dev opPokes `self` with val `val` and current timestamp signed by set of 28 | /// private keys `privKeys`. Note that a random private key is selected 29 | /// to opPoke. 30 | /// 31 | /// @dev Call via: 32 | /// 33 | /// ```bash 34 | /// $ forge script \ 35 | /// --private-key $PRIVATE_KEY \ 36 | /// --broadcast \ 37 | /// --rpc-url $RPC_URL \ 38 | /// --sig $(cast calldata "opPoke(address,uint[],uint128)" $SCRIBE $TEST_FEED_SIGNERS_PRIVATE_KEYS $TEST_POKE_VAL) \ 39 | /// -vvv \ 40 | /// script/dev/ScribeOptimisticTester.s.sol:ScribeOptimisticTesterScript 41 | /// ``` 42 | function opPoke(address self, uint[] memory privKeys, uint128 val) public { 43 | require(privKeys.length != 0, "No private keys given"); 44 | 45 | // Setup feeds. 46 | LibFeed.Feed[] memory feeds = new LibFeed.Feed[](privKeys.length); 47 | for (uint i; i < feeds.length; i++) { 48 | feeds[i] = LibFeed.newFeed({privKey: privKeys[i]}); 49 | 50 | vm.label( 51 | feeds[i].pubKey.toAddress(), 52 | string.concat("Feed #", vm.toString(i + 1)) 53 | ); 54 | 55 | // Verify feed is lifted. 56 | bool isFeed = IScribe(self).feeds(feeds[i].pubKey.toAddress()); 57 | require( 58 | isFeed, 59 | string.concat( 60 | "Private key not feed, privKey=", vm.toString(privKeys[i]) 61 | ) 62 | ); 63 | } 64 | 65 | // Create poke data. 66 | IScribe.PokeData memory pokeData; 67 | pokeData.val = val; 68 | pokeData.age = uint32(block.timestamp); 69 | 70 | // Construct poke message. 71 | bytes32 pokeMessage = IScribe(self).constructPokeMessage(pokeData); 72 | 73 | // Create Schnorr data proving poke message's integrity. 74 | IScribe.SchnorrData memory schnorrData = feeds.signSchnorr(pokeMessage); 75 | 76 | // Use "random" feed to opPoke. 77 | LibFeed.Feed memory signer = feeds[val % feeds.length]; 78 | 79 | IScribe.ECDSAData memory ecdsaData; 80 | ecdsaData = signer.signECDSA( 81 | IScribeOptimistic(self).constructOpPokeMessage( 82 | pokeData, schnorrData 83 | ) 84 | ); 85 | 86 | vm.startBroadcast(); 87 | IScribeOptimistic(self).opPoke(pokeData, schnorrData, ecdsaData); 88 | vm.stopBroadcast(); 89 | 90 | console2.log( 91 | string.concat( 92 | "opPoked, val=", 93 | vm.toString(pokeData.val), 94 | ", age=", 95 | vm.toString(pokeData.age) 96 | ) 97 | ); 98 | } 99 | 100 | /// @dev opPokes `self` with invalid signature for val `val` and current timestamp. 101 | /// Note that a random private key is selected to opPoke. 102 | /// 103 | /// @dev Call via: 104 | /// 105 | /// ```bash 106 | /// $ forge script \ 107 | /// --private-key $PRIVATE_KEY \ 108 | /// --broadcast \ 109 | /// --rpc-url $RPC_URL \ 110 | /// --sig $(cast calldata "opPoke_invalid(address,uint[],uint128)" $SCRIBE $TEST_FEED_SIGNERS_PRIVATE_KEYS $TEST_POKE_VAL) \ 111 | /// -vvv \ 112 | /// script/dev/ScribeOptimisticTester.s.sol:ScribeOptimisticTesterScript 113 | /// ``` 114 | function opPoke_invalid(address self, uint[] memory privKeys, uint128 val) 115 | public 116 | { 117 | require(privKeys.length != 0, "No private keys given"); 118 | 119 | // Setup feeds. 120 | LibFeed.Feed[] memory feeds = new LibFeed.Feed[](privKeys.length); 121 | for (uint i; i < feeds.length; i++) { 122 | feeds[i] = LibFeed.newFeed({privKey: privKeys[i]}); 123 | 124 | vm.label( 125 | feeds[i].pubKey.toAddress(), 126 | string.concat("Feed #", vm.toString(i + 1)) 127 | ); 128 | 129 | // Verify feed is lifted. 130 | bool isFeed = IScribe(self).feeds(feeds[i].pubKey.toAddress()); 131 | require( 132 | isFeed, 133 | string.concat( 134 | "Private key not feed, privKey=", vm.toString(privKeys[i]) 135 | ) 136 | ); 137 | } 138 | 139 | // Create poke data. 140 | IScribe.PokeData memory pokeData; 141 | pokeData.val = val; 142 | pokeData.age = uint32(block.timestamp); 143 | 144 | // Construct poke message. 145 | bytes32 pokeMessage = IScribe(self).constructPokeMessage(pokeData); 146 | 147 | // Create Schnorr data proving poke message's integrity. 148 | IScribe.SchnorrData memory schnorrData = feeds.signSchnorr(pokeMessage); 149 | 150 | // Mutate Schnorr data to make signature invalid. 151 | schnorrData.commitment = address(uint160(schnorrData.commitment) + 1); 152 | 153 | // Use "random" feed to opPoke. 154 | LibFeed.Feed memory signer = feeds[val % feeds.length]; 155 | 156 | IScribe.ECDSAData memory ecdsaData; 157 | ecdsaData = signer.signECDSA( 158 | IScribeOptimistic(self).constructOpPokeMessage( 159 | pokeData, schnorrData 160 | ) 161 | ); 162 | 163 | vm.startBroadcast(); 164 | IScribeOptimistic(self).opPoke(pokeData, schnorrData, ecdsaData); 165 | vm.stopBroadcast(); 166 | 167 | console2.log( 168 | string.concat( 169 | "opPoked, val=", 170 | vm.toString(pokeData.val), 171 | ", age=", 172 | vm.toString(pokeData.age) 173 | ) 174 | ); 175 | } 176 | 177 | /// @dev opChallenges `self`'s poke data `pokeData` and Schnorr signature 178 | /// `schnorrData`. 179 | /// 180 | /// @dev Call via: 181 | /// 182 | /// ```bash 183 | /// $ forge script \ 184 | /// --private-key $PRIVATE_KEY \ 185 | /// --broadcast \ 186 | /// --rpc-url $RPC_URL \ 187 | /// --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) \ 188 | /// -vvv \ 189 | /// script/dev/ScribeOptimisticTester.s.sol:ScribeOptimisticTesterScript 190 | /// ``` 191 | function opChallenge( 192 | address self, 193 | uint128 val, 194 | uint32 age, 195 | bytes32 schnorrSignature, 196 | address schnorrCommitment, 197 | bytes memory schnorrFeedIds 198 | ) public { 199 | // Construct pokeData and schnorrData. 200 | IScribe.PokeData memory pokeData; 201 | pokeData.val = val; 202 | pokeData.age = age; 203 | 204 | IScribe.SchnorrData memory schnorrData; 205 | schnorrData.signature = schnorrSignature; 206 | schnorrData.commitment = schnorrCommitment; 207 | schnorrData.feedIds = schnorrFeedIds; 208 | 209 | // Create poke message from pokeData. 210 | bytes32 pokeMessage = IScribe(self).constructPokeMessage(pokeData); 211 | 212 | // Check whether schnorrData is not acceptable. 213 | bool ok = IScribe(self).isAcceptableSchnorrSignatureNow( 214 | pokeMessage, schnorrData 215 | ); 216 | if (ok) { 217 | console2.log( 218 | "Schnorr signature is acceptable: expecting opChallenge to be unsuccessful" 219 | ); 220 | } else { 221 | console2.log( 222 | "Schnorr signature is unacceptable: expecting opChallenge to be successful" 223 | ); 224 | } 225 | 226 | // Challenge opPoke. 227 | vm.startBroadcast(); 228 | ok = IScribeOptimistic(self).opChallenge(schnorrData); 229 | vm.stopBroadcast(); 230 | 231 | console2.log(string.concat("OpChallenged, ok=", vm.toString(ok))); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/IScribe.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {IChronicle} from "chronicle-std/IChronicle.sol"; 5 | 6 | import {LibSecp256k1} from "./libs/LibSecp256k1.sol"; 7 | 8 | interface IScribe is IChronicle { 9 | /// @dev PokeData encapsulates a value and its age. 10 | struct PokeData { 11 | uint128 val; 12 | uint32 age; 13 | } 14 | 15 | /// @dev SchnorrData encapsulates a (aggregated) Schnorr signature. 16 | /// Schnorr signatures are used to prove a PokeData's integrity. 17 | struct SchnorrData { 18 | bytes32 signature; 19 | address commitment; 20 | bytes feedIds; 21 | } 22 | 23 | /// @dev ECDSAData encapsulates an ECDSA signature. 24 | struct ECDSAData { 25 | uint8 v; 26 | bytes32 r; 27 | bytes32 s; 28 | } 29 | 30 | /// @notice Thrown if a poked value's age is not greater than the oracle's 31 | /// current value's age. 32 | /// @param givenAge The poked value's age. 33 | /// @param currentAge The oracle's current value's age. 34 | error StaleMessage(uint32 givenAge, uint32 currentAge); 35 | 36 | /// @notice Thrown if a poked value's age is greater than the current 37 | /// time. 38 | /// @param givenAge The poked value's age. 39 | /// @param currentTimestamp The current time. 40 | error FutureMessage(uint32 givenAge, uint32 currentTimestamp); 41 | 42 | /// @notice Thrown if Schnorr signature not signed by exactly bar many 43 | /// signers. 44 | /// @param numberSigners The number of signers for given Schnorr signature. 45 | /// @param bar The bar security parameter. 46 | error BarNotReached(uint8 numberSigners, uint8 bar); 47 | 48 | /// @notice Thrown if given feed id invalid. 49 | /// @param feedId The invalid feed id. 50 | error InvalidFeedId(uint8 feedId); 51 | 52 | /// @notice Thrown if double signing attempted. 53 | /// @param feedId The id of the feed attempting to double sign. 54 | error DoubleSigningAttempted(uint8 feedId); 55 | 56 | /// @notice Thrown if Schnorr signature verification failed. 57 | error SchnorrSignatureInvalid(); 58 | 59 | /// @notice Emitted when oracle was successfully poked. 60 | /// @param caller The caller's address. 61 | /// @param val The value poked. 62 | /// @param age The age of the value poked. 63 | event Poked(address indexed caller, uint128 val, uint32 age); 64 | 65 | /// @notice Emitted when new feed lifted. 66 | /// @param caller The caller's address. 67 | /// @param feed The feed address lifted. 68 | event FeedLifted(address indexed caller, address indexed feed); 69 | 70 | /// @notice Emitted when feed dropped. 71 | /// @param caller The caller's address. 72 | /// @param feed The feed address dropped. 73 | event FeedDropped(address indexed caller, address indexed feed); 74 | 75 | /// @notice Emitted when bar updated. 76 | /// @param caller The caller's address. 77 | /// @param oldBar The old bar's value. 78 | /// @param newBar The new bar's value. 79 | event BarUpdated(address indexed caller, uint8 oldBar, uint8 newBar); 80 | 81 | /// @notice Returns the feed registration message. 82 | /// @dev This message must be signed by a feed in order to be lifted. 83 | /// @return feedRegistrationMessage Chronicle Protocol's feed registration 84 | /// message. 85 | function feedRegistrationMessage() 86 | external 87 | view 88 | returns (bytes32 feedRegistrationMessage); 89 | 90 | /// @notice Returns the bar security parameter. 91 | /// @return bar The bar security parameter. 92 | function bar() external view returns (uint8 bar); 93 | 94 | /// @notice Returns the number of decimals of the oracle's value. 95 | /// @dev Provides partial compatibility with Chainlink's 96 | /// IAggregatorV3Interface. 97 | /// @return decimals The oracle value's number of decimals. 98 | function decimals() external view returns (uint8 decimals); 99 | 100 | /// @notice Returns the oracle's latest value. 101 | /// @dev Provides partial compatibility with Chainlink's 102 | /// IAggregatorV3Interface. 103 | /// @return roundId 1. 104 | /// @return answer The oracle's latest value. 105 | /// @return startedAt 0. 106 | /// @return updatedAt The timestamp of oracle's latest update. 107 | /// @return answeredInRound 1. 108 | function latestRoundData() 109 | external 110 | view 111 | returns ( 112 | uint80 roundId, 113 | int answer, 114 | uint startedAt, 115 | uint updatedAt, 116 | uint80 answeredInRound 117 | ); 118 | 119 | /// @notice Returns the oracle's latest value. 120 | /// @dev Provides partial compatibility with Chainlink's 121 | /// IAggregatorV3Interface. 122 | /// @custom:deprecated See https://docs.chain.link/data-feeds/api-reference/#latestanswer. 123 | /// @return answer The oracle's latest value. 124 | function latestAnswer() external view returns (int); 125 | 126 | /// @notice Pokes the oracle. 127 | /// @dev Expects `pokeData`'s age to be greater than the timestamp of the 128 | /// last successful poke. 129 | /// @dev Expects `pokeData`'s age to not be greater than the current time. 130 | /// @dev Expects `schnorrData` to prove `pokeData`'s integrity. 131 | /// See `isAcceptableSchnorrSignatureNow(bytes32,SchnorrData)(bool)`. 132 | /// @param pokeData The PokeData being poked. 133 | /// @param schnorrData The SchnorrData proving the `pokeData`'s 134 | /// integrity. 135 | function poke(PokeData calldata pokeData, SchnorrData calldata schnorrData) 136 | external; 137 | 138 | /// @notice Returns whether the Schnorr signature `schnorrData` is 139 | /// currently acceptable for message `message`. 140 | /// @dev Note that a valid Schnorr signature is only acceptable if the 141 | /// signature was signed by exactly bar many feeds. 142 | /// For more info, see `bar()(uint8)` and `feeds()(address[])`. 143 | /// @dev Note that bar and feeds are configurable, meaning a once acceptable 144 | /// Schnorr signature may become unacceptable in the future. 145 | /// @param message The message expected to be signed via `schnorrData`. 146 | /// @param schnorrData The SchnorrData to verify whether it proves 147 | /// the `message`'s integrity. 148 | /// @return ok True if Schnorr signature is acceptable, false otherwise. 149 | function isAcceptableSchnorrSignatureNow( 150 | bytes32 message, 151 | SchnorrData calldata schnorrData 152 | ) external view returns (bool ok); 153 | 154 | /// @notice Returns the message expected to be signed via Schnorr for 155 | /// `pokeData`. 156 | /// @dev The message is defined as: 157 | /// H(tag ‖ H(wat ‖ pokeData)), where H() is the keccak256 function. 158 | /// @param pokeData The pokeData to create the message for. 159 | /// @return pokeMessage Message for `pokeData`. 160 | function constructPokeMessage(PokeData calldata pokeData) 161 | external 162 | view 163 | returns (bytes32 pokeMessage); 164 | 165 | /// @notice Returns whether address `who` is a feed. 166 | /// @param who The address to check. 167 | /// @return isFeed True if `who` is feed, false otherwise. 168 | function feeds(address who) external view returns (bool isFeed); 169 | 170 | /// @notice Returns whether feed id `feedId` is a feed and, if so, the 171 | /// feed's address. 172 | /// @param feedId The feed id to check. 173 | /// @return isFeed True if `feedId` is a feed, false otherwise. 174 | /// @return feed Address of the feed with id `feedId` if `feedId` is a feed, 175 | /// zero-address otherwise. 176 | function feeds(uint8 feedId) 177 | external 178 | view 179 | returns (bool isFeed, address feed); 180 | 181 | /// @notice Returns list of feed addresses. 182 | /// @dev Note that this function has a high gas consumption and is not 183 | /// intended to be called onchain. 184 | /// @return feeds List of feed addresses. 185 | function feeds() external view returns (address[] memory feeds); 186 | 187 | /// @notice Lifts public key `pubKey` to being a feed. 188 | /// @dev Only callable by auth'ed address. 189 | /// @dev The message expected to be signed by `ecdsaData` is defined via 190 | /// `feedRegistrationMessage()(bytes32)`. 191 | /// @custom:security The lift function's proof of possession is vulnerable 192 | /// to rogue-key attacks. Additional verification MUST be 193 | /// performed before lifting to ensure a feed's public key 194 | /// validity. 195 | /// @param pubKey The public key of the feed. 196 | /// @param ecdsaData ECDSA signed message by the feed's public key. 197 | /// @return feedId The id of the newly lifted feed. 198 | function lift(LibSecp256k1.Point memory pubKey, ECDSAData memory ecdsaData) 199 | external 200 | returns (uint8 feedId); 201 | 202 | /// @notice Lifts public keys `pubKeys` to being feeds. 203 | /// @dev Only callable by auth'ed address. 204 | /// @dev The message expected to be signed by `ecdsaDatas` is defined via 205 | /// `feedRegistrationMessage()(bytes32)`. 206 | /// @custom:security The lift function's proof of possession is vulnerable 207 | /// to rogue-key attacks. Additional verification MUST be 208 | /// performed before lifting to ensure a feed's public key 209 | /// validity. 210 | /// @param pubKeys The public keys of the feeds. 211 | /// @param ecdsaDatas ECDSA signed message by the feeds' public keys. 212 | /// @return List of feed ids of the newly lifted feeds. 213 | function lift( 214 | LibSecp256k1.Point[] memory pubKeys, 215 | ECDSAData[] memory ecdsaDatas 216 | ) external returns (uint8[] memory); 217 | 218 | /// @notice Drops feed with id `feedId`. 219 | /// @dev Only callable by auth'ed address. 220 | /// @param feedId The feed id to drop. 221 | function drop(uint8 feedId) external; 222 | 223 | /// @notice Drops feeds with ids' `feedIds`. 224 | /// @dev Only callable by auth'ed address. 225 | /// @param feedIds The feed ids to drop. 226 | function drop(uint8[] memory feedIds) external; 227 | 228 | /// @notice Updates the bar security parameters to `bar`. 229 | /// @dev Only callable by auth'ed address. 230 | /// @dev Reverts if `bar` is zero. 231 | /// @param bar The value to update bar to. 232 | function setBar(uint8 bar) external; 233 | 234 | /// @notice Returns the oracle's current value. 235 | /// @custom:deprecated Use `tryRead()(bool,uint)` instead. 236 | /// @return value The oracle's current value if it exists, zero otherwise. 237 | /// @return isValid True if value exists, false otherwise. 238 | function peek() external view returns (uint value, bool isValid); 239 | 240 | /// @notice Returns the oracle's current value. 241 | /// @custom:deprecated Use `tryRead()(bool,uint)` instead. 242 | /// @return value The oracle's current value if it exists, zero otherwise. 243 | /// @return isValid True if value exists, false otherwise. 244 | function peep() external view returns (uint value, bool isValid); 245 | } 246 | -------------------------------------------------------------------------------- /docs/Management.md: -------------------------------------------------------------------------------- 1 | # Management 2 | 3 | This document describes how to manage deployed `Scribe` and `ScribeOptimistic` instances. 4 | 5 | ## Table of Contents 6 | 7 | - [Management](#management) 8 | - [Table of Contents](#table-of-contents) 9 | - [Environment Variables](#environment-variables) 10 | - [Functions](#functions) 11 | - [`IScribe::setBar`](#iscribesetbar) 12 | - [`IScribe::lift`](#iscribelift) 13 | - [`IScribe::lift multiple`](#iscribelift-multiple) 14 | - [`IScribe::drop`](#iscribedrop) 15 | - [`IScribe::drop multiple`](#iscribedrop-multiple) 16 | - [`IScribeOptimistic::setOpChallengePeriod`](#iscribeoptimisticsetopchallengeperiod) 17 | - [`IScribeOptimistic::setMaxChallengeReward`](#iscribeoptimisticsetmaxchallengereward) 18 | - [`IAuth::rely`](#iauthrely) 19 | - [`IAuth::deny`](#iauthdeny) 20 | - [`IToll::kiss`](#itollkiss) 21 | - [`IToll::diss`](#itolldiss) 22 | - [Offboarding](#offboarding) 23 | - [Deactivation](#deactivation) 24 | - [Fund Rescue](#fund-resuce) 25 | - [Killing](#killing) 26 | 27 | ## Environment Variables 28 | 29 | The following environment variables must be set for all commands: 30 | 31 | - `RPC_URL`: The RPC URL of an EVM node 32 | - `KEYSTORE`: The path to the keystore file containing the encrypted private key 33 | - `KEYSTORE_PASSWORD`: The password of the keystore file 34 | - `SCRIBE`: The `Scribe`/`ScribeOptimistic` instance to manage 35 | - `SCRIBE_FLAVOUR`: The `Scribe` flavour to manage 36 | - Note that value must be either `Scribe` or `ScribeOptimistic` 37 | 38 | Note that an `.env.example` file is provided in the project root. To set all environment variables at once, create a copy of the file and rename the copy to `.env`, adjust the variable's values', and run `source .env`. 39 | 40 | To easily check the environment variables, run: 41 | 42 | ```bash 43 | $ env | grep -e "RPC_URL" -e "KEYSTORE" -e "KEYSTORE_PASSWORD" -e "SCRIBE" -e "SCRIBE_FLAVOUR" 44 | ``` 45 | 46 | ## Functions 47 | 48 | ### `IScribe::setBar` 49 | 50 | Set the following environment variables: 51 | 52 | - `BAR`: The bar to set 53 | 54 | Run: 55 | 56 | ```bash 57 | $ forge script \ 58 | --keystore "$KEYSTORE" \ 59 | --password "$KEYSTORE_PASSWORD" \ 60 | --broadcast \ 61 | --rpc-url "$RPC_URL" \ 62 | --sig $(cast calldata "setBar(address,uint8)" "$SCRIBE" "$BAR") \ 63 | -vvv \ 64 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 65 | ``` 66 | 67 | ### `IScribe::lift` 68 | 69 | Set the following environment variables: 70 | 71 | - `PUBLIC_KEY_X_COORDINATE`: The feed's public key's `x` coordinate 72 | - `PUBLIC_KEY_Y_COORDINATE`: The feed's public key's `y` coordinate 73 | - `ECDSA_V`: The feed's `feedRegistrationMessage` ECDSA signature's `v` field 74 | - `ECDSA_R`: The feed's `feedRegistrationMessage` ECDSA signature's `r` field 75 | - Note that the value must be provided as `bytes32` 76 | - `ECDSA_S`: The feed's `feedRegistrationMessage` ECDSA signature's `s` field 77 | - Note that the value must be provided as `bytes32` 78 | 79 | Run: 80 | 81 | ```bash 82 | $ forge script \ 83 | --keystore "$KEYSTORE" \ 84 | --password "$KEYSTORE_PASSWORD" \ 85 | --broadcast \ 86 | --rpc-url "$RPC_URL" \ 87 | --sig $(cast calldata "lift(address,uint,uint,uint8,bytes32,bytes32)" "$SCRIBE" "$PUBLIC_KEY_X_COORDINATE" "$PUBLIC_KEY_Y_COORDINATE" "$ECDSA_V" "$ECDSA_R" "$ECDSA_S") \ 88 | -vvv \ 89 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 90 | ``` 91 | 92 | ### `IScribe::lift multiple` 93 | 94 | Set the following environment variables: 95 | 96 | - `PUBLIC_KEY_X_COORDINATES`: The feeds' public keys' `x` coordinates 97 | - `PUBLIC_KEY_Y_COORDINATES`: The feeds' public keys' `y` coordinates 98 | - `ECDSA_VS`: The feeds' `feedRegistrationMessage` ECDSA signatures' `v` fields 99 | - `ECDSA_RS`: The feeds' `feedRegistrationMessage` ECDSA signatures' `r` fields 100 | - Note that the values must be provided as `bytes32` 101 | - `ECDSA_SS`: The feeds' `feedRegistrationMessage` ECDSA signatures' `s` fields 102 | - Note that the values must be provided as `bytes32` 103 | 104 | Note to use the following format for lists: `"[,]"` 105 | 106 | Run: 107 | 108 | ```bash 109 | $ forge script \ 110 | --keystore "$KEYSTORE" \ 111 | --password "$KEYSTORE_PASSWORD" \ 112 | --broadcast \ 113 | --rpc-url "$RPC_URL" \ 114 | --sig $(cast calldata "lift(address,uint[],uint[],uint8[],bytes32[],bytes32[])" "$SCRIBE" "$PUBLIC_KEY_X_COORDINATES" "$PUBLIC_KEY_Y_COORDINATES" "$ECDSA_VS" "$ECDSA_RS" "$ECDSA_SS") \ 115 | -vvv \ 116 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 117 | ``` 118 | 119 | ### `IScribe::drop` 120 | 121 | Set the following environment variables: 122 | 123 | - `FEED_ID`: The feed's id 124 | 125 | Run: 126 | 127 | ```bash 128 | $ forge script \ 129 | --keystore "$KEYSTORE" \ 130 | --password "$KEYSTORE_PASSWORD" \ 131 | --broadcast \ 132 | --rpc-url "$RPC_URL" \ 133 | --sig $(cast calldata "drop(address,uint8)" "$SCRIBE" "$FEED_ID") \ 134 | -vvv \ 135 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 136 | ``` 137 | 138 | ### `IScribe::drop multiple` 139 | 140 | Set the following environment variables: 141 | 142 | - `FEED_IDS`: The feeds' ids 143 | 144 | Note to use the following format for lists: `"[,]"` 145 | 146 | Run: 147 | 148 | ```bash 149 | $ forge script \ 150 | --keystore "$KEYSTORE" \ 151 | --password "$KEYSTORE_PASSWORD" \ 152 | --broadcast \ 153 | --rpc-url "$RPC_URL" \ 154 | --sig $(cast calldata "drop(address,uint8[])" "$SCRIBE" "$FEED_IDS") \ 155 | -vvv \ 156 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 157 | ``` 158 | 159 | ### `IScribeOptimistic::setOpChallengePeriod` 160 | 161 | > **Warning** 162 | > 163 | > This command is only supported if the `Scribe` instance is of type `IScribeOptimistic`! 164 | 165 | Set the following environment variables: 166 | 167 | - `OP_CHALLENGE_PERIOD`: The length of the optimistic challenge period to set, in seconds 168 | 169 | Run: 170 | 171 | ```bash 172 | $ forge script \ 173 | --keystore "$KEYSTORE" \ 174 | --password "$KEYSTORE_PASSWORD" \ 175 | --broadcast \ 176 | --rpc-url "$RPC_URL" \ 177 | --sig $(cast calldata "setOpChallengePeriod(address,uint16)" "$SCRIBE" "$OP_CHALLENGE_PERIOD") \ 178 | -vvv \ 179 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 180 | ``` 181 | 182 | 183 | ### `IScribeOptimistic::setMaxChallengeReward` 184 | 185 | > **Warning** 186 | > 187 | > This command is only supported if the `Scribe` instance is of type `IScribeOptimistic`! 188 | 189 | Set the following environment variables: 190 | 191 | - `MAX_CHALLENGE_REWARD`: The max challenge reward to set 192 | 193 | Run: 194 | 195 | ```bash 196 | $ forge script \ 197 | --keystore "$KEYSTORE" \ 198 | --password "$KEYSTORE_PASSWORD" \ 199 | --broadcast \ 200 | --rpc-url "$RPC_URL" \ 201 | --sig $(cast calldata "setMaxChallengeReward(address,uint)" "$SCRIBE" "$MAX_CHALLENGE_REWARD") \ 202 | -vvv \ 203 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 204 | ``` 205 | 206 | 207 | ### `IAuth::rely` 208 | 209 | Set the following environment variables: 210 | 211 | - `WHO`: The address to grant auth to 212 | 213 | Run: 214 | 215 | ```bash 216 | $ forge script \ 217 | --keystore "$KEYSTORE" \ 218 | --password "$KEYSTORE_PASSWORD" \ 219 | --broadcast \ 220 | --rpc-url "$RPC_URL" \ 221 | --sig $(cast calldata "rely(address,address)" "$SCRIBE" "$WHO") \ 222 | -vvv \ 223 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 224 | ``` 225 | 226 | ### `IAuth::deny` 227 | 228 | Set the following environment variables: 229 | 230 | - `WHO`: The address to renounce auth from 231 | 232 | Run: 233 | 234 | ```bash 235 | $ forge script \ 236 | --keystore "$KEYSTORE" \ 237 | --password "$KEYSTORE_PASSWORD" \ 238 | --broadcast \ 239 | --rpc-url "$RPC_URL" \ 240 | --sig $(cast calldata "deny(address,address)" "$SCRIBE" "$WHO") \ 241 | -vvv \ 242 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 243 | ``` 244 | 245 | ### `IToll::kiss` 246 | 247 | Set the following environment variables: 248 | 249 | - `WHO`: The address to grant toll to 250 | 251 | Run: 252 | 253 | ```bash 254 | $ forge script \ 255 | --keystore "$KEYSTORE" \ 256 | --password "$KEYSTORE_PASSWORD" \ 257 | --broadcast \ 258 | --rpc-url "$RPC_URL" \ 259 | --sig $(cast calldata "kiss(address,address)" "$SCRIBE" "$WHO") \ 260 | -vvv \ 261 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 262 | ``` 263 | 264 | ### `IToll::diss` 265 | 266 | Set the following environment variables: 267 | 268 | - `WHO`: The address to renounce toll from 269 | 270 | Run: 271 | 272 | ```bash 273 | $ forge script \ 274 | --keystore "$KEYSTORE" \ 275 | --password "$KEYSTORE_PASSWORD" \ 276 | --broadcast \ 277 | --rpc-url "$RPC_URL" \ 278 | --sig $(cast calldata "diss(address,address)" "$SCRIBE" "$WHO") \ 279 | -vvv \ 280 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 281 | ``` 282 | 283 | ## Offboarding 284 | 285 | Offboarding a Scribe(Optimistic) instance simply means _Chronicle Protocol_ is not guaranteeing pokes anymore, ie the oracle is not being updated anymore. 286 | 287 | However, to ensure an offboarded Scribe(Optimistic) instance may not behave unexpectedly it needs to be deactivated. Furthermore, if the contract is a ScribeOptimistic instance ETH held by the contract may need to be rescued. If its certain the contract will never be used again it is recommended to kill it. 288 | 289 | ### Deactivation 290 | 291 | Deactivating a Scribe(Optimistic) instance means its value is set to zero, leading all `read()` calls to revert/fail, no feeds are lifted, and `bar` is set to `255`. 292 | 293 | Note that one or more addresses still hold `auth` on the contract meaning the instance can be reactivated via `lift`-ing feeds and updating `bar` again. 294 | 295 | > [!IMPORTANT] 296 | > 297 | > Deactivation requires running two distinct `forge script` commands. 298 | > 299 | > It is of utmost importance to run both commands and NOT leave the Scribe(Optimistic) instance in an undefined state. 300 | 301 | Step 1: 302 | 303 | ```bash 304 | $ forge script \ 305 | --keystore "$KEYSTORE" \ 306 | --password "$KEYSTORE_PASSWORD" \ 307 | --broadcast \ 308 | --rpc-url "$RPC_URL" \ 309 | --sig $(cast calldata "deactivate_Step1(address)" "$SCRIBE") \ 310 | -vvv \ 311 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 312 | ``` 313 | 314 | Step 2: 315 | 316 | ```bash 317 | $ forge script \ 318 | --keystore "$KEYSTORE" \ 319 | --password "$KEYSTORE_PASSWORD" \ 320 | --broadcast \ 321 | --rpc-url "$RPC_URL" \ 322 | --sig $(cast calldata "deactivate_Step2(address)" "$SCRIBE") \ 323 | -vvv \ 324 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 325 | ``` 326 | 327 | ### Fund Rescue 328 | 329 | TODO: Rescuing funds 330 | 331 | ### Killing 332 | 333 | Killing a deactivated Scribe(Optimistic) instance ensures it cannot be activated again. Note that killing an instance makes the contract's state immutable via `deny`-ing `auth` for every address. 334 | 335 | Run: 336 | 337 | ```bash 338 | $ forge script \ 339 | --keystore "$KEYSTORE" \ 340 | --password "$KEYSTORE_PASSWORD" \ 341 | --broadcast \ 342 | --rpc-url "$RPC_URL" \ 343 | --sig $(cast calldata "kill(address)" "$SCRIBE") \ 344 | --sender $(cast wallet address --keystore $KEYSTORE --password $KEYSTORE_PASSWORD) \ 345 | -vvv \ 346 | script/${SCRIBE_FLAVOUR}.s.sol:${SCRIBE_FLAVOUR}Script 347 | ``` 348 | -------------------------------------------------------------------------------- /test/LibSchnorrTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {console2} from "forge-std/console2.sol"; 6 | 7 | import {LibSchnorr} from "src/libs/LibSchnorr.sol"; 8 | import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; 9 | 10 | import {LibSchnorrExtended} from "script/libs/LibSchnorrExtended.sol"; 11 | import {LibSecp256k1Extended} from "script/libs/LibSecp256k1Extended.sol"; 12 | import {LibDissig} from "script/libs/LibDissig.sol"; 13 | import {LibOracleSuite} from "script/libs/LibOracleSuite.sol"; 14 | 15 | abstract contract LibSchnorrTest is Test { 16 | using LibSecp256k1 for LibSecp256k1.Point; 17 | using LibSecp256k1Extended for uint; 18 | using LibSchnorrExtended for uint; 19 | using LibSchnorrExtended for uint[]; 20 | using LibSchnorrExtended for LibSecp256k1.Point; 21 | using LibSchnorrExtended for LibSecp256k1.Point[]; 22 | 23 | function testFuzzDifferentialOracleSuite_verifySignature( 24 | uint[] memory privKeySeeds, 25 | bytes32 message 26 | ) public { 27 | vm.assume(privKeySeeds.length > 1); 28 | // Keep number of signers low to not run out-of-gas. 29 | if (privKeySeeds.length > 50) { 30 | assembly ("memory-safe") { 31 | mstore(privKeySeeds, 50) 32 | } 33 | } 34 | 35 | // Let each privKey ∊ [2, Q). 36 | // Note that we allow double signing. 37 | uint[] memory privKeys = new uint[](privKeySeeds.length); 38 | for (uint i; i < privKeySeeds.length; i++) { 39 | privKeys[i] = _bound(privKeySeeds[i], 2, LibSecp256k1.Q() - 1); 40 | } 41 | 42 | // Make list of public key. 43 | LibSecp256k1.Point[] memory pubKeys = 44 | new LibSecp256k1.Point[](privKeySeeds.length); 45 | for (uint i; i < privKeySeeds.length; i++) { 46 | pubKeys[i] = privKeys[i].derivePublicKey(); 47 | } 48 | 49 | // Compute aggregated public key. 50 | LibSecp256k1.Point memory aggPubKey = pubKeys.aggregatePublicKeys(); 51 | 52 | // IMPORTANT: Don't do anything if pubKey.x is zero. 53 | if (aggPubKey.x == 0) { 54 | console2.log("Received public key with zero x coordinate"); 55 | console2.log("-- Public key's y coordinate", aggPubKey.y); 56 | return; 57 | } 58 | 59 | // Create signature via oracle-suite. 60 | uint signatureSuite; 61 | address commitmentSuite; 62 | (signatureSuite, commitmentSuite) = 63 | LibOracleSuite.sign(privKeys, message); 64 | 65 | // Create signature via LibSchnorrExtended. 66 | uint signatureLibSchnorr; 67 | address commitmentLibSchnorr; 68 | (signatureLibSchnorr, commitmentLibSchnorr) = 69 | LibSchnorrExtended.signMessage(privKeys, message); 70 | 71 | // Expect both signatures to be verifiable via LibSchnorr. 72 | assertTrue( 73 | LibSchnorr.verifySignature( 74 | aggPubKey, message, bytes32(signatureSuite), commitmentSuite 75 | ) 76 | ); 77 | assertTrue( 78 | LibSchnorr.verifySignature( 79 | aggPubKey, 80 | message, 81 | bytes32(signatureLibSchnorr), 82 | commitmentLibSchnorr 83 | ) 84 | ); 85 | 86 | // Expect both signatures to be verifiable via oracle-suite. 87 | assertTrue( 88 | LibOracleSuite.verify( 89 | aggPubKey, message, bytes32(signatureSuite), commitmentSuite 90 | ) 91 | ); 92 | assertTrue( 93 | LibOracleSuite.verify( 94 | aggPubKey, 95 | message, 96 | bytes32(signatureLibSchnorr), 97 | commitmentLibSchnorr 98 | ) 99 | ); 100 | } 101 | 102 | function testFuzz_verifySignature_SingleSigner( 103 | uint privKeySeed, 104 | bytes32 message 105 | ) public { 106 | // Let privKey ∊ [1, Q). 107 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 108 | 109 | // Compute pubKey. 110 | LibSecp256k1.Point memory pubKey = privKey.derivePublicKey(); 111 | 112 | // Sign message. 113 | uint signature; 114 | address commitment; 115 | (signature, commitment) = privKey.signMessage(message); 116 | 117 | // Signature is _not_ verifiable if one of the following cases hold: 118 | // - commitment == address(0) 119 | // - pubKey.x == 0 120 | // - signature == 0 121 | // - signature >= Q 122 | bool shouldBeOk = true; 123 | if (commitment == address(0)) shouldBeOk = false; 124 | if (pubKey.x == 0) shouldBeOk = false; 125 | if (signature == 0) shouldBeOk = false; 126 | if (signature >= LibSecp256k1.Q()) shouldBeOk = false; 127 | 128 | // Signature verification should equal expected value. 129 | bool ok = LibSchnorr.verifySignature( 130 | privKey.derivePublicKey(), message, bytes32(signature), commitment 131 | ); 132 | assertEq(ok, shouldBeOk); 133 | } 134 | 135 | function testFuzz_verifySignature_MultipleSigners( 136 | uint[] memory privKeySeeds, 137 | bytes32 message 138 | ) public { 139 | vm.assume(privKeySeeds.length > 1); 140 | // Keep low to not run out-of-gas. 141 | vm.assume(privKeySeeds.length < 50); 142 | 143 | // Let each privKey ∊ [2, Q). 144 | // Note that we allow double signing. 145 | uint[] memory privKeys = new uint[](privKeySeeds.length); 146 | for (uint i; i < privKeySeeds.length; i++) { 147 | privKeys[i] = _bound(privKeySeeds[i], 2, LibSecp256k1.Q() - 1); 148 | } 149 | 150 | // Make list of public key. 151 | LibSecp256k1.Point[] memory pubKeys = 152 | new LibSecp256k1.Point[](privKeySeeds.length); 153 | for (uint i; i < privKeySeeds.length; i++) { 154 | pubKeys[i] = privKeys[i].derivePublicKey(); 155 | } 156 | 157 | // Compute aggregated public key. 158 | LibSecp256k1.Point memory aggPubKey = pubKeys.aggregatePublicKeys(); 159 | 160 | // Sign message. 161 | uint signature; 162 | address commitment; 163 | (signature, commitment) = privKeys.signMessage(message); 164 | 165 | // Signature is _not_ verifiable if one of the following cases hold: 166 | // - commitment == address(0) 167 | // - pubKey.x == 0 168 | // - signature == 0 169 | // - signature >= Q 170 | bool shouldBeOk = true; 171 | if (commitment == address(0)) shouldBeOk = false; 172 | if (aggPubKey.x == 0) shouldBeOk = false; 173 | if (signature == 0) shouldBeOk = false; 174 | if (signature >= LibSecp256k1.Q()) shouldBeOk = false; 175 | 176 | // Signature verification should equal expected value. 177 | bool ok = LibSchnorr.verifySignature( 178 | pubKeys.aggregatePublicKeys(), 179 | message, 180 | bytes32(signature), 181 | commitment 182 | ); 183 | assertEq(ok, shouldBeOk); 184 | } 185 | 186 | function testFuzz_verifySignature_FailsIf_SignatureMutated( 187 | uint privKeySeed, 188 | bytes32 message, 189 | uint signatureMask 190 | ) public { 191 | vm.assume(signatureMask != 0); 192 | 193 | // Let privKey ∊ [1, Q). 194 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 195 | 196 | // Sign message. 197 | uint signature; 198 | address commitment; 199 | (signature, commitment) = privKey.signMessage(message); 200 | 201 | // Mutate signature. 202 | signature ^= signatureMask; 203 | 204 | // Signature verification should not succeed. 205 | bool ok = LibSchnorr.verifySignature( 206 | privKey.derivePublicKey(), message, bytes32(signature), commitment 207 | ); 208 | assertFalse(ok); 209 | } 210 | 211 | function testFuzz_verifySignature_FailsIf_CommitmentMutated( 212 | uint privKeySeed, 213 | bytes32 message, 214 | uint160 commitmentMask 215 | ) public { 216 | vm.assume(commitmentMask != 0); 217 | 218 | // Let privKey ∊ [1, Q). 219 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 220 | 221 | // Sign message. 222 | uint signature; 223 | address commitment; 224 | (signature, commitment) = privKey.signMessage(message); 225 | 226 | // Mutate commitment. 227 | commitment = address(uint160(commitment) ^ commitmentMask); 228 | 229 | // Signature verification should not succeed. 230 | bool ok = LibSchnorr.verifySignature( 231 | privKey.derivePublicKey(), message, bytes32(signature), commitment 232 | ); 233 | assertFalse(ok); 234 | } 235 | 236 | function testFuzz_verifySignature_FailsIf_MessageMutated( 237 | uint privKeySeed, 238 | bytes32 message, 239 | uint messageMask 240 | ) public { 241 | vm.assume(messageMask != 0); 242 | 243 | // Let privKey ∊ [1, Q). 244 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 245 | 246 | // Sign message. 247 | uint signature; 248 | address commitment; 249 | (signature, commitment) = privKey.signMessage(message); 250 | 251 | // Mutate message. 252 | message = bytes32(uint(message) ^ messageMask); 253 | 254 | // Signature verification should not succeed. 255 | bool ok = LibSchnorr.verifySignature( 256 | privKey.derivePublicKey(), message, bytes32(signature), commitment 257 | ); 258 | assertFalse(ok); 259 | } 260 | 261 | function testFuzz_verifySignature_FailsIf_PubKeyNotOnCurve( 262 | uint privKeySeed, 263 | bytes32 message, 264 | uint pubKeyXMask, 265 | bool flipParity 266 | ) public { 267 | vm.assume(pubKeyXMask != 0 || flipParity); 268 | 269 | // Let privKey ∊ [1, Q). 270 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 271 | 272 | // Sign message. 273 | uint signature; 274 | address commitment; 275 | (signature, commitment) = privKey.signMessage(message); 276 | 277 | // Compute and mutate pubKey. 278 | LibSecp256k1.Point memory pubKey = privKey.derivePublicKey(); 279 | pubKey.x ^= pubKeyXMask; 280 | pubKey.y = flipParity ? pubKey.y + 1 : pubKey.y; 281 | 282 | vm.assume(!pubKey.isOnCurve()); 283 | 284 | // Signature verification should not succeed. 285 | bool ok = LibSchnorr.verifySignature( 286 | pubKey, message, bytes32(signature), commitment 287 | ); 288 | assertFalse(ok); 289 | } 290 | 291 | function testFuzz_verifySignature_FailsIf_SignatureIsZero( 292 | uint privKeySeed, 293 | bytes32 message 294 | ) public { 295 | // Let privKey ∊ [1, Q). 296 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 297 | 298 | // Sign message. 299 | uint signature; 300 | address commitment; 301 | (signature, commitment) = privKey.signMessage(message); 302 | 303 | // Let signature be zero. 304 | signature = 0; 305 | 306 | // Signature verification should not succeed. 307 | bool ok = LibSchnorr.verifySignature( 308 | privKey.derivePublicKey(), message, bytes32(signature), commitment 309 | ); 310 | assertFalse(ok); 311 | } 312 | 313 | function testFuzz_verifySignature_FailsIf_CommitmentIsZero( 314 | uint privKeySeed, 315 | bytes32 message 316 | ) public { 317 | // Let privKey ∊ [1, Q). 318 | uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); 319 | 320 | // Sign message. 321 | uint signature; 322 | address commitment; 323 | (signature, commitment) = privKey.signMessage(message); 324 | 325 | // Let commitment be zero. 326 | commitment = address(0); 327 | 328 | // Signature verification should not succeed. 329 | bool ok = LibSchnorr.verifySignature( 330 | privKey.derivePublicKey(), message, bytes32(signature), commitment 331 | ); 332 | assertFalse(ok); 333 | } 334 | } 335 | --------------------------------------------------------------------------------