Created
April 5, 2023 11:54
-
-
Save HarryR/7bc718b794085e24abe653e15ff4fce9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: AGPL-3.0-or-later | |
pragma solidity ^0.8.9; | |
contract WW | |
{ | |
event EncryptedResponse(bytes32 nonce, bytes data); | |
event PublicKey(bytes32 x25519_public); | |
struct Coupon { | |
bytes32 nonce; | |
uint256 value; | |
bytes32 auth; | |
} | |
struct WithdrawOp { | |
address pay_to; | |
uint256 value; | |
} | |
struct EncryptedOperations { | |
Coupon[] input_coupons; | |
uint256[] output_values; | |
WithdrawOp[] withdraws; | |
} | |
// ------------------------------------------------------------------ | |
// Separate secret to secure the coupons | |
bytes32 immutable private m_coupon_secret; | |
// Separate keypair to hide client<->contract comms from relayer | |
bytes32 immutable private m_comms_secret_x25519; | |
bytes32 immutable private m_comms_public_x25519; | |
// Keep track of which coupons have been spent | |
mapping(bytes32 => bool) private m_invalidations; | |
mapping(address => uint256) private m_fees; | |
// ------------------------------------------------------------------ | |
constructor() | |
{ | |
m_coupon_secret = _random_bytes32(); | |
(m_comms_secret_x25519, m_comms_public_x25519) = _x25519_keypair(); | |
} | |
function emitPublicKey() | |
external | |
{ | |
emit PublicKey(m_comms_public_x25519); | |
} | |
function deposit(bytes32 ephemeral_pubkey) | |
external payable | |
{ | |
bytes32 comms_secret_x25519 = m_comms_secret_x25519; | |
bytes32 coupon_secret = m_coupon_secret; | |
(bytes32 ephemeral_derived, bytes32 ephemeral_IV) = _relay_anti_tamper_secret(ephemeral_pubkey, comms_secret_x25519); | |
Coupon memory coupon = _coupon_create(msg.value, coupon_secret); | |
_encrypt_and_emit_response(ephemeral_derived, ephemeral_IV, abi.encode(coupon)); | |
} | |
function join_split_pay( | |
bytes32 ephemeral_pubkey, | |
bytes calldata encrypted_operations, | |
uint256 fee | |
) | |
external | |
{ | |
bytes32 comms_secret_x25519 = m_comms_secret_x25519; | |
bytes32 coupon_secret = m_coupon_secret; | |
(bytes32 ephemeral_derived, bytes32 ephemeral_IV) = _relay_anti_tamper_secret(ephemeral_pubkey, comms_secret_x25519); | |
(EncryptedOperations memory ops) = abi.decode( | |
_decrypt(ephemeral_derived, // Key | |
ephemeral_IV, // IV | |
encrypted_operations, // Ciphertext | |
abi.encodePacked(fee)), // Extra authenticated data | |
(EncryptedOperations)); | |
uint256 total = 0; | |
// Open all input tokens | |
for( uint i = 0; i < ops.input_coupons.length; i++ ) | |
{ | |
total += _coupon_use_once(ops.input_coupons[i], coupon_secret); | |
} | |
// Create output tokens & encrypt them for client | |
{ | |
Coupon[] memory output_coupons = new Coupon[](ops.output_values.length); | |
for( uint i = 0; i < ops.output_values.length; i++ ) | |
{ | |
uint256 value = ops.output_values[i]; | |
total -= value; | |
output_coupons[i] = _coupon_create(value, coupon_secret); | |
} | |
ephemeral_IV = keccak256(abi.encodePacked(ephemeral_IV)); | |
_encrypt_and_emit_response(ephemeral_derived, ephemeral_IV, abi.encode(output_coupons)); | |
} | |
// Provide fee to relayer | |
{ | |
total -= fee; | |
m_fees[msg.sender] += fee; | |
} | |
// Perform withdraws | |
{ | |
for( uint i = 0; i < ops.withdraws.length; i++ ) | |
{ | |
WithdrawOp memory w = ops.withdraws[i]; | |
require( 0 == _codesize(w.pay_to) ); | |
uint256 value = w.value; | |
total -= value; | |
payable(w.pay_to).transfer(value); | |
} | |
} | |
require( 0 == total ); | |
} | |
function withdrawFees() | |
external | |
{ | |
uint256 value = m_fees[msg.sender]; | |
require( 0 != value ); | |
m_fees[msg.sender] = 0; | |
payable(msg.sender).transfer(value); | |
} | |
function withdraw(Coupon[] memory coupons, address pay_to) | |
external | |
{ | |
bytes32 coupon_secret = m_coupon_secret; | |
uint256 total = 0; | |
for( uint i = 0; i < coupons.length; i++ ) | |
{ | |
total += _coupon_use_once(coupons[i], coupon_secret); | |
} | |
payable(pay_to).transfer(total); | |
} | |
// ------------------------------------------------------------------ | |
address private constant RANDOM_BYTES = 0x0100000000000000000000000000000000000001; | |
address private constant DERIVE_KEY = 0x0100000000000000000000000000000000000002; | |
address private constant ENCRYPT = 0x0100000000000000000000000000000000000003; | |
address private constant DECRYPT = 0x0100000000000000000000000000000000000004; | |
address private constant CURVE25519_PUBLIC_KEY = 0x0100000000000000000000000000000000000008; | |
function _random_bytes32() | |
private view | |
returns (bytes32) | |
{ | |
bytes memory p13n = abi.encodePacked(block.chainid, block.number, block.timestamp, msg.sender, address(this)); | |
(bool success, bytes memory entropy) = RANDOM_BYTES.staticcall(abi.encode(uint256(32), p13n)); | |
require(success); | |
return bytes32(entropy); | |
} | |
function _encrypt(bytes32 key, bytes32 nonce, bytes memory plaintext, bytes memory additionalData) | |
private view | |
returns (bytes memory) | |
{ | |
(bool success, bytes memory ciphertext) = ENCRYPT.staticcall( | |
abi.encode(key, nonce, plaintext, additionalData) | |
); | |
require(success); | |
return ciphertext; | |
} | |
function _decrypt(bytes32 key, bytes32 nonce, bytes memory ciphertext, bytes memory additionalData) | |
private view | |
returns (bytes memory) | |
{ | |
(bool success, bytes memory plaintext) = DECRYPT.staticcall( | |
abi.encode(key, nonce, ciphertext, additionalData) | |
); | |
require(success); | |
return plaintext; | |
} | |
function _coupon_auth(uint256 value, bytes32 nonce, bytes32 secret) | |
private view | |
returns (bytes32) | |
{ | |
return keccak256(abi.encodePacked(value, nonce, secret, address(this))); | |
} | |
function _coupon_create(uint256 value, bytes32 secret) | |
private view | |
returns (Coupon memory r) | |
{ | |
r.value = value; | |
r.nonce = _random_bytes32(); | |
r.auth = _coupon_auth(value, r.nonce, secret); | |
} | |
function _coupon_use_once(Coupon memory t, bytes32 secret) | |
private | |
returns (uint256) | |
{ | |
require( _coupon_auth(t.value, t.nonce, secret) == t.auth ); | |
require( m_invalidations[t.nonce] == false ); | |
m_invalidations[t.nonce] = true; | |
return t.value; | |
} | |
function _relay_anti_tamper_secret(bytes32 ephemeral_x25519_public, bytes32 secret) | |
private view | |
returns (bytes32 ephemeral_derived, bytes32 ephemeral_IV) | |
{ | |
(bool success, bytes memory derived) = DERIVE_KEY.staticcall( | |
abi.encode(ephemeral_x25519_public, secret) | |
); | |
require( success ); | |
ephemeral_derived = bytes32(derived); | |
ephemeral_IV = keccak256(abi.encodePacked(ephemeral_x25519_public)); | |
} | |
function _encrypt_and_emit_response(bytes32 key, bytes32 IV, bytes memory plaintext) | |
private | |
{ | |
bytes memory response = _encrypt(key, IV, plaintext, new bytes(0)); | |
emit EncryptedResponse(IV, response); | |
} | |
function _x25519_keypair() | |
private view | |
returns (bytes32 x25519_secret, bytes32 x25519_public) | |
{ | |
x25519_secret = _random_bytes32(); | |
require( 0 != uint256(x25519_secret) ); | |
(bool success, bytes memory public_bytes) = CURVE25519_PUBLIC_KEY.staticcall(abi.encodePacked(x25519_secret)); | |
require( success ); | |
x25519_public = bytes32(public_bytes); | |
} | |
function _codesize(address _addr) | |
private view | |
returns (uint size) | |
{ | |
assembly { size := extcodesize(_addr) } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment