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

echecks implementation #53

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
166 changes: 166 additions & 0 deletions src/examples/KettleCash.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
pragma solidity ^0.8.13;

import "../crypto/secp256k1.sol";
import "../crypto/encryption.sol";

import "../KeyManager.sol";
import "../IAndromeda.sol";

struct Check {
// address sender; // User account of sender
// address issuer; // Kettle of sender
uint amount;
address recipient; // User account of recipient
address kettle; // Recipient's Kettle
bytes32 nonce; // Unique tag
bytes att; // Signed by issuer
}

contract KettleCash {
KeyManager_v0 keymgr;
IAndromeda Suave;

///////////////////////////////////////
// Onchain functions
///////////////////////////////////////

constructor(KeyManager_v0 _keymgr) {
keymgr = _keymgr;
Suave = _keymgr.Suave();
}

// On-Chain deposit to a Kettle
// This produces an off-chain check with an empty attestation.
// This can be deposited at the specified kettle.
event Deposit(address depositor, uint amt, address kettle);
mapping (bytes32 serial => bool) deposits;
uint deposit_counter;
function onchain_Deposit(address kettle) public payable
returns(Check memory c) {
c.amount = msg.value;
c.recipient = msg.sender;
c.kettle = kettle;
c.nonce = bytes32(deposit_counter);
deposits[CheckSerial(c)] = true;
deposit_counter += 1;
emit Deposit(msg.sender, msg.value, kettle);
}

// Cash an on-chain check
mapping (bytes32 serial => bool) public cashed;
event Withdrawal(bytes32 serial, Check check);
function onchain_Withdraw(Check memory check) public {
bytes32 serial = CheckSerial(check);

// A withdrawal check has check.kettle == 0
require(check.kettle == address(0));

// Only cash a check once
require(!cashed[serial]);

// Must verify under this contract
require(keymgr.verify(address(this), serial, check.att));

// Carry out the transfer
cashed[serial] = true;
payable(check.recipient).transfer(check.amount);
}

///////////////////////////////////////
// CoProcessor functions for a Kettle
///////////////////////////////////////

function offchain_ThisKettle() public returns (address) {
// Must be registered with key manager
require(keymgr.derivedPriv() != bytes32(0));

// Store our volatile address
address addr = address(bytes20(Suave.volatileGet(bytes32("_this_kettle"))));
if (addr == address(0)) {
addr = address(bytes20(Suave.localRandom()));
Suave.volatileSet(bytes32("_this_kettle"), bytes32(abi.encodePacked(addr)));
}

return addr;
}

function CheckSerial(Check memory check) public pure returns(bytes32){
// Serialize everything except the signature
Check memory c;
//c.sender = check.sender;
c.amount = check.amount;
c.recipient = check.recipient;
c.kettle = check.kettle;
c.nonce = check.nonce;
c.att = bytes("");
return keccak256(abi.encode(c));
}

function _IsSpent(Check memory check) public returns(bool) {
// Only meaningful if it belongs to this kettle ours
require(offchain_ThisKettle() == check.kettle);
bytes32 serial = CheckSerial(check);
bytes32 key = keccak256(abi.encodePacked("cashed",serial));
return Suave.volatileGet(key) != bytes32(0);
}

// Query the volatile accountBalance
function offchain_QueryBalance(address depositor) public returns(uint) {
// Read the balance from volatile memory
bytes32 key = keccak256(abi.encodePacked("balance",depositor));
return uint(Suave.volatileGet(key));
}

function _WriteBalance(address depositor, uint balance) internal {
// Write the balance to volatile memory
bytes32 key = keccak256(abi.encodePacked("balance",depositor));
Suave.volatileSet(key, bytes32(balance));
}

// Deposit a check
function offchain_DepositCheck(Check memory check) public {
// Only cash checks pointing to this kettle
require(offchain_ThisKettle() == check.kettle);

// If a deposit, needs to be finalized on-chain
if (check.att.length == 0) {
require(deposits[CheckSerial(check)]);
} else {
// Otherwise needs need to have a valid attestation
require(keymgr.verify(address(this), CheckSerial(check),
check.att));
}

// Only valid if signed by a kettle
bytes32 serial = CheckSerial(check);
bytes32 key = keccak256(abi.encodePacked("cashed",serial));
require(!_IsSpent(check));

// Mark the check as spent, then update balance
uint balance = offchain_QueryBalance(check.recipient);
Suave.volatileSet(key, bytes32("voidvoidvoidvoidvoidvoidvoidvoid"));
_WriteBalance(check.recipient, balance + check.amount);
}

// Issue a check
function offchain_IssueCheck(address recipient, address kettle, uint amount) public returns (Check memory) {
// Verify the balance is OK
uint balance = offchain_QueryBalance(msg.sender);
require(balance >= amount);

// Define the check
Check memory c;
c.amount = amount;
c.recipient = recipient;
c.kettle = kettle;
c.nonce = Suave.localRandom();

// Sign the check
c.att = keymgr.attest(CheckSerial(c));
require(keymgr.verify(address(this), CheckSerial(c), c.att));

// Update the balance and return the Check
_WriteBalance(msg.sender, balance - amount);
return c;
}
}
128 changes: 128 additions & 0 deletions test/examples/KettleCash.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {AndromedaRemote,SigVerifyLib} from "src/AndromedaRemote.sol";
import {Test, console2} from "forge-std/Test.sol";

