Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[WIP] Adds useful gadgets #56

Open
wants to merge 8 commits into
base: qol-lints
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@
[submodule "lib/sgx-tcbInfos"]
path = lib/sgx-tcbInfos
url = https://github.com/Ruteri/sgx-tcbInfos
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
24 changes: 24 additions & 0 deletions demos/confstore/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at dbb610
4 changes: 4 additions & 0 deletions src/AndromedaForge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ contract AndromedaForge is IAndromeda {
activeHost = host;
}

function randomHost() public {
switchHost(iToHex(abi.encodePacked(localRandom())));
}

function iToHex(bytes memory buffer) public pure returns (string memory) {
bytes memory converted = new bytes(buffer.length * 2);
bytes memory _base = "0123456789abcdef";
Expand Down
1 change: 1 addition & 0 deletions src/IAndromeda.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ interface IAndromeda is IHash {
bytes body;
bool withFlashbotsSignature;
}

function doHTTPRequest(HttpRequest memory request) external returns (bytes memory);
}
10 changes: 9 additions & 1 deletion src/crypto/secp256k1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ library Secp256k1 {
}

function verify(address signer, bytes32 digest, bytes memory sig) internal pure returns (bool) {
return signer == recover_signer(digest, sig);
}

function recover_signer(bytes32 digest, bytes memory sig) internal pure returns (address) {
uint8 v;
bytes32 r;
bytes32 s;
Expand All @@ -23,7 +27,7 @@ library Secp256k1 {
r := mload(add(sig, 33))
s := mload(add(sig, 65))
}
return signer == ecrecover(digest, v, r, s);
return ecrecover(digest, v, r, s);
}

function sign(uint256 privateKey, bytes32 digest) internal pure returns (bytes memory) {
Expand Down Expand Up @@ -64,6 +68,10 @@ library Secp256k1 {
return abi.encodePacked(v, bytes32(r), bytes32(s));
}

function deriveAddress(bytes memory pubkey) internal pure returns (address) {
return address(uint160(uint256(keccak256(pubkey))));
}

function deriveAddress(uint256 privKey) internal pure returns (address) {
(uint256 qx, uint256 qy) = derivePubKey(privKey);
bytes memory ser = bytes.concat(bytes32(qx), bytes32(qy));
Expand Down
31 changes: 31 additions & 0 deletions src/gadgets/AttestationGadget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
pragma solidity ^0.8.19;

import {IAndromeda} from "src/IAndromeda.sol";

abstract contract AttestationGadget {
function andromeda() internal view virtual returns (IAndromeda);

modifier onchain_verify(bytes memory user_data, bytes memory attestation) virtual {
onchain_verify_fn(msg.sig, user_data, attestation);
_;
}

function onchain_verify_fn(bytes4 target, bytes memory user_data, bytes memory attestation) internal virtual {
require(
andromeda().verifySgx(
address(this), keccak256(abi.encodePacked(target, address(this), user_data)), attestation
),
"invalid attestation data"
);
}

function offchain_attest(bytes4 target, bytes memory user_data)
internal
virtual
returns (bytes memory attestation)
{
return andromeda().attestSgx(keccak256(abi.encodePacked(target, address(this), user_data)));
}
}

/* TODO: provide signature-based attestation similar to KeyManagerBase as an alternative to the above */
50 changes: 50 additions & 0 deletions src/gadgets/AuthGadget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
pragma solidity ^0.8.19;

import {Secp256k1} from "src/crypto/secp256k1.sol";

import {VersionGadget} from "src/gadgets/VersionGadget.sol";
import {CensorshipResistanceGadget} from "src/gadgets/CensorshipResistanceGadget.sol";

abstract contract AuthGadget is VersionGadget, CensorshipResistanceGadget {
/* https://docs.openzeppelin.com/contracts/4.x/api/access#AccessControl */
function hasRole(bytes32 role, address account) public view virtual returns (bool);

// function grantRole(bytes32 role, bytes memory quote, bytes memory user_data, address account) internal virtual returns (bool);
// function revokeRole(bytes32 role, address account) virtual internal returns (bool);
// function renounceRole(bytes32 role, address account) virtual internal returns (bool);

// Reentrancy check and signer lookup
address internal auth_signer;

// auth version relying only on the transaction signature
modifier auth(bytes32 role) {
// Nonreentrant
require(auth_signer == address(0), "auth: reentrancy");
require(hasRole(role, msg.sender), "auth: sender unauthorized");

auth_signer = msg.sender;
_;
auth_signer = address(0);
}

modifier auth_query(bytes32 role, bytes memory query, bytes memory signature) {
{
require(auth_signer == address(0), "auth: reentrancy");
bytes32 digest = keccak256(abi.encodePacked(current_version(), current_epoch(), msg.sig, query));
address signer = Secp256k1.recover_signer(digest, signature);
require(Secp256k1.verify(signer, digest, signature), "auth: invalid signature");
require(hasRole(role, signer), "auth: signer unauthorized");

auth_signer = signer;
}

_;

auth_signer = address(0);
}

// Local node helper
function auth_query_hash(bytes4 method, bytes memory query) public returns (bytes32) {
return keccak256(abi.encodePacked(current_version(), current_epoch(), method, query));
}
}
129 changes: 129 additions & 0 deletions src/gadgets/CensorshipResistanceGadget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
pragma solidity ^0.8.19;

import {AttestationGadget} from "src/gadgets/AttestationGadget.sol";

abstract contract CensorshipResistanceGadget is AttestationGadget {
// require explicit onchain challenge on kettle restart
mapping(bytes32 => bool) private cr_challenges;
uint256 private max_offchain_calls_per_epoch; /* hard limit how many times an offchain call can be performed without epoch bump (0 - no limit) */

constructor(uint256 _max_offchain_calls_per_epoch) {
max_offchain_calls_per_epoch = _max_offchain_calls_per_epoch;
}

function offchain_restart_challenge() public returns (bytes32 challenge, bytes memory attestation) {
challenge = _get_local_challenge(); /* do not override previous local challenge to prevent dos */
require(!cr_challenges[challenge], "cr: challenge already provided"); /* already challenged */
attestation = offchain_attest(this.onchain_restart_challenge.selector, abi.encodePacked(challenge));
}

function onchain_restart_challenge(bytes32 challenge, bytes memory attestation)
public
onchain_verify(abi.encodePacked(challenge), attestation)
{
cr_challenges[challenge] = true;
}
Ruteri marked this conversation as resolved.
Show resolved Hide resolved
// epoch as seen in volatile memory of the kettle (can be delayed if kettle operator censors calls)

function _get_local_challenge() private returns (bytes32 challenge) {
challenge = andromeda().volatileGet("local_challenge");
if (challenge == bytes32(0)) {
challenge = andromeda().localRandom();
_set_uses_since_local_epoch_update(0);
_set_local_challenge(challenge);
}
}

function _set_local_challenge(bytes32 challenge) private {
andromeda().volatileSet("local_challenge", challenge);
}

// epoch as seen onchain (can be delayed if chain is censored)
uint256 public cr_epoch;

function current_epoch() public view returns (uint256) {
return cr_epoch;
}

// epoch as seen in volatile memory of the kettle (can be delayed if kettle operator censors calls)
function _get_local_epoch() private returns (uint256 epoch) {
epoch = uint256(andromeda().volatileGet("local_epoch"));
}

function _set_local_epoch(uint256 epoch) private {
_set_uses_since_local_epoch_update(0);
andromeda().volatileSet(("local_epoch"), bytes32(epoch));
}

// Hard limit amount of requests the contract will allow be processed before it forces a re-challenge
// The contract is expected to be bumped every now and then, and if we stop seeing cr epoch bumps
// we should assume the kettle operator is maliciously filtering out requests and force chain sync by clearing challenge
function _set_uses_since_local_epoch_update(uint256 uses) private {
andromeda().volatileSet(("uses_since_epoch_update"), bytes32(uses));
}

function _get_uses_since_local_epoch_update() private returns (uint256 uses) {
uses = uint256(andromeda().volatileGet("uses_since_epoch_update"));
}

function verify_uses_since_last_epoch_update() private {
if (max_offchain_calls_per_epoch == 0) {
return;
}
uint256 uses_since_last_update = _get_uses_since_local_epoch_update();
if (uses_since_last_update >= max_offchain_calls_per_epoch) {
// force onchain challenge
_set_local_challenge(andromeda().localRandom());
_set_uses_since_local_epoch_update(0);
require(false, "cr: epoch update not seen for too long");
} else {
_set_uses_since_local_epoch_update(uses_since_last_update + 1);
}
}

// Onchain!
function bump_cr_epoch(uint256 epoch) internal {
require(cr_epoch == epoch, "cr: bump with invalid epoch");
cr_epoch = epoch + 1;
}

// add the following modifiers to your calls to make them censorship resistant
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, delighted to see a censorship resistance queue, like Ethereum Alarm Clock but for kettle apps. This looks like a nice way to do it with a modifier. I didn't follow how it works yet though, I was expecting a queue I guess

// upgrading the epoch via cr_check call will make the kettle *refuse* to serve previous epochs through this cr gadget
// until the chain state matches, *to all callers* to prevent selective censorship
// VERY IMPORTANT! The caller must verify who sets the epoch as it can otherwise be a DoS vector!
// use for things like forcing key rotations
modifier force_cr(uint256 epoch) {
/* IMPORTANT! This is set regardless of revert status */
verify_uses_since_last_epoch_update();
require(cr_challenges[_get_local_challenge()], "cr: challenge not provided");
require(epoch >= cr_epoch, "cr: old epoch"); // resubmission, ignore - user's view is outdated
uint256 c_local_epoch = _get_local_epoch();
if (epoch > c_local_epoch) {
// user requested epoch higher than kettle's local view - this means noone else did so before (can be benign)
_set_local_epoch(epoch); // refuse to serve anything less than epoch in subsequent calls
c_local_epoch = epoch; // use max(local, epoch) in the rest of the function
}
require(cr_epoch >= c_local_epoch, "cr: local view ahead of chain"); // local view of the chain must be at least until local epoch - otherwise the chain is being censored
require(cr_epoch == epoch, "cr: caller epoch mismatch"); // and request and chain epochs must match - otherwise we are getting spoofed
_;
}

// more benign version - does not enforce cr for other callers
// the caller here only makes sure the local chain view is at least up to epoch
modifier check_cr(uint256 epoch) {
{
verify_uses_since_last_epoch_update();
require(cr_challenges[_get_local_challenge()], "cr: challenge not provided");
require(cr_epoch == epoch, "cr: caller epoch mismatch"); // request and chain epochs must match

uint256 c_local_epoch = _get_local_epoch();
require(cr_epoch >= c_local_epoch, "cr: local view ahead of chain"); // local view of the chain must be at least until local epoch. this means no user requested this kettle with a higher epoch

if (cr_epoch > c_local_epoch) {
// Always safe - bump local view to (local) chain view. Refuse to serve rollbacks
_set_local_epoch(cr_epoch);
}
}
_;
}
}
Loading