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 1 commit
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
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);
}
7 changes: 7 additions & 0 deletions src/crypto/encryption.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ library PKE {
return abi.encodePacked(p.X, p.Y);
}

function verify(bytes memory pubkey, bytes32 digest, bytes memory sig) internal pure returns (bool) {
(uint256 qx, uint256 qy) = abi.decode(pubkey, (uint256, uint256));
bytes memory ser = bytes.concat(bytes32(qx), bytes32(qy));
address signer = address(uint160(uint256(keccak256(ser))));
return Secp256k1.verify(signer, digest, sig);
}

function encrypt(bytes memory pubkey, bytes32 r, bytes memory message) internal view returns (bytes memory) {
(uint256 gx, uint256 gy) = abi.decode(pubkey, (uint256, uint256));
Curve.G1Point memory pub = Curve.G1Point(gx, gy);
Expand Down
23 changes: 23 additions & 0 deletions src/gadgets/AttestationGadget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
pragma solidity ^0.8.19;

abstract contract AttestationGadget {
function offchain_attest(bytes32 user_data) internal virtual returns (bytes memory attestation); // Generate MEVM-side attestation of user data for computation integrity guarantee
function onchain_verify_attestation(bytes32 user_data, bytes memory attestation) internal virtual returns (bool); // Verify the attestation produced by offchain_attest, can be done onchain

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) public virtual {
require(onchain_verify_attestation(keccak256(abi.encodePacked(target, address(this), user_data)), attestation));
}

function offchain_attest(bytes4 target, bytes memory user_data)
internal
virtual
returns (bytes memory attestation)
{
return offchain_attest(keccak256(abi.encodePacked(target, address(this), user_data)));
}
}
34 changes: 34 additions & 0 deletions src/gadgets/AuthGadget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
pragma solidity ^0.8.19;

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

abstract contract AuthGadget {
/* https://docs.openzeppelin.com/contracts/4.x/api/access#AccessControl */
function hasRole(bytes32 role, address account) internal virtual returns (bool);
// Grants role based on quote provided (in particular - user data and enclave measurement
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);

// auth version relying only on the transaction signature
modifier auth(bytes32 role) {
require(hasRole(role, msg.sender));
_;
}
// query should contain current_version() and cr_epoch!