import {KeyManager_v0} from "src/KeyManager.sol";
//import {KeyManager_v0_Test} from "test/KeyManager.t.sol";
import {KettleCash, Check} from "src/examples/KettleCash.sol";

contract KettleCashTest is Test {
AndromedaRemote andromeda;
KeyManager_v0 keymgr;
KettleCash cash;

address alice;
address bob;

function setUp_Bootstrap() public {
andromeda.switchHost("bootstrap");
(address xPub, bytes memory att) = keymgr.offchain_Bootstrap();
keymgr.onchain_Bootstrap(xPub, att);

(bytes memory dPub, bytes memory sig) = keymgr.offchain_DeriveKey(address(cash));
keymgr.onchain_DeriveKey(address(cash), dPub, sig);
}
function setUp_Onboard(string memory newHost) public {
// 2. Register a new node
andromeda.switchHost(newHost);
(address kettle, bytes memory bPub, bytes memory attB) = keymgr
.offchain_Register();
keymgr.onchain_Register(kettle, bPub, attB);

// 3. Help onboard a new node
// 3a. Offchain generate a ciphertext with the key
andromeda.switchHost("bootstrap");
bytes memory ciphertext = keymgr.offchain_Onboard(kettle);
// 3b. Onchain post the ciphertext
keymgr.onchain_Onboard(kettle, ciphertext);
// 3c. Load the data received
andromeda.switchHost(newHost);
keymgr.finish_Onboard(ciphertext);

// 3.1. Help onboard a second node
andromeda.switchHost("bootstrap");
ciphertext = keymgr.offchain_Onboard(kettle);
// 3.1b. Onchain post the ciphertext
keymgr.onchain_Onboard(kettle, ciphertext);
// 3.1c. Load the data received
andromeda.switchHost(newHost);
keymgr.finish_Onboard(ciphertext);
}

function setUp() public {
SigVerifyLib lib = new SigVerifyLib();
andromeda = new AndromedaRemote(address(lib));
andromeda.initialize();

vm.prank(vm.addr(uint256(keccak256("examples/EChecks.t.sol"))));
keymgr = new KeyManager_v0(address(andromeda));

cash = new KettleCash(keymgr);

alice = vm.addr(uint256(keccak256("alice")));
bob = vm.addr(uint256(keccak256("bob")));

setUp_Bootstrap();
setUp_Onboard("alice");
setUp_Onboard("bob");
}

function test_echecks() public {
andromeda.switchHost("alice");
address alice_kettle = cash.offchain_ThisKettle();
console2.logAddress(alice_kettle);
andromeda.switchHost("bob");
address bob_kettle = cash.offchain_ThisKettle();
console2.logAddress(bob_kettle);

// On chain deposit
andromeda.switchHost("alice");
vm.prank(alice);
vm.deal(alice, 1 ether);
Check memory c = cash.onchain_Deposit{value: 1 ether}(alice_kettle);
console2.logUint(c.amount);
assert(!cash.cashed(cash.CheckSerial(c)));
assert(!cash._IsSpent(c));

// Alice deposits her check into her own Kettle
cash.offchain_DepositCheck(c);
assert(cash._IsSpent(c));
assert(cash.offchain_QueryBalance(alice) == 1 ether);

// Can't deposit twice
vm.expectRevert();
cash.offchain_DepositCheck(c);

// Alice issues a check to pay Bob at Bob's kettle
vm.prank(alice);
Check memory c2 = cash.offchain_IssueCheck(bob, bob_kettle, 0.3 ether);
assert(cash.offchain_QueryBalance(alice) == 0.7 ether);

// Can't cash at the wrong kettle
andromeda.switchHost("alice");
vm.expectRevert();
cash.offchain_DepositCheck(c2);

// Let's try to deposit the check at Bob's kettle
andromeda.switchHost("bob");
assert(!cash._IsSpent(c2));
cash.offchain_DepositCheck(c2);
assert(cash.offchain_QueryBalance(bob) == 0.3 ether);
assert(cash._IsSpent(c2));

// To withdraw, Issue a check to kettle address(0)
vm.prank(bob);
Check memory c3 = cash.offchain_IssueCheck(bob, address(0), 0.2 ether);

// We can cash the withdrawal check on-chain
assert(!cash.cashed(cash.CheckSerial(c3)));
cash.onchain_Withdraw(c3);
assert(bob.balance == 0.2 ether);

// Can't cash twice
vm.expectRevert();
cash.onchain_Withdraw(c3);
}
}