diff --git a/src/IScribe.sol b/src/IScribe.sol index 3870dff..21bfa30 100644 --- a/src/IScribe.sol +++ b/src/IScribe.sol @@ -1,248 +1,253 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; -// forgefmt: disable-start - -import {IChronicle} from "chronicle-std/IChronicle.sol"; - -import {LibSecp256k1} from "./libs/LibSecp256k1.sol"; - -interface IScribe is IChronicle { - /// @dev PokeData encapsulates a value and its age. - struct PokeData { - uint128 val; - uint32 age; - } - - /// @dev SchnorrData encapsulates a (aggregated) Schnorr signature. - /// Schnorr signatures are used to prove a PokeData's integrity. - struct SchnorrData { - bytes32 signature; - address commitment; - bytes feedIds; - } - - /// @dev ECDSAData encapsulates an ECDSA signature. - struct ECDSAData { - uint8 v; - bytes32 r; - bytes32 s; - } - - /// @notice Thrown if a poked value's age is not greater than the oracle's - /// current value's age. - /// @param givenAge The poked value's age. - /// @param currentAge The oracle's current value's age. - error StaleMessage(uint32 givenAge, uint32 currentAge); - - /// @notice Thrown if a poked value's age is greater than the current - /// time. - /// @param givenAge The poked value's age. - /// @param currentTimestamp The current time. - error FutureMessage(uint32 givenAge, uint32 currentTimestamp); - - /// @notice Thrown if Schnorr signature not signed by exactly bar many - /// signers. - /// @param numberSigners The number of signers for given Schnorr signature. - /// @param bar The bar security parameter. - error BarNotReached(uint8 numberSigners, uint8 bar); - - /// @notice Thrown if given feed id invalid. - /// @param feedId The invalid feed id. - error InvalidFeedId(uint8 feedId); - - /// @notice Thrown if double signing attempted. - /// @param feedId The id of the feed attempting to double sign. - error DoubleSigningAttempted(uint8 feedId); - - /// @notice Thrown if Schnorr signature verification failed. - error SchnorrSignatureInvalid(); - - /// @notice Emitted when oracle was successfully poked. - /// @param caller The caller's address. - /// @param val The value poked. - /// @param age The age of the value poked. - event Poked(address indexed caller, uint128 val, uint32 age); - - /// @notice Emitted when new feed lifted. - /// @param caller The caller's address. - /// @param feed The feed address lifted. - event FeedLifted(address indexed caller, address indexed feed); - - /// @notice Emitted when feed dropped. - /// @param caller The caller's address. - /// @param feed The feed address dropped. - event FeedDropped(address indexed caller, address indexed feed); - - /// @notice Emitted when bar updated. - /// @param caller The caller's address. - /// @param oldBar The old bar's value. - /// @param newBar The new bar's value. - event BarUpdated(address indexed caller, uint8 oldBar, uint8 newBar); - - /// @notice Returns the feed registration message. - /// @dev This message must be signed by a feed in order to be lifted. - /// @return feedRegistrationMessage Chronicle Protocol's feed registration - /// message. - function feedRegistrationMessage() - external - view - returns (bytes32 feedRegistrationMessage); - - /// @notice Returns the bar security parameter. - /// @return bar The bar security parameter. - function bar() external view returns (uint8 bar); - - /// @notice Returns the number of decimals of the oracle's value. - /// @dev Provides partial compatibility with Chainlink's - /// IAggregatorV3Interface. - /// @return decimals The oracle value's number of decimals. - function decimals() external view returns (uint8 decimals); - - /// @notice Returns the oracle's latest value. - /// @dev Provides partial compatibility with Chainlink's - /// IAggregatorV3Interface. - /// @return roundId 1. - /// @return answer The oracle's latest value. - /// @return startedAt 0. - /// @return updatedAt The timestamp of oracle's latest update. - /// @return answeredInRound 1. - function latestRoundData() - external - view - returns ( - uint80 roundId, - int answer, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ); - - /// @notice Returns the oracle's latest value. - /// @dev Provides partial compatibility with Chainlink's - /// IAggregatorV3Interface. - /// @custom:deprecated See https://docs.chain.link/data-feeds/api-reference/#latestanswer. - /// @return answer The oracle's latest value. - function latestAnswer() external view returns (int); - - /// @notice Pokes the oracle. - /// @dev Expects `pokeData`'s age to be greater than the timestamp of the - /// last successful poke. - /// @dev Expects `pokeData`'s age to not be greater than the current time. - /// @dev Expects `schnorrData` to prove `pokeData`'s integrity. - /// See `isAcceptableSchnorrSignatureNow(bytes32,SchnorrData)(bool)`. - /// @param pokeData The PokeData being poked. - /// @param schnorrData The SchnorrData proving the `pokeData`'s - /// integrity. - function poke(PokeData calldata pokeData, SchnorrData calldata schnorrData) - external; - - /// @notice Returns whether the Schnorr signature `schnorrData` is - /// currently acceptable for message `message`. - /// @dev Note that a valid Schnorr signature is only acceptable if the - /// signature was signed by exactly bar many feeds. - /// For more info, see `bar()(uint8)` and `feeds()(address[])`. - /// @dev Note that bar and feeds are configurable, meaning a once acceptable - /// Schnorr signature may become unacceptable in the future. - /// @param message The message expected to be signed via `schnorrData`. - /// @param schnorrData The SchnorrData to verify whether it proves - /// the `message`'s integrity. - /// @return ok True if Schnorr signature is acceptable, false otherwise. - function isAcceptableSchnorrSignatureNow( - bytes32 message, - SchnorrData calldata schnorrData - ) external view returns (bool ok); - - /// @notice Returns the message expected to be signed via Schnorr for - /// `pokeData`. - /// @dev The message is defined as: - /// H(tag ‖ H(wat ‖ pokeData)), where H() is the keccak256 function. - /// @param pokeData The pokeData to create the message for. - /// @return pokeMessage Message for `pokeData`. - function constructPokeMessage(PokeData calldata pokeData) - external - view - returns (bytes32 pokeMessage); - - /// @notice Returns whether address `who` is a feed. - /// @param who The address to check. - /// @return isFeed True if `who` is feed, false otherwise. - function feeds(address who) external view returns (bool isFeed); - - /// @notice Returns whether feed id `feedId` is a feed and, if so, the - /// feed's address. - /// @param feedId The feed id to check. - /// @return isFeed True if `feedId` is a feed, false otherwise. - /// @return feed Address of the feed with id `feedId` if `feedId` is a feed, - /// zero-address otherwise. - function feeds(uint8 feedId) - external - view - returns (bool isFeed, address feed); - - /// @notice Returns list of feed addresses. - /// @dev Note that this function has a high gas consumption and is not - /// intended to be called onchain. - /// @return feeds List of feed addresses. - function feeds() external view returns (address[] memory feeds); - - /// @notice Lifts public key `pubKey` to being a feed. - /// @dev Only callable by auth'ed address. - /// @dev The message expected to be signed by `ecdsaData` is defined via - /// `feedRegistrationMessage()(bytes32)`. - /// @custom:security The lift function's proof of possession is vulnerable - /// to rogue-key attacks. Additional verification MUST be - /// performed before lifting to ensure a feed's public key - /// validity. - /// @param pubKey The public key of the feed. - /// @param ecdsaData ECDSA signed message by the feed's public key. - /// @return feedId The id of the newly lifted feed. - function lift(LibSecp256k1.Point memory pubKey, ECDSAData memory ecdsaData) - external - returns (uint8 feedId); - - /// @notice Lifts public keys `pubKeys` to being feeds. - /// @dev Only callable by auth'ed address. - /// @dev The message expected to be signed by `ecdsaDatas` is defined via - /// `feedRegistrationMessage()(bytes32)`. - /// @custom:security The lift function's proof of possession is vulnerable - /// to rogue-key attacks. Additional verification MUST be - /// performed before lifting to ensure a feed's public key - /// validity. - /// @param pubKeys The public keys of the feeds. - /// @param ecdsaDatas ECDSA signed message by the feeds' public keys. - /// @return List of feed ids of the newly lifted feeds. - function lift( - LibSecp256k1.Point[] memory pubKeys, - ECDSAData[] memory ecdsaDatas - ) external returns (uint8[] memory); - - /// @notice Drops feed with id `feedId`. - /// @dev Only callable by auth'ed address. - /// @param feedId The feed id to drop. - function drop(uint8 feedId) external; - - /// @notice Drops feeds with ids' `feedIds`. - /// @dev Only callable by auth'ed address. - /// @param feedIds The feed ids to drop. - function drop(uint8[] memory feedIds) external; - - /// @notice Updates the bar security parameters to `bar`. - /// @dev Only callable by auth'ed address. - /// @dev Reverts if `bar` is zero. - /// @param bar The value to update bar to. - function setBar(uint8 bar) external; - - /// @notice Returns the oracle's current value. - /// @custom:deprecated Use `tryRead()(bool,uint)` instead. - /// @return value The oracle's current value if it exists, zero otherwise. - /// @return isValid True if value exists, false otherwise. - function peek() external view returns (uint value, bool isValid); - - /// @notice Returns the oracle's current value. - /// @custom:deprecated Use `tryRead()(bool,uint)` instead. - /// @return value The oracle's current value if it exists, zero otherwise. - /// @return isValid True if value exists, false otherwise. - function peep() external view returns (uint value, bool isValid); -} - -// forgefmt: disable-end +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; +// forgefmt: disable-start + +import {IChronicle} from "chronicle-std/IChronicle.sol"; + +import {LibSecp256k1} from "./libs/LibSecp256k1.sol"; + +interface IScribe is IChronicle { + /// @dev PokeData encapsulates a value and its age. + struct PokeData { + uint128 val; + uint32 age; + } + + /// @dev SchnorrData encapsulates a (aggregated) Schnorr signature. + /// Schnorr signatures are used to prove a PokeData's integrity. + struct SchnorrData { + bytes32 signature; + address commitment; + bytes feedIds; + } + + /// @dev ECDSAData encapsulates an ECDSA signature. + struct ECDSAData { + uint8 v; + bytes32 r; + bytes32 s; + } + + /// @notice Thrown if a poked value's age is not greater than the oracle's + /// current value's age. + /// @param givenAge The poked value's age. + /// @param currentAge The oracle's current value's age. + error StaleMessage(uint32 givenAge, uint32 currentAge); + + /// @notice Thrown if a poked value's age is greater than the current + /// time. + /// @param givenAge The poked value's age. + /// @param currentTimestamp The current time. + error FutureMessage(uint32 givenAge, uint32 currentTimestamp); + + /// @notice Thrown if Schnorr signature not signed by exactly bar many + /// signers. + /// @param numberSigners The number of signers for given Schnorr signature. + /// @param bar The bar security parameter. + error BarNotReached(uint8 numberSigners, uint8 bar); + + /// @notice Thrown if given feed id invalid. + /// @param feedId The invalid feed id. + error InvalidFeedId(uint8 feedId); + + /// @notice Thrown if double signing attempted. + /// @param feedId The id of the feed attempting to double sign. + error DoubleSigningAttempted(uint8 feedId); + + /// @notice Thrown if Schnorr signature verification failed. + error SchnorrSignatureInvalid(); + + /// @notice Emitted when oracle was successfully poked. + /// @param caller The caller's address. + /// @param val The value poked. + /// @param age The age of the value poked. + event Poked(address indexed caller, uint128 val, uint32 age); + + /// @notice Emitted when new feed lifted. + /// @param caller The caller's address. + /// @param feed The feed address lifted. + event FeedLifted(address indexed caller, address indexed feed); + + /// @notice Emitted when feed dropped. + /// @param caller The caller's address. + /// @param feed The feed address dropped. + event FeedDropped(address indexed caller, address indexed feed); + + /// @notice Emitted when bar updated. + /// @param caller The caller's address. + /// @param oldBar The old bar's value. + /// @param newBar The new bar's value. + event BarUpdated(address indexed caller, uint8 oldBar, uint8 newBar); + + /// @notice Returns the feed registration message. + /// @dev This message must be signed by a feed in order to be lifted. + /// @return feedRegistrationMessage Chronicle Protocol's feed registration + /// message. + function feedRegistrationMessage() + external + view + returns (bytes32 feedRegistrationMessage); + + /// @notice Returns the bar security parameter. + /// @return bar The bar security parameter. + function bar() external view returns (uint8 bar); + + /// @notice Returns the number of decimals of the oracle's value. + /// @dev Provides partial compatibility with Chainlink's + /// IAggregatorV3Interface. + /// @return decimals The oracle value's number of decimals. + function decimals() external view returns (uint8 decimals); + + /// @notice Returns the oracle's latest value. + /// @dev Provides partial compatibility with Chainlink's + /// IAggregatorV3Interface. + /// @return roundId 1. + /// @return answer The oracle's latest value. + /// @return startedAt 0. + /// @return updatedAt The timestamp of oracle's latest update. + /// @return answeredInRound 1. + function latestRoundData() + external + view + returns ( + uint80 roundId, + int answer, + uint startedAt, + uint updatedAt, + uint80 answeredInRound + ); + + /// @notice Returns the oracle's latest value. + /// @dev Provides partial compatibility with Chainlink's + /// IAggregatorV3Interface. + /// @custom:deprecated See https://docs.chain.link/data-feeds/api-reference/#latestanswer. + /// @return answer The oracle's latest value. + function latestAnswer() external view returns (int); + + /// @notice Pokes the oracle. + /// @dev Expects `pokeData`'s age to be greater than the timestamp of the + /// last successful poke. + /// @dev Expects `pokeData`'s age to not be greater than the current time. + /// @dev Expects `schnorrData` to prove `pokeData`'s integrity. + /// See `isAcceptableSchnorrSignatureNow(bytes32,SchnorrData)(bool)`. + /// @param pokeData The PokeData being poked. + /// @param schnorrData The SchnorrData proving the `pokeData`'s + /// integrity. + function poke(PokeData calldata pokeData, SchnorrData calldata schnorrData) + external; + + /// @notice Returns whether the Schnorr signature `schnorrData` is + /// currently acceptable for message `message`. + /// @dev Note that a valid Schnorr signature is only acceptable if the + /// signature was signed by exactly bar many feeds. + /// For more info, see `bar()(uint8)` and `feeds()(address[])`. + /// @dev Note that bar and feeds are configurable, meaning a once acceptable + /// Schnorr signature may become unacceptable in the future. + /// @param message The message expected to be signed via `schnorrData`. + /// @param schnorrData The SchnorrData to verify whether it proves + /// the `message`'s integrity. + /// @return ok True if Schnorr signature is acceptable, false otherwise. + function isAcceptableSchnorrSignatureNow( + bytes32 message, + SchnorrData calldata schnorrData + ) external view returns (bool ok); + + /// @notice Returns the message expected to be signed via Schnorr for + /// `pokeData`. + /// @dev The message is defined as: + /// H(tag ‖ H(this ‖ chainid ‖ wat ‖ pokeData)), where H() is the + /// keccak256 function. + /// + /// `this` and `chainid` provide domain separation so that a signature + /// produced for one Scribe instance cannot be replayed against another + /// instance sharing the same `wat`, on this or any other chain. + /// @param pokeData The pokeData to create the message for. + /// @return pokeMessage Message for `pokeData`. + function constructPokeMessage(PokeData calldata pokeData) + external + view + returns (bytes32 pokeMessage); + + /// @notice Returns whether address `who` is a feed. + /// @param who The address to check. + /// @return isFeed True if `who` is feed, false otherwise. + function feeds(address who) external view returns (bool isFeed); + + /// @notice Returns whether feed id `feedId` is a feed and, if so, the + /// feed's address. + /// @param feedId The feed id to check. + /// @return isFeed True if `feedId` is a feed, false otherwise. + /// @return feed Address of the feed with id `feedId` if `feedId` is a feed, + /// zero-address otherwise. + function feeds(uint8 feedId) + external + view + returns (bool isFeed, address feed); + + /// @notice Returns list of feed addresses. + /// @dev Note that this function has a high gas consumption and is not + /// intended to be called onchain. + /// @return feeds List of feed addresses. + function feeds() external view returns (address[] memory feeds); + + /// @notice Lifts public key `pubKey` to being a feed. + /// @dev Only callable by auth'ed address. + /// @dev The message expected to be signed by `ecdsaData` is defined via + /// `feedRegistrationMessage()(bytes32)`. + /// @custom:security The lift function's proof of possession is vulnerable + /// to rogue-key attacks. Additional verification MUST be + /// performed before lifting to ensure a feed's public key + /// validity. + /// @param pubKey The public key of the feed. + /// @param ecdsaData ECDSA signed message by the feed's public key. + /// @return feedId The id of the newly lifted feed. + function lift(LibSecp256k1.Point memory pubKey, ECDSAData memory ecdsaData) + external + returns (uint8 feedId); + + /// @notice Lifts public keys `pubKeys` to being feeds. + /// @dev Only callable by auth'ed address. + /// @dev The message expected to be signed by `ecdsaDatas` is defined via + /// `feedRegistrationMessage()(bytes32)`. + /// @custom:security The lift function's proof of possession is vulnerable + /// to rogue-key attacks. Additional verification MUST be + /// performed before lifting to ensure a feed's public key + /// validity. + /// @param pubKeys The public keys of the feeds. + /// @param ecdsaDatas ECDSA signed message by the feeds' public keys. + /// @return List of feed ids of the newly lifted feeds. + function lift( + LibSecp256k1.Point[] memory pubKeys, + ECDSAData[] memory ecdsaDatas + ) external returns (uint8[] memory); + + /// @notice Drops feed with id `feedId`. + /// @dev Only callable by auth'ed address. + /// @param feedId The feed id to drop. + function drop(uint8 feedId) external; + + /// @notice Drops feeds with ids' `feedIds`. + /// @dev Only callable by auth'ed address. + /// @param feedIds The feed ids to drop. + function drop(uint8[] memory feedIds) external; + + /// @notice Updates the bar security parameters to `bar`. + /// @dev Only callable by auth'ed address. + /// @dev Reverts if `bar` is zero. + /// @param bar The value to update bar to. + function setBar(uint8 bar) external; + + /// @notice Returns the oracle's current value. + /// @custom:deprecated Use `tryRead()(bool,uint)` instead. + /// @return value The oracle's current value if it exists, zero otherwise. + /// @return isValid True if value exists, false otherwise. + function peek() external view returns (uint value, bool isValid); + + /// @notice Returns the oracle's current value. + /// @custom:deprecated Use `tryRead()(bool,uint)` instead. + /// @return value The oracle's current value if it exists, zero otherwise. + /// @return isValid True if value exists, false otherwise. + function peep() external view returns (uint value, bool isValid); +} + +// forgefmt: disable-end diff --git a/src/Scribe.sol b/src/Scribe.sol index da29e69..bc37e26 100644 --- a/src/Scribe.sol +++ b/src/Scribe.sol @@ -1,531 +1,564 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.16; -// forgefmt: disable-start - -import {IChronicle} from "chronicle-std/IChronicle.sol"; -import {Auth} from "chronicle-std/auth/Auth.sol"; -import {Toll} from "chronicle-std/toll/Toll.sol"; - -import {IScribe} from "./IScribe.sol"; - -import {LibSchnorr} from "./libs/LibSchnorr.sol"; -import {LibSecp256k1} from "./libs/LibSecp256k1.sol"; - -/** - * @title Scribe - * @custom:version 2.0.1 - * - * @notice Efficient Schnorr multi-signature based Oracle - * - * @author Chronicle Labs, Inc - * @custom:security-contact security@chroniclelabs.org - */ -contract Scribe is IScribe, Auth, Toll { - using LibSchnorr for LibSecp256k1.Point; - using LibSecp256k1 for LibSecp256k1.Point; - using LibSecp256k1 for LibSecp256k1.JacobianPoint; - - /// @inheritdoc IScribe - uint8 public constant decimals = 18; - - /// @inheritdoc IScribe - bytes32 public constant feedRegistrationMessage = keccak256( - abi.encodePacked( - "\x19Ethereum Signed Message:\n32", - keccak256("Chronicle Feed Registration") - ) - ); - - /// @inheritdoc IChronicle - bytes32 public immutable wat; - - // -- Storage -- - - /// @dev Scribe's current value and corresponding age. - PokeData internal _pokeData; - - /// @dev Statically allocated array of feeds' public keys. - /// Indexed via the public keys address' highest-order byte. - LibSecp256k1.Point[256] internal _pubKeys; - - /// @inheritdoc IScribe - /// @dev Note to have as last in storage to enable downstream contracts to - /// pack the slot. - uint8 public bar; - - // -- Constructor -- - - constructor(address initialAuthed, bytes32 wat_) - payable - Auth(initialAuthed) - { - require(wat_ != 0); - - // Set wat immutable. - wat = wat_; - - // Let initial bar be 2. - _setBar(2); - } - - // -- Poke Functionality -- - - /// @dev Optimized function selector: 0x00000082. - /// Note that this function is _not_ defined via the IScribe interface - /// and one should _not_ depend on it. - function poke_optimized_7136211( - PokeData calldata pokeData, - SchnorrData calldata schnorrData - ) external { - _poke(pokeData, schnorrData); - } - - /// @inheritdoc IScribe - function poke(PokeData calldata pokeData, SchnorrData calldata schnorrData) - external - { - _poke(pokeData, schnorrData); - } - - function _poke(PokeData calldata pokeData, SchnorrData calldata schnorrData) - internal - virtual - { - // Revert if pokeData stale. - if (pokeData.age <= _pokeData.age) { - revert StaleMessage(pokeData.age, _pokeData.age); - } - // Revert if pokeData from the future. - if (pokeData.age > uint32(block.timestamp)) { - revert FutureMessage(pokeData.age, uint32(block.timestamp)); - } - - // Revert if schnorrData does not prove integrity of pokeData. - bool ok; - bytes memory err; - // forgefmt: disable-next-item - (ok, err) = _verifySchnorrSignature( - constructPokeMessage(pokeData), - schnorrData - ); - if (!ok) { - _revert(err); - } - - // Store pokeData's val in _pokeData storage and set its age to now. - _pokeData.val = pokeData.val; - _pokeData.age = uint32(block.timestamp); - - emit Poked(msg.sender, pokeData.val, pokeData.age); - } - - /// @inheritdoc IScribe - function constructPokeMessage(PokeData memory pokeData) - public - view - returns (bytes32) - { - return keccak256( - abi.encodePacked( - "\x19Ethereum Signed Message:\n32", - keccak256(abi.encodePacked(wat, pokeData.val, pokeData.age)) - ) - ); - } - - // -- Schnorr Signature Verification -- - - /// @inheritdoc IScribe - function isAcceptableSchnorrSignatureNow( - bytes32 message, - SchnorrData calldata schnorrData - ) external view returns (bool) { - bool ok; - (ok, /*err*/ ) = _verifySchnorrSignature(message, schnorrData); - - return ok; - } - - /// @custom:invariant Reverts iff out of gas. - /// @custom:invariant Runtime is O(bar). - function _verifySchnorrSignature( - bytes32 message, - SchnorrData calldata schnorrData - ) internal view returns (bool, bytes memory) { - // Let feedPubKey be the currently processed feed's public key. - LibSecp256k1.Point memory feedPubKey; - // Let feedId be the currently processed feed's id. - uint8 feedId; - // Let aggPubKey be the sum of processed feeds' public keys. - // Note that Jacobian coordinates are used. - LibSecp256k1.JacobianPoint memory aggPubKey; - // Let bloom be a bloom filter to check for double signing attempts. - uint bloom; - - // Fail if number feeds unequal to bar. - // - // Note that requiring equality constrains the verification's runtime - // from Ω(bar) to Θ(bar). - uint numberFeeds = schnorrData.feedIds.length; - if (numberFeeds != bar) { - return (false, _errorBarNotReached(uint8(numberFeeds), bar)); - } - - // Initiate feed variables with schnorrData's 0's feed index. - feedId = uint8(schnorrData.feedIds[0]); - feedPubKey = _pubKeys[feedId]; - - // Fail if feed not lifted. - if (feedPubKey.isZeroPoint()) { - return (false, _errorInvalidFeedId(feedId)); - } - - // Initiate bloom filter with feedId set. - bloom = 1 << feedId; - - // Initiate aggPubKey with value of first feed's public key. - aggPubKey = feedPubKey.toJacobian(); - - for (uint8 i = 1; i < numberFeeds;) { - // Update feed variables. - feedId = uint8(schnorrData.feedIds[i]); - feedPubKey = _pubKeys[feedId]; - - // Fail if feed not lifted. - if (feedPubKey.isZeroPoint()) { - return (false, _errorInvalidFeedId(feedId)); - } - - // Fail if double signing attempted. - if (bloom & (1 << feedId) != 0) { - return (false, _errorDoubleSigningAttempted(feedId)); - } - // Update bloom filter. - bloom |= 1 << feedId; - - // assert(aggPubKey.x != feedPubKey.x); // Indicates rogue-key attack - - // Add feedPubKey to already aggregated public keys. - aggPubKey.addAffinePoint(feedPubKey); - - // forgefmt: disable-next-item - unchecked { ++i; } - } - - // Fail if signature verification fails. - bool ok = aggPubKey.toAffine().verifySignature( - message, schnorrData.signature, schnorrData.commitment - ); - if (!ok) { - return (false, _errorSchnorrSignatureInvalid()); - } - - // Otherwise Schnorr signature is valid. - return (true, new bytes(0)); - } - - // -- Toll'ed Read Functionality -- - - // - IChronicle Functions - - /// @inheritdoc IChronicle - /// @dev Only callable by toll'ed address. - function read() external view virtual toll returns (uint) { - uint val = _pokeData.val; - require(val != 0); - return val; - } - - /// @inheritdoc IChronicle - /// @dev Only callable by toll'ed address. - function tryRead() external view virtual toll returns (bool, uint) { - uint val = _pokeData.val; - return (val != 0, val); - } - - /// @inheritdoc IChronicle - /// @dev Only callable by toll'ed address. - function readWithAge() external view virtual toll returns (uint, uint) { - uint val = _pokeData.val; - uint age = _pokeData.age; - require(val != 0); - return (val, age); - } - - /// @inheritdoc IChronicle - /// @dev Only callable by toll'ed address. - function tryReadWithAge() - external - view - virtual - toll - returns (bool, uint, uint) - { - uint val = _pokeData.val; - uint age = _pokeData.age; - return val != 0 ? (true, val, age) : (false, 0, 0); - } - - // - MakerDAO Compatibility - - /// @inheritdoc IScribe - /// @dev Only callable by toll'ed address. - function peek() external view virtual toll returns (uint, bool) { - uint val = _pokeData.val; - return (val, val != 0); - } - - /// @inheritdoc IScribe - /// @dev Only callable by toll'ed address. - function peep() external view virtual toll returns (uint, bool) { - uint val = _pokeData.val; - return (val, val != 0); - } - - // - Chainlink Compatibility - - /// @inheritdoc IScribe - /// @dev Only callable by toll'ed address. - function latestRoundData() - external - view - virtual - toll - returns ( - uint80 roundId, - int answer, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) - { - roundId = 1; - answer = int(uint(_pokeData.val)); - // assert(uint(answer) == uint(_pokeData.val)); - startedAt = 0; - updatedAt = _pokeData.age; - answeredInRound = roundId; - } - - /// @inheritdoc IScribe - /// @dev Only callable by toll'ed address. - function latestAnswer() external view virtual toll returns (int) { - uint val = _pokeData.val; - return int(val); - } - - // -- Public Read Functionality -- - - /// @inheritdoc IScribe - function feeds(address who) external view returns (bool) { - uint8 feedId = uint8(uint(uint160(who)) >> 152); - - LibSecp256k1.Point memory pubKey = _pubKeys[feedId]; - - return !pubKey.isZeroPoint() && pubKey.toAddress() == who; - } - - /// @inheritdoc IScribe - function feeds(uint8 feedId) external view returns (bool, address) { - LibSecp256k1.Point memory pubKey = _pubKeys[feedId]; - - return pubKey.isZeroPoint() - ? (false, address(0)) - : (true, pubKey.toAddress()); - } - - /// @inheritdoc IScribe - function feeds() external view returns (address[] memory) { - address[] memory feeds_ = new address[](256); - - LibSecp256k1.Point memory pubKey; - address feed; - uint ctr; - for (uint i; i < 256;) { - pubKey = _pubKeys[uint8(i)]; - - if (!pubKey.isZeroPoint()) { - feed = pubKey.toAddress(); - - feeds_[ctr] = feed; - - // forgefmt: disable-next-item - unchecked { ++ctr; } - } - - // forgefmt: disable-next-item - unchecked { ++i; } - } - - assembly ("memory-safe") { - mstore(feeds_, ctr) - } - - return feeds_; - } - - // -- Auth'ed Functionality -- - - /// @inheritdoc IScribe - function lift(LibSecp256k1.Point memory pubKey, ECDSAData memory ecdsaData) - external - auth - returns (uint8) - { - return _lift(pubKey, ecdsaData); - } - - /// @inheritdoc IScribe - function lift( - LibSecp256k1.Point[] memory pubKeys, - ECDSAData[] memory ecdsaDatas - ) external auth returns (uint8[] memory) { - require(pubKeys.length == ecdsaDatas.length); - - uint8[] memory feedIds = new uint8[](pubKeys.length); - for (uint i; i < pubKeys.length;) { - feedIds[i] = _lift(pubKeys[i], ecdsaDatas[i]); - - // forgefmt: disable-next-item - unchecked { ++i; } - } - - return feedIds; - } - - function _lift(LibSecp256k1.Point memory pubKey, ECDSAData memory ecdsaData) - internal - returns (uint8) - { - address feed = pubKey.toAddress(); - // assert(feed != address(0)); - - // forgefmt: disable-next-item - address recovered = ecrecover( - feedRegistrationMessage, - ecdsaData.v, - ecdsaData.r, - ecdsaData.s - ); - require(feed == recovered); - - uint8 feedId = uint8(uint(uint160(feed)) >> 152); - - LibSecp256k1.Point memory sPubKey = _pubKeys[feedId]; - if (sPubKey.isZeroPoint()) { - _pubKeys[feedId] = pubKey; - - emit FeedLifted(msg.sender, feed); - } else { - // Note to be idempotent. However, disallow updating an id's feed - // via lifting without dropping the previous feed. - require(feed == sPubKey.toAddress()); - } - - return feedId; - } - - /// @inheritdoc IScribe - function drop(uint8 feedId) external auth { - _drop(msg.sender, feedId); - } - - /// @inheritdoc IScribe - function drop(uint8[] memory feedIds) external auth { - for (uint i; i < feedIds.length;) { - _drop(msg.sender, feedIds[i]); - - // forgefmt: disable-next-item - unchecked { ++i; } - } - } - - function _drop(address caller, uint8 feedId) internal virtual { - LibSecp256k1.Point memory pubKey = _pubKeys[feedId]; - if (!pubKey.isZeroPoint()) { - delete _pubKeys[feedId]; - - emit FeedDropped(caller, pubKey.toAddress()); - } - } - - /// @inheritdoc IScribe - function setBar(uint8 bar_) external auth { - _setBar(bar_); - } - - function _setBar(uint8 bar_) internal virtual { - require(bar_ != 0); - - if (bar != bar_) { - emit BarUpdated(msg.sender, bar, bar_); - bar = bar_; - } - } - - // -- Internal Helpers -- - - function _revert(bytes memory err) internal pure { - // assert(err.length != 0); - assembly ("memory-safe") { - let size := mload(err) - let offset := add(err, 0x20) - revert(offset, size) - } - } - - function _errorBarNotReached(uint8 got, uint8 want) - internal - pure - returns (bytes memory) - { - // assert(got != want); - return abi.encodeWithSelector(IScribe.BarNotReached.selector, got, want); - } - - function _errorInvalidFeedId(uint8 feedId) - internal - pure - returns (bytes memory) - { - // assert(_pubKeys[feedId].isZeroPoint()); - return abi.encodeWithSelector(IScribe.InvalidFeedId.selector, feedId); - } - - function _errorDoubleSigningAttempted(uint8 feedId) - internal - pure - returns (bytes memory) - { - return abi.encodeWithSelector( - IScribe.DoubleSigningAttempted.selector, feedId - ); - } - - function _errorSchnorrSignatureInvalid() - internal - pure - returns (bytes memory) - { - return abi.encodeWithSelector(IScribe.SchnorrSignatureInvalid.selector); - } - - // -- Overridden Toll Functions -- - - /// @dev Defines authorization for IToll's authenticated functions. - function toll_auth() internal override(Toll) auth {} -} - -/** - * @dev Contract overwrite to deploy contract instances with specific naming. - * - * For more info, see docs/Deployment.md. - */ -contract Chronicle_BASE_QUOTE_COUNTER is Scribe { - // @todo ^^^^ ^^^^^ ^^^^^^^ Adjust name of Scribe instance. - constructor(address initialAuthed, bytes32 wat_) - Scribe(initialAuthed, wat_) - {} -} - -// forgefmt: disable-end +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.16; +// forgefmt: disable-start + +import {IChronicle} from "chronicle-std/IChronicle.sol"; +import {Auth} from "chronicle-std/auth/Auth.sol"; +import {Toll} from "chronicle-std/toll/Toll.sol"; + +import {IScribe} from "./IScribe.sol"; + +import {LibSchnorr} from "./libs/LibSchnorr.sol"; +import {LibSecp256k1} from "./libs/LibSecp256k1.sol"; + +/** + * @title Scribe + * @custom:version 2.0.1 + * + * @notice Efficient Schnorr multi-signature based Oracle + * + * @author Chronicle Labs, Inc + * @custom:security-contact security@chroniclelabs.org + */ +contract Scribe is IScribe, Auth, Toll { + using LibSchnorr for LibSecp256k1.Point; + using LibSecp256k1 for LibSecp256k1.Point; + using LibSecp256k1 for LibSecp256k1.JacobianPoint; + + /// @inheritdoc IScribe + uint8 public constant decimals = 18; + + /// @inheritdoc IScribe + bytes32 public constant feedRegistrationMessage = keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256("Chronicle Feed Registration") + ) + ); + + /// @inheritdoc IChronicle + bytes32 public immutable wat; + + // -- Storage -- + + /// @dev Scribe's current value and corresponding age. + PokeData internal _pokeData; + + /// @dev Statically allocated array of feeds' public keys. + /// Indexed via the public keys address' highest-order byte. + LibSecp256k1.Point[256] internal _pubKeys; + + /// @inheritdoc IScribe + /// @dev Note to have as last in storage to enable downstream contracts to + /// pack the slot. + uint8 public bar; + + // -- Constructor -- + + constructor(address initialAuthed, bytes32 wat_) + payable + Auth(initialAuthed) + { + require(wat_ != 0); + + // Set wat immutable. + wat = wat_; + + // Let initial bar be 2. + _setBar(2); + } + + // -- Poke Functionality -- + + /// @dev Optimized function selector: 0x00000082. + /// Note that this function is _not_ defined via the IScribe interface + /// and one should _not_ depend on it. + function poke_optimized_7136211( + PokeData calldata pokeData, + SchnorrData calldata schnorrData + ) external { + _poke(pokeData, schnorrData); + } + + /// @inheritdoc IScribe + function poke(PokeData calldata pokeData, SchnorrData calldata schnorrData) + external + { + _poke(pokeData, schnorrData); + } + + function _poke(PokeData calldata pokeData, SchnorrData calldata schnorrData) + internal + virtual + { + // Revert if pokeData stale. + if (pokeData.age <= _pokeData.age) { + revert StaleMessage(pokeData.age, _pokeData.age); + } + // Revert if pokeData from the future. + if (pokeData.age > uint32(block.timestamp)) { + revert FutureMessage(pokeData.age, uint32(block.timestamp)); + } + + // Revert if schnorrData does not prove integrity of pokeData. + bool ok; + bytes memory err; + // forgefmt: disable-next-item + (ok, err) = _verifySchnorrSignature( + constructPokeMessage(pokeData), + schnorrData + ); + if (!ok) { + _revert(err); + } + + // Store pokeData's val in _pokeData storage and set its age to now. + _pokeData.val = pokeData.val; + _pokeData.age = uint32(block.timestamp); + + emit Poked(msg.sender, pokeData.val, pokeData.age); + } + + /// @inheritdoc IScribe + function constructPokeMessage(PokeData memory pokeData) + public + view + returns (bytes32) + { + // Note the domain separation via `address(this)` and `block.chainid`. + // + // Without them the signed payload is fully determined by + // (wat, val, age), which is identical for every Scribe instance sharing + // the same `wat` on every chain. A Schnorr signature produced for one + // instance could then be replayed verbatim against any other deployment + // of the same feed set — including on a different chain, where the + // validator set and governance may be entirely different. Binding the + // message to this contract and chain makes such a replay impossible. + return keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256( + abi.encodePacked( + address(this), + block.chainid, + wat, + pokeData.val, + pokeData.age + ) + ) + ) + ); + } + + // -- Schnorr Signature Verification -- + + /// @inheritdoc IScribe + function isAcceptableSchnorrSignatureNow( + bytes32 message, + SchnorrData calldata schnorrData + ) external view returns (bool) { + bool ok; + (ok, /*err*/ ) = _verifySchnorrSignature(message, schnorrData); + + return ok; + } + + /// @custom:invariant Reverts iff out of gas. + /// @custom:invariant Runtime is O(bar). + function _verifySchnorrSignature( + bytes32 message, + SchnorrData calldata schnorrData + ) internal view returns (bool, bytes memory) { + // Let feedPubKey be the currently processed feed's public key. + LibSecp256k1.Point memory feedPubKey; + // Let feedId be the currently processed feed's id. + uint8 feedId; + // Let aggPubKey be the sum of processed feeds' public keys. + // Note that Jacobian coordinates are used. + LibSecp256k1.JacobianPoint memory aggPubKey; + // Let bloom be a bloom filter to check for double signing attempts. + uint bloom; + + // Fail if number feeds unequal to bar. + // + // Note that requiring equality constrains the verification's runtime + // from Ω(bar) to Θ(bar). + uint numberFeeds = schnorrData.feedIds.length; + if (numberFeeds != bar) { + return (false, _errorBarNotReached(uint8(numberFeeds), bar)); + } + + // Initiate feed variables with schnorrData's 0's feed index. + feedId = uint8(schnorrData.feedIds[0]); + feedPubKey = _pubKeys[feedId]; + + // Fail if feed not lifted. + if (feedPubKey.isZeroPoint()) { + return (false, _errorInvalidFeedId(feedId)); + } + + // Initiate bloom filter with feedId set. + bloom = 1 << feedId; + + // Initiate aggPubKey with value of first feed's public key. + aggPubKey = feedPubKey.toJacobian(); + + for (uint8 i = 1; i < numberFeeds;) { + // Update feed variables. + feedId = uint8(schnorrData.feedIds[i]); + feedPubKey = _pubKeys[feedId]; + + // Fail if feed not lifted. + if (feedPubKey.isZeroPoint()) { + return (false, _errorInvalidFeedId(feedId)); + } + + // Fail if double signing attempted. + if (bloom & (1 << feedId) != 0) { + return (false, _errorDoubleSigningAttempted(feedId)); + } + // Update bloom filter. + bloom |= 1 << feedId; + + // assert(aggPubKey.x != feedPubKey.x); // Indicates rogue-key attack + + // Add feedPubKey to already aggregated public keys. + aggPubKey.addAffinePoint(feedPubKey); + + // forgefmt: disable-next-item + unchecked { ++i; } + } + + // Fail if signature verification fails. + bool ok = aggPubKey.toAffine().verifySignature( + message, schnorrData.signature, schnorrData.commitment + ); + if (!ok) { + return (false, _errorSchnorrSignatureInvalid()); + } + + // Otherwise Schnorr signature is valid. + return (true, new bytes(0)); + } + + // -- Toll'ed Read Functionality -- + + // - IChronicle Functions + + /// @inheritdoc IChronicle + /// @dev Only callable by toll'ed address. + function read() external view virtual toll returns (uint) { + uint val = _pokeData.val; + require(val != 0); + return val; + } + + /// @inheritdoc IChronicle + /// @dev Only callable by toll'ed address. + function tryRead() external view virtual toll returns (bool, uint) { + uint val = _pokeData.val; + return (val != 0, val); + } + + /// @inheritdoc IChronicle + /// @dev Only callable by toll'ed address. + function readWithAge() external view virtual toll returns (uint, uint) { + uint val = _pokeData.val; + uint age = _pokeData.age; + require(val != 0); + return (val, age); + } + + /// @inheritdoc IChronicle + /// @dev Only callable by toll'ed address. + function tryReadWithAge() + external + view + virtual + toll + returns (bool, uint, uint) + { + uint val = _pokeData.val; + uint age = _pokeData.age; + return val != 0 ? (true, val, age) : (false, 0, 0); + } + + // - MakerDAO Compatibility + + /// @inheritdoc IScribe + /// @dev Only callable by toll'ed address. + function peek() external view virtual toll returns (uint, bool) { + uint val = _pokeData.val; + return (val, val != 0); + } + + /// @inheritdoc IScribe + /// @dev Only callable by toll'ed address. + function peep() external view virtual toll returns (uint, bool) { + uint val = _pokeData.val; + return (val, val != 0); + } + + // - Chainlink Compatibility + + /// @inheritdoc IScribe + /// @dev Only callable by toll'ed address. + function latestRoundData() + external + view + virtual + toll + returns ( + uint80 roundId, + int answer, + uint startedAt, + uint updatedAt, + uint80 answeredInRound + ) + { + roundId = 1; + answer = int(uint(_pokeData.val)); + // assert(uint(answer) == uint(_pokeData.val)); + startedAt = 0; + updatedAt = _pokeData.age; + answeredInRound = roundId; + } + + /// @inheritdoc IScribe + /// @dev Only callable by toll'ed address. + function latestAnswer() external view virtual toll returns (int) { + uint val = _pokeData.val; + return int(val); + } + + // -- Public Read Functionality -- + + /// @inheritdoc IScribe + function feeds(address who) external view returns (bool) { + uint8 feedId = uint8(uint(uint160(who)) >> 152); + + LibSecp256k1.Point memory pubKey = _pubKeys[feedId]; + + return !pubKey.isZeroPoint() && pubKey.toAddress() == who; + } + + /// @inheritdoc IScribe + function feeds(uint8 feedId) external view returns (bool, address) { + LibSecp256k1.Point memory pubKey = _pubKeys[feedId]; + + return pubKey.isZeroPoint() + ? (false, address(0)) + : (true, pubKey.toAddress()); + } + + /// @inheritdoc IScribe + function feeds() external view returns (address[] memory) { + address[] memory feeds_ = new address[](256); + + LibSecp256k1.Point memory pubKey; + address feed; + uint ctr; + for (uint i; i < 256;) { + pubKey = _pubKeys[uint8(i)]; + + if (!pubKey.isZeroPoint()) { + feed = pubKey.toAddress(); + + feeds_[ctr] = feed; + + // forgefmt: disable-next-item + unchecked { ++ctr; } + } + + // forgefmt: disable-next-item + unchecked { ++i; } + } + + assembly ("memory-safe") { + mstore(feeds_, ctr) + } + + return feeds_; + } + + // -- Auth'ed Functionality -- + + /// @inheritdoc IScribe + function lift(LibSecp256k1.Point memory pubKey, ECDSAData memory ecdsaData) + external + auth + returns (uint8) + { + return _lift(pubKey, ecdsaData); + } + + /// @inheritdoc IScribe + function lift( + LibSecp256k1.Point[] memory pubKeys, + ECDSAData[] memory ecdsaDatas + ) external auth returns (uint8[] memory) { + require(pubKeys.length == ecdsaDatas.length); + + uint8[] memory feedIds = new uint8[](pubKeys.length); + for (uint i; i < pubKeys.length;) { + feedIds[i] = _lift(pubKeys[i], ecdsaDatas[i]); + + // forgefmt: disable-next-item + unchecked { ++i; } + } + + return feedIds; + } + + function _lift(LibSecp256k1.Point memory pubKey, ECDSAData memory ecdsaData) + internal + returns (uint8) + { + address feed = pubKey.toAddress(); + + // Reject signature malleability. + // + // `ecrecover` accepts both (r, s) and (r, Q - s) for the same signer, so + // without this guard a single valid feed registration has two distinct + // on-chain encodings. This is the standard defence-in-depth check for + // ECDSA verification and preserves a unique byte encoding per feed. + // + // Note that `ecrecover` reverts on r == 0 or s == 0 and returns + // address(0) on any other invalid input; the `feed == recovered` check + // below already rejects that, because a valid on-curve public key never + // hashes to address(0). + require( + ecdsaData.s + <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, + "Scribe: signature s not canonical" + ); + + // forgefmt: disable-next-item + address recovered = ecrecover( + feedRegistrationMessage, + ecdsaData.v, + ecdsaData.r, + ecdsaData.s + ); + require(feed == recovered); + + uint8 feedId = uint8(uint(uint160(feed)) >> 152); + + LibSecp256k1.Point memory sPubKey = _pubKeys[feedId]; + if (sPubKey.isZeroPoint()) { + _pubKeys[feedId] = pubKey; + + emit FeedLifted(msg.sender, feed); + } else { + // Note to be idempotent. However, disallow updating an id's feed + // via lifting without dropping the previous feed. + require(feed == sPubKey.toAddress()); + } + + return feedId; + } + + /// @inheritdoc IScribe + function drop(uint8 feedId) external auth { + _drop(msg.sender, feedId); + } + + /// @inheritdoc IScribe + function drop(uint8[] memory feedIds) external auth { + for (uint i; i < feedIds.length;) { + _drop(msg.sender, feedIds[i]); + + // forgefmt: disable-next-item + unchecked { ++i; } + } + } + + function _drop(address caller, uint8 feedId) internal virtual { + LibSecp256k1.Point memory pubKey = _pubKeys[feedId]; + if (!pubKey.isZeroPoint()) { + delete _pubKeys[feedId]; + + emit FeedDropped(caller, pubKey.toAddress()); + } + } + + /// @inheritdoc IScribe + function setBar(uint8 bar_) external auth { + _setBar(bar_); + } + + function _setBar(uint8 bar_) internal virtual { + require(bar_ != 0); + + if (bar != bar_) { + emit BarUpdated(msg.sender, bar, bar_); + bar = bar_; + } + } + + // -- Internal Helpers -- + + function _revert(bytes memory err) internal pure { + // assert(err.length != 0); + assembly ("memory-safe") { + let size := mload(err) + let offset := add(err, 0x20) + revert(offset, size) + } + } + + function _errorBarNotReached(uint8 got, uint8 want) + internal + pure + returns (bytes memory) + { + // assert(got != want); + return abi.encodeWithSelector(IScribe.BarNotReached.selector, got, want); + } + + function _errorInvalidFeedId(uint8 feedId) + internal + pure + returns (bytes memory) + { + // assert(_pubKeys[feedId].isZeroPoint()); + return abi.encodeWithSelector(IScribe.InvalidFeedId.selector, feedId); + } + + function _errorDoubleSigningAttempted(uint8 feedId) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + IScribe.DoubleSigningAttempted.selector, feedId + ); + } + + function _errorSchnorrSignatureInvalid() + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector(IScribe.SchnorrSignatureInvalid.selector); + } + + // -- Overridden Toll Functions -- + + /// @dev Defines authorization for IToll's authenticated functions. + function toll_auth() internal override(Toll) auth {} +} + +/** + * @dev Contract overwrite to deploy contract instances with specific naming. + * + * For more info, see docs/Deployment.md. + */ +contract Chronicle_BASE_QUOTE_COUNTER is Scribe { + // @todo ^^^^ ^^^^^ ^^^^^^^ Adjust name of Scribe instance. + constructor(address initialAuthed, bytes32 wat_) + Scribe(initialAuthed, wat_) + {} +} + +// forgefmt: disable-end