-
Notifications
You must be signed in to change notification settings - Fork 6
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
base: qol-lints
Are you sure you want to change the base?
Changes from 1 commit
86514ea
a405158
5574cd5
b6e8aec
7cc048e
ee263e6
b1707b0
77bac53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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))); | ||
} | ||
} |
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)); | ||||||||||||||||
_; | ||||||||||||||||
} | ||||||||||||||||
// 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)); | ||||||||||||||||
} | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you use the auth modifier from above here too?
Suggested change
|
||||||||||||||||
} |
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); | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 */ | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this check seems off a bit. |
||||||||||||||||
challenge = _get_local_challenge(); /* do not override previous local challenge to prevent dos */ | ||||||||||||||||
if (challenge == bytes32(0)) { | ||||||||||||||||
challenge = Suave().localRandom(); | ||||||||||||||||
_set_local_challenge(challenge); | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Suggested change
|
||||||||||||||||
} | ||||||||||||||||
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 | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||||||
_; | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am confused and not sure what to get out of this. |
||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// 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 | ||||||||||||||||
_; | ||||||||||||||||
} | ||||||||||||||||
} |
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'"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small nit
Suggested change
|
||||||
|
||||||
// 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; | ||||||
} |
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; | ||
} | ||
} |
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 */ | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Suggested change
|
||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
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.