modifier auth_query(bytes32 role, bytes memory query, bytes memory signature) {
require(Secp256k1.verify(msg.sender, keccak256(abi.encodePacked(msg.sender, query)), signature));
require(hasRole(role, msg.sender));
_;
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure what the purpose of this modifier but it can use the auth modifier internally since it is doing the same check there too.

Suggested change
modifier auth_query(bytes32 role, bytes memory query, bytes memory signature) {
require(Secp256k1.verify(msg.sender, keccak256(abi.encodePacked(msg.sender, query)), signature));
require(hasRole(role, msg.sender));
_;
modifier auth_query(bytes32 role, bytes memory query, bytes memory signature) {
auth(role);
require(Secp256k1.verify(msg.sender, keccak256(abi.encodePacked(msg.sender, query)), signature));
_;

}
// Local node helper

function auth_query_hash(bytes32 role, bytes memory query) public returns (bytes32) {
require(hasRole(role, msg.sender));
return keccak256(abi.encodePacked(msg.sender, query));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

can you use the auth modifier from above here too?

Suggested change
function auth_query_hash(bytes32 role, bytes memory query) public returns (bytes32) {
require(hasRole(role, msg.sender));
return keccak256(abi.encodePacked(msg.sender, query));
}
function auth_query_hash(bytes32 role, bytes memory query) public auth(role) returns (bytes32) {
return keccak256(abi.encodePacked(msg.sender, query));
}

}
84 changes: 84 additions & 0 deletions src/gadgets/CensorshipResistanceGadget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
pragma solidity ^0.8.19;

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

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

abstract contract CensorshipResistanceGadget is AttestationGadget {
function Suave() internal virtual returns (IAndromeda);
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the purpose of this function?

To force the contract using the CRgadget to have IAndromeda instance as part of it?


// require explicit onchain challenge on kettle restart
mapping(bytes32 => bool) private cr_challenges;

function offchain_restart_challenge() public returns (bytes32 challenge, bytes memory attestation) {
require(!cr_challenges[challenge]); /* already challenged */
Copy link
Contributor

Choose a reason for hiding this comment

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

this check seems off a bit.
It is checking if the returned challenge was already done but it has not been set yet. So this require will always pass because challenge at this point is still empty and not set. right? or Did I oversee something?

challenge = _get_local_challenge(); /* do not override previous local challenge to prevent dos */
if (challenge == bytes32(0)) {
challenge = Suave().localRandom();
_set_local_challenge(challenge);
Copy link
Contributor

Choose a reason for hiding this comment

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

I know the probability is very low but here the randomly generated challenge might collide with a previously generated one from before. So probably there should be a check here before setting the new local challenge.
Or do you need to generate a one shot challenge?

Suggested change
challenge = Suave().localRandom();
_set_local_challenge(challenge);
challenge = Suave().localRandom();
while(cr_challenges[challenge]) {
challenge = Suave().localRandom();
}
_set_local_challenge(challenge);

}
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 = Suave().volatileGet("local_challenge");
}

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

// epoch as seen onchain (can be delayed if chain is censored)
uint256 public 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(Suave().volatileGet("local_epoch"));
}

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

function bump_cr_epoch(uint256 epoch) internal {
require(cr_epoch == 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 */
require(cr_challenges[_get_local_challenge()]);
require(epoch >= cr_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); // local view of the chain must be at least until local epoch - otherwise the chain is being censored
require(cr_epoch == epoch); // and request and chain epochs must match - otherwise we are getting spoofed
_;
Copy link
Contributor

Choose a reason for hiding this comment

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

I am confused and not sure what to get out of this.
Could you please elaborate more on what this is potentially doing?
I can't see how this is censorship resistant and can you give me an example of a censorship case here where this would be prevented by this modifier?

}

// 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) {
require(cr_challenges[_get_local_challenge()]);
require(cr_epoch == epoch); // request and chain epochs must match
require(cr_epoch >= _get_local_epoch()); // local view of the chain must be at least until local epoch. this means no user requested this kettle with a higher epoch
_;
}
}
124 changes: 124 additions & 0 deletions src/gadgets/EncryptedInputsGadget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
pragma solidity ^0.8.19;

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

import {PKE} from "src/crypto/encryption.sol";
import {BIP32} from "src/BIP32.sol";

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

// CensorshipResistanceGadget is used to enforce CR on pubkey rotation
// AttestationGadget is used to update pubkey onchain
abstract contract EncryptedInputsGadget is VersionGadget, AttestationGadget, CensorshipResistanceGadget {
function km() internal pure virtual returns (KeyManager);

bytes private constant input_enc_derivation_path_prefix = "m/1'";
Copy link
Contributor

Choose a reason for hiding this comment

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

small nit

Suggested change
bytes private constant input_enc_derivation_path_prefix = "m/1'";
bytes private constant INPUT_ENC_DERIVATION_PATH_PREFIX = "m/1'";


// Encrypt secret inputs with this key! Rotation triggers new cr epoch.
bytes public contract_pubkey = bytes("");
uint256 private pubkey_nonce = 0;

/* Should only be called on local, trusted node! */
function encrypt_contract_inputs(bytes memory plaintext, bytes32 r)
public
view
returns (uint256, /* cr_epoch */ uint256, /* pubkey_nonce */ bytes memory /* ciphertext */ )
{
return (cr_epoch, pubkey_nonce, PKE.encrypt(contract_pubkey, r, plaintext));
}
/* Called internally */

function decrypt_contract_inputs(uint256 epoch, uint256 nonce, bytes memory ciphertext)
internal
check_cr(epoch)
returns (bytes memory)
{
return km().decrypt(_input_enc_derive_path(nonce), ciphertext);
}

/* Only on local node! */
function encrypt_call(bytes4 selector, bytes memory encoded_inputs, bytes32 r)
public
view
returns (uint256, /* cr_epoch */ uint256, /* pubkey_nonce */ bytes memory /* calldata */ )
{
return (cr_epoch, pubkey_nonce, PKE.encrypt(contract_pubkey, r, abi.encodePacked(selector, encoded_inputs)));
}
/* Manage encryption at the call level */

function ecrypted_dispatch(
uint256 epoch,
uint256 nonce,
bytes memory encrypted_calldata,
bytes memory return_pubkey,
bytes memory signature
) public check_cr(epoch) returns (bytes memory) {
require(pubkey_nonce == nonce);
require(
PKE.verify(
return_pubkey, keccak256(abi.encodePacked(epoch, nonce, encrypted_calldata, return_pubkey)), signature
)
);
bytes memory raw_calldata = km().decrypt(_input_enc_derive_path(nonce), encrypted_calldata);
(bool success, bytes memory data) = address(this).delegatecall(raw_calldata);
bytes memory return_data = km().encrypt_to_pubkey(return_pubkey, data);
require(success);
return return_data;
}

function rotate_contract_pubkey(uint256 epoch, uint256 nonce)
public
check_cr(epoch)
returns (bytes memory pubkey, bytes memory attestation)
{
pubkey = km().derive_pubkey(_input_enc_derive_path(nonce));
attestation = attest_rotate(_onchain_rotate_pubkey_data(cr_epoch, nonce, pubkey));
}

function onchain_rotate_pubkey(bytes memory pubkey, bytes memory attestation)
public
verify_rotate(_onchain_rotate_pubkey_data(cr_epoch, pubkey_nonce, pubkey), attestation)
{
contract_pubkey = pubkey;
pubkey_nonce = pubkey_nonce;
bump_cr_epoch(cr_epoch);
}

// Typed attestation helper (can be autogenerated)
struct _onchain_rotate_pubkey_data {
uint256 cr_epoch;
uint256 pubkey_nonce;
bytes pubkey;
}

function attest_rotate(_onchain_rotate_pubkey_data memory user_data) private returns (bytes memory) {
return offchain_attest(this.onchain_rotate_pubkey.selector, abi.encode(user_data));
}

modifier verify_rotate(_onchain_rotate_pubkey_data memory user_data, bytes memory attestation) {
onchain_verify_fn(this.onchain_rotate_pubkey.selector, abi.encode(user_data), attestation);
_;
}

function _input_enc_derive_path(uint256 nonce) private returns (bytes memory) {
return versioned_derive_path(bytes.concat(input_enc_derivation_path_prefix, abi.encodePacked(nonce), "'"));
}
}

/* Helper */
interface KeyManager {
// function _seed() private returns (bytes memory); // See KeyManagerBase

function derive_pubkey(bytes memory path) external returns (bytes memory privkey); // { return abi.encodePacked(BIP32.deriveChildKeyPairFromPath(_seed(), path)[1].key); }
function derive_privkey(bytes memory path) external returns (bytes memory privkey); // { return abi.encodePacked(BIP32.deriveChildKeyPairFromPath(_seed(), path)[0].key); }

function encrypt(bytes memory path, bytes memory plaintext) external returns (bytes memory ciphertext); // { return PKE.encrypt(this.derive_pubkey(path), Suave().localRandom(), plaintext); }
function encrypt_to_pubkey(bytes memory pubkey, bytes memory plaintext)
external
returns (bytes memory ciphertext); // { return PKE.encrypt(pubkey, Suave().localRandom(), plaintext); }
function decrypt(bytes memory path, bytes memory ciphertext) external returns (bytes memory plaintext); // { return PKE.decrypt(this.derive_privkey(path), ciphertext); }

function refresh() external;
}
70 changes: 70 additions & 0 deletions src/gadgets/KillswitchGadget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
pragma solidity ^0.8.19;

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

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

abstract contract KillswitchGadget is AuthGadget, AttestationGadget, CensorshipResistanceGadget {
// Killswitch - disaster damage control
// Like versioning, but requires a migration instead of an upgrade
bool killswitch_active;
bytes killswitch_reason;

// Enclave killswitch
bytes32 public constant ATTESTED_KILLSWITCH_ROLE = keccak256("attested_killswitch");
// Governance killswitch
bytes32 public constant KILLSWITCH_ROLE = keccak256("killswitch");

// local kettle view before onchain trigger
// killswitch should require reaching to other kettles after a restart
// to prevent restarting the kettle after the offchain trigger is propagated but before it hits the chain
function _get_local_killswitch() private returns (bool) {
return Suave().volatileGet("local_killswitch") == bytes32("1");
}

function _set_local_killswitch() private {
Suave().volatileSet("local_killswitch", bytes32("1"));
}

modifier ks( /* should we put check_cr here by default? */ ) {
require(!killswitch_active);
require(!_get_local_killswitch()); /* TODO: review onboarding after restart */
_;
}

modifier ks_active() {
require(killswitch_active);
/* does not require local ks */
_;
}

// Attested offchain killswitch (enclaves)
function killswitch(uint256 epoch, bytes memory reason, bytes memory signature)
public
force_cr(epoch)
auth_query(ATTESTED_KILLSWITCH_ROLE, reason, signature)
returns (bytes memory attestation)
{
_set_local_killswitch(); /* does not prevent the kettle operator from restarting before ks is put on chain! mischief can still be about */
return offchain_attest(this.onchain_attested_killswitch.selector, reason);
}

// Attested onchain counterpart (enclaves)
function onchain_attested_killswitch(bytes memory reason, bytes memory attestation)
public
onchain_verify((reason), attestation)
{
require(!killswitch_active);
killswitch_active = true;
killswitch_reason = reason;
}

// Non-attested onchain version (governance / admin)
function onchain_killswitch(bytes memory reason) public auth(KILLSWITCH_ROLE) {
require(!killswitch_active);
killswitch_active = true;
killswitch_reason = reason;
}
}
19 changes: 19 additions & 0 deletions src/gadgets/VersionGadget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
pragma solidity ^0.8.19;

abstract contract VersionGadget {
// Return a hash of all the upgradable code
function current_version() public virtual returns (bytes32);
/* Example implemetation:
function current_version() public override returns (bytes32) {
return keccak256(abi.encodePacked(address(this), excodehash(this), address(auth), extcodehash(auth), address(km), extcodehash(km));
}
*/

// Adds current version to derive path - automatically prevents access across upgrades
function versioned_derive_path(bytes memory path) internal returns (bytes memory) {
return bytes.concat(path, bytes("/"), abi.encodePacked(current_version()), bytes("'"));
}

function all_versions() public returns (bytes32[] memory) { /* TODO */ }
/* TODO: allow using past versions, maybe */
Copy link
Contributor

Choose a reason for hiding this comment

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

Since sets data structure are not there by default, we can achieve this by using mapping and arrays.
Here is a minimal suggestion

Suggested change
function all_versions() public returns (bytes32[] memory) { /* TODO */ }
/* TODO: allow using past versions, maybe */
bytes32[] private versions;
mapping(bytes32 => bool) private versionExists;
// Adds the current version to the version history if it doesn't already exist
function add_current_version() public {
bytes32 version = current_version();
if (!versionExists[version]) {
versions.push(version);
versionExists[version] = true;
}
}
// Returns an array of all recorded versions
function all_versions() public view returns (bytes32[] memory) {
return versions;
}

}
Loading