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

The spec. #2

Closed
wants to merge 7 commits into from
Closed
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
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Ethereum Machine Oracle

This project is a set of smart contracts for Ethereum, capable of verifying large computations off chain.

It aims to be generic, capable of verifying computations done on any abstract machine implementing a specified [interface](./src/Machine.template.sol), using the [truebit](https://people.cs.uchicago.edu/~teutsch/papers/truebit.pdf) style verification game.

This is a spiritual successor to [solEVM enforcer](https://github.com/leapdao/solEVM-enforcer).

## Developer guide

### Requirements

You have to have the [solidity command line compiler](https://solidity.readthedocs.io/en/v0.6.2/installing-solidity.html#binary-packages) (version >= 0.6.0) installed on your machine.

### Compilation

To typecheck run:

```./typecheck.sh```

## The internal model and terminology

In this section we will explain the terminology used in the code and further documentation.

### Machine

The word _machine_ is used to refer to any deterministic state machine that implements a specified [interface](./src/Machine.template.sol).

Each _machine_ is defined by 3 types: _State_, _Seed_ and _Image_, and 6 functions: _create_, _project_, _isTerminal_, _next_, _stateHash_ and _imageHash_.

_State_ describes the state of the machine at a particular moment of execution.

The function _next_ performs the smallest possible step of computation, mapping the input _State_ into an output _State_, **or failing** (all of the situations in which it fails will be described later).

The function _isTerminal_ determines if a given _State_ is terminal, meaning that if _isTerminal(s) == true_, where _s_ is of type _State_, then _next(s)_ must either fail or return _s_ unchanged.
Copy link
Member

Choose a reason for hiding this comment

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

next(s) must either fail or return s unchanged.

is an "unchanged s" really possible? if next is supposed to "perform the smallest possible step of computation", then state must have changed, hence same state can not be returned.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I guess it depends on whether you consider doing nothing, or no-op to be "a step of computation".

But the point I was trying to get across is that you define a terminal state by either saying "a terminal state is one from which no further computation can be done", or "a terminal state is a fixpoint with respect to the state-transition function".

And actually, I think I will have to commit to one of these in future, as my implementation efforts continue.


The function _stateHash_ maps a _State_ to _bytes32_, giving a "fingerprint" to every _State_. The following has to hold for any valid _machine_ implementation:

Let _s1_ and _s2_ be of type _State_. If _stateHash(s1) == stateHash(s2)_, then _stateHash(next(s1)) == stateHash(next(s2))_ (assuming here neither _next_ fails).

Every value of type _Seed_ uniquely determines a _State_ through the function _create_. _Seed_ can be thought of as the initial parameters to a computation. For example, if the _machine_ in question is something like the [EVM](https://ethereum.github.io/yellowpaper/paper.pdf), it's _Seed_ would be a combination of a function, the parameters to it, and the state of the blockchain (i.e. the execution environment). In the EVM example, the _State_ created with _create_ from such a _Seed_, would include all of the _Seed_ data, but also an empty memory, the program counter set to 0 etc.

The type _Image_ represents the part of _State_ we care about when the computation is finished. Usually when running a compuation, one is not interested in the entire state of the machine running it, but only in some part of it. The function _project_ extracts this part (the _Image_) from a given _State_. We can also take a fingerprint of an _Image_ using the function _imageHash_. The following has to hold for any valid _machine_ implementation:

Let _s1_ and _s2_ be of type _State_. If _stateHash(s1) == stateHash(s2)_, then _imageHash(project(s1)) == imageHash(project(s2))_.

#### Example implementation

Let's take a look at an example _machine_ [implementation](./src/ExampleMachine.sol). This machine is initialized with an array of numbers (the Seed). It's job is to add these numbers together. It does so by putting the initial numbers onto a stack, and keeping a running sum (together these form the State). At each step the top element of the stack is added to the running sum. The part of the State interesting to us is the sum, so the function project just gives us the sum (the Image) in any given State.

### Oracle

Coming soon.

### Court

Coming soon.

## Incentives

Coming soon.
313 changes: 313 additions & 0 deletions src/Court.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
pragma solidity >=0.6.0;
pragma experimental ABIEncoderV2;

import "./Oracle.sol";
import "./Merkle.sol";

interface ICourt {
using Merkle for Merkle.TreeNode;
using Merkle for Merkle.Proof;

enum DisputeState {
DoesNotExist,
Opened,
ProsecutorTurn,
DefendantTurn,
Bottom
}

// disputeKey is prosecutorRoot
struct Dispute {
bytes32 answerKey;
bytes32 defendantRoot;
address prosecutor;
uint lastActionTimestamp;
uint numberOfSteps;
uint disagreementPoint;
bytes32 firstDivergentStateHash;
uint depth;
bool goRight;
Merkle.TreeNode defendantNode;
Merkle.TreeNode prosecutorNode;
DisputeState state;
}

event NewDispute (
bytes32 answerKey,
bytes32 prosecutorRoot
);

event Reveal (
bytes32 disputeKey,
bytes32 defendantRoot,
Machine.State finalState
);

function oracle()
external view returns (IOracle);

function getDispute(
bytes32 disputeKey
) external view returns (Dispute memory);

function newDispute (
bytes32 answerKey,
Merkle.TreeNode calldata prosecutorNode
) external payable;

function reveal (
bytes32 disputeKey,
Merkle.TreeNode calldata node,
Merkle.Proof calldata proofLeft,
Merkle.Proof calldata proofRight,
Machine.State calldata finalState
) external;

function prosecutorRespond (
bytes32 disputeKey,
Merkle.TreeNode calldata node
) external;

function defendantRespond (
bytes32 disputeKey,
Merkle.TreeNode calldata node
) external;

function defendantRevealBottom (
bytes32 disputeKey,
Merkle.Proof calldata proof,
Machine.State calldata state
) external;

function timeout (
bytes32 disputeKey
) external;
}

abstract contract ACourt is ICourt {

IOracle public override oracle;
mapping (bytes32 => Dispute) public disputes;
uint public STAKE_SIZE;
uint public MAX_TREE_DEPTH;

function getDispute (
bytes32 disputeKey
) external view override returns (Dispute memory)
{
return disputes[disputeKey];
}

function newDispute (
bytes32 answerKey,
Merkle.TreeNode calldata prosecutorNode
) override external payable
{
bytes32 prosecutorRoot = prosecutorNode.hash();
Dispute storage dispute = disputes[prosecutorRoot];

require(msg.value >= STAKE_SIZE, "Not enough stake sent.");
require(dispute.state == DisputeState.DoesNotExist, "Dispute already exists.");
require(_answerExists(answerKey), "Answer does not exists.");
require(_enoughTimeForDispute(answerKey), "There is not enough time left for a dispute.");

dispute.answerKey = answerKey;
dispute.state = DisputeState.Opened;
dispute.prosecutor = msg.sender;
dispute.lastActionTimestamp = now;
dispute.prosecutorNode = prosecutorNode;

emit NewDispute(answerKey, prosecutorRoot);
}

function reveal (
bytes32 disputeKey,
Merkle.TreeNode calldata node,
Merkle.Proof calldata proofLeft,
Merkle.Proof calldata proofRight,
Machine.State calldata finalState
) override external
{
Dispute storage dispute = disputes[disputeKey];
IOracle.Answer memory answer = oracle.getAnswer(dispute.answerKey);

bytes32 defendantRoot = node.hash();
(bytes32 leftLeaf, bytes32 leftRoot, uint leftIndex) = proofLeft.eval();
(bytes32 rightLeaf, bytes32 rightRoot, uint rightIndex) = proofRight.eval();

require(dispute.state == DisputeState.Opened, "Dispute state is not correct for this action.");
require(leftIndex == 0, "Left index must be 0.");
require(leftRoot == defendantRoot, "Left proof root does not match claimed root.");
require(rightRoot == defendantRoot, "Right proof root does not match claimed root.");
require(leftLeaf == answer.questionKey, "Left leaf does not match initial state hash.");
require(rightLeaf == Machine.stateHash(finalState), "Right leaf does not match the final state hash.");
require(Machine.imageHash(Machine.project(finalState)) == dispute.answerKey, "The revealed final state does not produce the image hash submitted in answer.");
require(Machine.isTerminal(finalState), "The revealed final state is not terminal");

dispute.defendantRoot = defendantRoot;
dispute.defendantNode = node;
dispute.lastActionTimestamp = now;
dispute.state = DisputeState.ProsecutorTurn;
dispute.numberOfSteps = rightIndex;
dispute.goRight = _goRight(dispute.prosecutorNode, dispute.defendantNode);
dispute.disagreementPoint = _updateDisagreementPoint(dispute.disagreementPoint, dispute.goRight);
dispute.depth = 1;

emit Reveal(disputeKey, defendantRoot, finalState);
}

function prosecutorRespond (
bytes32 disputeKey,
Merkle.TreeNode calldata node
) override external
{
Dispute storage dispute = disputes[disputeKey];

require(dispute.state == DisputeState.ProsecutorTurn, "Dispute state is not correct for this action.");
require(dispute.goRight ? node.hash() == dispute.prosecutorNode.right : node.hash() == dispute.prosecutorNode.left, "Brought node from the wrong side.");

dispute.prosecutorNode = node;
dispute.lastActionTimestamp = now;
dispute.state = DisputeState.DefendantTurn;

// emit something
}

function defendantRespond (
bytes32 disputeKey,
Merkle.TreeNode calldata node
) override external
{
Dispute storage dispute = disputes[disputeKey];

require(dispute.state == DisputeState.DefendantTurn, "Dispute state is not correct for this action.");
require(dispute.goRight ? node.hash() == dispute.defendantNode.right : node.hash() == dispute.prosecutorNode.left, "Brought node from the wrong side.");

dispute.defendantNode = node;
dispute.lastActionTimestamp = now;
dispute.goRight = _goRight(dispute.prosecutorNode, dispute.defendantNode);
dispute.disagreementPoint = _updateDisagreementPoint(dispute.disagreementPoint, dispute.goRight);
dispute.depth += 1;

if (_reachedBottom(dispute.depth)) {
if (dispute.disagreementPoint > dispute.numberOfSteps) {
_defendantWins(disputeKey);
// emit something
} else {
dispute.state = DisputeState.Bottom;
dispute.firstDivergentStateHash = dispute.goRight ? node.right : node.left;
//emit something
}
} else {
dispute.state = DisputeState.ProsecutorTurn;
// emit something
}
}

function defendantRevealBottom (
bytes32 disputeKey,
Merkle.Proof calldata proof,
Machine.State calldata state
) override external
{
Dispute storage dispute = disputes[disputeKey];

(bytes32 leaf, bytes32 root, uint index) = proof.eval();

require(dispute.state == DisputeState.Bottom, "Dispute state is not correct for this action.");
require(leaf == Machine.stateHash(state), "The submitted proof is not of the revealed state");
require(root == dispute.defendantRoot, "The submitted proof root does not match defendant root");
require(index == dispute.disagreementPoint - 1, "The revealed state is not the one before the disagreement point.");

(Machine.State memory nextState, bool canNext) = Machine.next(state);

require(canNext, "The machine was unable to compute next state for the revealed state.");
require(Machine.stateHash(nextState) == dispute.firstDivergentStateHash, "Next computed state is not the one commited to.");

_defendantWins(disputeKey);
// emit something
}

function timeout (
bytes32 disputeKey
) override external
{
require(disputes[disputeKey].state != DisputeState.DoesNotExist, "Can not timeout a non existent dispute.");
require(_canTimeout(disputeKey), "This dispute can not be timeout out at this moment");
if (_defendantWinsOnTimeout(disputeKey)) {
_defendantWins(disputeKey);
} else {
_prosecutorWins(disputeKey);
}
}

function _answerExists (
bytes32 answerKey
) internal view returns (bool)
{
IOracle.Answer memory answer = oracle.getAnswer(answerKey);
return answer.questionKey > 0;
}

function _enoughTimeForDispute (
bytes32 answerKey
) internal view returns (bool)
{
IOracle.Answer memory answer = oracle.getAnswer(answerKey);
IOracle.Question memory question = oracle.getQuestion(answer.questionKey);
return now < question.askTime + (2 * question.timeout / 3);
}

function _goRight (
Merkle.TreeNode memory prosecutorNode,
Merkle.TreeNode memory defendantNode
) internal pure returns (bool)
{
return prosecutorNode.left == defendantNode.left;
}

function _updateDisagreementPoint (
uint disagreementPoint,
bool goRight
) virtual internal pure returns (uint);

function _reachedBottom (
uint depth
) internal view returns (bool)
{
return depth == MAX_TREE_DEPTH;
}

function _defendantWins (
bytes32 disputeKey
) internal
{
address payable answerer = payable(oracle.getAnswer(disputes[disputeKey].answerKey).answerer);
delete disputes[disputeKey];
answerer.call.value(STAKE_SIZE)("");
}

function _prosecutorWins (
bytes32 disputeKey
) internal
{
Dispute storage dispute = disputes[disputeKey];
oracle.falsify(dispute.answerKey, dispute.prosecutor);
address payable prosecutor = payable(dispute.prosecutor);
delete disputes[disputeKey];
prosecutor.call.value(STAKE_SIZE)("");
}

function _canTimeout (
bytes32 disputeKey
) virtual internal view returns (bool);

function _defendantWinsOnTimeout (
bytes32 disputeKey
) internal view returns (bool)
{
DisputeState state = disputes[disputeKey].state;
return state == DisputeState.ProsecutorTurn;
}

}
Loading