truffle

DevLog 15 - State minimized implementation on current evm

Cross posted from https://ethresear.ch/t/state-minimized-implementation-on-current-evm/1255

In an effort to implement the functionality described in the post https://ethresear.ch/t/state-minimised-executions/748/26 , I've worked up the following contract for a 'virtual token' that allows for the holding of state in a  Double-batched Merkle log accumulator  as proposed by @JustinDrake.

This is a toy implementation to show what I mean by 'virtual functions'. The idea being that users would call the Transfer() function to emit an event that indicates that they want to transfer.  The Collators respond to that call bo collecting witnesses from the provided dataHash(perhaps via IPFS) and using the vTransfer() function to calculate the logs.

My original thought was to have the virtual function emit logs as it was run in an off chain evm, but with current tooling those logs are discarded or ignored so instead accumulate the new logs in the results bytes.  This makes the code messier, but it generally works.

pragma solidity ^0.4.18;

import "./BytesLib.sol";

contract VirtualToken {

address public owner;
uint256 public totalSupply;

event NewBranch(bytes32 indexed addressFrom, bytes32 amountFrom, bytes32 indexed addressTo, bytes32 amountTo, bytes32 indexed parent);
event SwapLeaf(bytes32 indexed oldLeaf, bytes32 indexed newLeaf);

function VirtualToken() public{
    owner = msg.sender;
    totalSupply = 1000000 * 10**18;
    DataLog(keccak256(address(this), "balance", msg.sender),1,0, bytes32(totalSupply));
}

function getKeyProof(bytes32 key, bytes data) pure returns(bytes32[] result){
    result = new bytes32[](8);
    uint bytePosition = 0;
    while(bytePosition <= data.length){
        uint length = uint256(BytesLib.toBytes32(BytesLib.slice(data, bytePosition, 32)));
        bytes32 thisKey = BytesLib.toBytes32(BytesLib.slice(data, bytePosition + 32, 32));
        if(thisKey == key){
            for(uint thisGroup = 1; thisGroup < length; thisGroup++){
                bytes32 aGroup = BytesLib.toBytes32(BytesLib.slice(data,bytePosition + (32 * thisGroup), 32));

                result[thisGroup - 1] = aGroup;
            }
            return ;
        }
        bytePosition = bytePosition + (32 * length);
    }
}

event DataLog(bytes32 path, uint logType, uint nonce, bytes32 value);

bytes32[8192] public bottomLayer;
uint bottomLayerPosition;
bytes32[] public topLayer;

function pushLog(uint logType, uint nonce, bytes32 value, bytes32[] proof, bytes startBytes) internal returns(bytes result) {
  result = startBytes;
  result = BytesLib.concat(result, BytesLib.fromBytes32(proof[0]));
  result = BytesLib.concat(result, BytesLib.fromBytes32(bytes32(logType)));
  result = BytesLib.concat(result, BytesLib.fromBytes32(bytes32(nonce)));
  result = BytesLib.concat(result, BytesLib.fromBytes32(value));
  //DataLog(proof[0], logType, uint(proof[2]) + 1, value);
}

function pushDel(bytes32[] proof, bytes startBytes) internal returns(bytes result){
    result = startBytes;
    result = pushLog(2, uint(proof[2]), proof[3], proof, result);
}

function pushAdd(bytes32 newValue, bytes32[] proof, bytes startBytes) internal returns(bytes result){
    result = startBytes;
    result = pushLog(1, uint(proof[2]) + 1, newValue, proof, result);
}

function publishCollation(bytes32 newBottomCollation, bytes32 newTopCollation) public {
    if(newTopCollation == 0x0){
        bottomLayer[bottomLayerPosition] = newBottomCollation;
        bottomLayerPosition++;
    } else {
        topLayer.push(newTopCollation);
        bottomLayerPosition = 0;
    }

}


function vTransfer(bytes data) view returns(bytes results){

    address sender = address(getKeyProof(keccak256("msg.sender"), data)[1]);
    bytes32[] memory senderProof = getKeyProof(keccak256(address(this), "balance", sender), data);
    require(verifyProof(keccak256(address(this), "balance", sender), senderProof));
    uint senderBalance =  uint(senderProof[3]);

    address destination = address(getKeyProof(keccak256("destination"), data)[1]);
    bytes32[] memory destinationProof = getKeyProof(keccak256(address(this),"balance", destination), data);
    require(verifyProof(keccak256(address(this), "balance", destination), destinationProof));
    uint destinationBalance = uint(destinationProof[3]);

    uint amount = uint(getKeyProof(keccak256("amount"), data)[1]);

    require(senderBalance >= amount);

    //invalidate existing proofs
    //todo: what if an item goes to 0

    results = pushDel(senderProof, results);
    if (destinationBalance > 0) {
     results = pushDel(destinationProof, results);
    }

    //publish new proofs
    destinationBalance = destinationBalance + amount;
    senderBalance = senderBalance - amount;

    if(senderBalance > 0){
        results = pushAdd(bytes32(senderBalance), senderProof, results);
    }

    if(destinationBalance > 0){
        results = pushAdd(bytes32(destinationBalance), destinationProof, results);
    }

    return results;

}

/* things that need to be in the log
    inputDataHash: -> maybe put everything but signature here
        sender:
        gasPrice:
        maxGas:
        timeOut:
        contract:
        function:
        data:
        value:
        nonce:
    signatureInputDataHash: -> to ecrecover sender

    //value will need to be stored in escrow until the op can be proven to have run and a reciept generated

    //structure of data passed to a function

    [length][variableName][loghash][value][map][map][map][map][proof]...[proof]

    [length][path][nonce][type][value][proff]...[proof]


*/

event Transfer(bytes32 dataHash, bytes signature);

function transfer(bytes32 dataHash, bytes sig) public{
    Transfer(dataHash, sig);
}

 //utility function that can verify merkel proofs
//todo: can be optimized
//todo: move to library
function calcRoot(bytes32 path,  bytes32[] proof) constant public returns(bytes32 lastHash, uint logType, uint nonce, bytes32 proofPath, bytes32 value){
   for(uint thisProof = 0; thisProof < proof.length; thisProof++){
    if(thisProof == 0){
       //path
       require(path == proof[thisProof]);
       proofPath = path;
     } else if(thisProof == 2){
         nonce = uint(proof[thisProof]);
     } else if(thisProof == 1){
         //type
         logType = uint(proof[thisProof]);
         if(logType == 3){
             //null
             return;
         }
     } else if(thisProof == 3){
       value = proof[thisProof];
       lastHash = keccak256(path,logType, nonce, value);
     } else if(proof[thisProof] == 0x0){
        return;
     } else{
       if(proof[thisProof] == lastHash){
        if(proof[thisProof + 1] != 0x0) {
          lastHash = keccak256(lastHash, proof[thisProof + 1]);
        } else {
          lastHash = keccak256(lastHash);
        }

         thisProof++;
       } else {
         require(proof[thisProof + 1] == lastHash);
         lastHash = keccak256(proof[thisProof], lastHash);
         thisProof++;
       }
     }
    }
    return;
}

//utility function that can verify merkel proofs
//todo: can be optimized
//todo: move to library
function verifyProof(bytes32 path,  bytes32[] proof) constant public returns(bool){
   bytes32 lastHash;
   bytes32 emptyBytes;
   bytes32 value;
   uint nonce;
   uint logType;

   (lastHash, logType, nonce, path,  value) = calcRoot(path, proof);

   if(nonce == 3){
       return true;
   }

   for(uint thisLayer; thisLayer < bottomLayer.length; thisLayer++){
       if(bottomLayer[thisLayer] == lastHash){
           return true;
       }
   }

   for(thisLayer = 0; thisLayer < topLayer.length; thisLayer++){
       if(topLayer[thisLayer] == lastHash){
           return true;
       }
   }
   return false;
 }



}

   

The following code builds a little tree generator that helps manage the state of a tree and to produce Merkle proofs.  These proofs can probably be streamlined as I'm including derived hashes in the proofs for simplicity's sake.  Although since we are doing these virtually the size of the proofs only affects the bandwidth for collators, and the proofs aren't that big.

class DataLogTree
  constructor: ()->
    @web3Utils = require('web3-utils')
    @root = null
    @layers = []
    @layers.push([])
  addItem: (logType, nonce, path, value)=>
    @layers[0].push [logType, nonce, path, value]
  getVar: (key, value)=>
    map = []
    map.push(@web3Utils.padLeft(@web3Utils.toHex(3),64))
    map.push(key)
    map.push(value)
    return map
  getNullProof: (key)=>
    map = []
    map.push(@web3Utils.padLeft(5,64))
    map.push key
    map.push(@web3Utils.padLeft(@web3Utils.toHex(3),64))
    map.push(@web3Utils.padLeft(@web3Utils.toHex(0),64))
    map.push(@web3Utils.padLeft(@web3Utils.toHex(0),64))
    return map
  proofBytes: (items)=>
    bytes = "0x"
    items.map (item)=>

      item.map (subItem)=>
        bytes = bytes + subItem.slice(2)
        return
      return
    return bytes
  proofBytesArray: (myBytes)=>
    @web3Utils.hexToBytes(myBytes)
  parseLogs: (logsBytes)=>
    logsBytes = logsBytes.slice(2)
    position = 0
    logs = []
    while position < logsBytes.length
      logs.push [
        "0x" + logsBytes.substring(position,position + 64)
        "0x" + logsBytes.substring(position + 64,position + 128)
        "0x" + logsBytes.substring(position + 128,position + 192)
        "0x" + logsBytes.substring(position + 192,position + 256)
      ]
      position = position + 256
    return logs
  getProof: (type, nonce, path)=>
    map = []
    seek = null
    console.log type
    console.log nonce
    console.log path
    # 'looking for ' + key
    # to produce a proof we look through each layer from bottom to top looking for first the
    # passed in key and then the hash combination
    for thisLayer in [0...@layers.length]
      console.log 'seeking layer '+ thisLayer
      for thisItem in [0...@layers[thisLayer].length]
        console.log 'inspecting:' + thisItem
        console.log @layers[thisLayer][thisItem]
        if thisLayer is 0
          console.log 'in 0'
          if @layers[thisLayer][thisItem][0] is path and @layers[thisLayer][thisItem][1] is type and @layers[thisLayer][thisItem][2] is nonce
            console.log 'found 0'
            map.push @layers[thisLayer][thisItem][0]
            map.push @layers[thisLayer][thisItem][1]
            map.push @layers[thisLayer][thisItem][2]
            map.push @layers[thisLayer][thisItem][3]
            console.log map
            seek = @hasher map[0], map[1], map[2], map[3]
            console.log 'new seek is ' + seek
            break
        else
          # The found item will be either on the left or right hand side
          if @layers[thisLayer][thisItem][0] is seek
            # console.log 'found seek in position 0'
            # push the item onto the proof and find the next item
            map.push seek
            if @layers[thisLayer][thisItem][1]?
              map.push @layers[thisLayer][thisItem][1]
              seek = @hasher @layers[thisLayer][thisItem][0], @layers[thisLayer][thisItem][1]
            else
              map.push @web3Utils.padLeft(0,64)
              seek = @hasher @layers[thisLayer][thisItem][0]
            console.log 'new seek is ' + seek
            console.log map
            break
          if @layers[thisLayer][thisItem][1] is seek
            #console.log 'found seek in position 1'
            # push the item onto the proof and find the next item
            map.push @layers[thisLayer][thisItem][0]
            map.push seek
            seek = @hasher @layers[thisLayer][thisItem][0], @layers[thisLayer][thisItem][1]
            console.log 'new seek is ' + seek
            console.log map
            break
          if thisItem is @layers[thisLayer].length
            throw 'seek not found'
    if seek is @root
      map.unshift(@web3Utils.padLeft(@web3Utils.toHex(map.length + 1),64))
      return map
    else
      throw 'root not found'
  hasher:(val1, val2, val3, val4) =>
    if val4?
      hash = @web3Utils.soliditySha3({t:"bytes",v:val1},{t:"bytes",v:val2},{t:"bytes",v:val3},{t:"bytes",v:val4})
    else if val2?
      hash = @web3Utils.soliditySha3({t:"bytes",v:val1},{t:"bytes",v:val2})
    else
      hash = @web3Utils.soliditySha3({t:"bytes",v:val1})
    return hash
  buildTree: ()=>
    if @layers[1]?.length > 0
      @layers = [@layers[0]]
    console.log @layers[0].length
    pair = []
    currentLayer = 0
    console.log 'currentLayer:' + currentLayer
    console.log 'currentLayer Length:' + @layers[currentLayer].length

    if @layers[currentLayer].length is 1
      console.log @layers[currentLayer]
      hash = @hasher @layers[currentLayer][0][0], @layers[currentLayer][0][1], @layers[currentLayer][0][2], @layers[currentLayer][0][3]
      @root = hash
      console.log @root
      return

    while @layers[currentLayer]? and @layers[currentLayer].length > 1
      console.log 'in layer loop'
      console.log currentLayer
      console.log @layers[currentLayer]
      console.log 'end'

      @layers.push []
      console.log @layers
      #console.log @layers[currentLayer]

      for thisItem in @layers[currentLayer]
        console.log thisItem
        #console.log 'odd item' if thisItem.length != 2

        if currentLayer is 0
          console.log 'building 0 layer'
          hash = @hasher thisItem[0], thisItem[1], thisItem[2], thisItem[3]
          console.log hash
        else
          console.log 'building layer ' + currentLayer
          if thisItem[1]?
            hash = @hasher thisItem[0], thisItem[1]
          else
            hash = @hasher thisItem[0]

        #console.log hash
        pair.push hash
        console.log 'update pair'
        console.log pair.length

        if pair.length is 2
          console.log 'pushing hash'
          @layers[currentLayer + 1].push [pair[0],pair[1]]
          pair = []

      if pair.length is 1
        console.log 'pushing leftover hash'
        @layers[currentLayer + 1].push [pair[0]]
        pair = []
      console.log 'advancing layer'
      currentLayer = currentLayer + 1
      if currentLayer > 16
        throw new Error('yo')
    console.log 'done'
    console.log @layers

    @root = @hasher @layers[@layers.length - 1][0][0], @layers[@layers.length - 1][0][1]
    console.log @root


exports.DataLogTree = DataLogTree

    

Here is a truffle scenario that runs three transactions

console.log 'hello'
web3Utils = require('web3-utils')
VirtualToken =  artifacts.require('./VirtualToken.sol')
DataLogTree = require('../src/DataLogTree.js')



contract 'VirtualToken', (paccount)->
  owner = paccount[0]
  firstPayee = paccount[1]
  secondPayee = paccount[2]

  setUpNetwork = (options)=>
    return new Promise (resolve, reject)=>
      results = {}
      console.log 'creating virtual token'
      tree = new DataLogTree.DataLogTree()
      web3.eth.getBlockNumber (err, result)=>
        startBlock = result
        console.log startBlock
        token = await VirtualToken.new(from: owner)
        resolve
          token: token
          startBlock: startBlock
          tree: tree

  it "can set crete token", ->
    network = await setUpNetwork()
    logs = await new Promise (resolve, reject)=>
      network.token.DataLog({},{fromBlock: network.startBlock,toBlock:'latest'}).get (err, foundLogs)=>
        resolve foundLogs



    console.log logs

    genesisLog = null
    logs.map (o)->
      console.log o
      if o.event is 'DataLog'
        genesisLog = o
        #console.log [o.args.logType]#, web3Utils.padLeft(web3Utils.toHex(o.nonce),64), o.path, o.value]
        network.tree.addItem(o.args.path, web3Utils.padLeft(web3Utils.toHex(o.args.logType),64), web3Utils.padLeft(web3Utils.toHex(o.args.nonce),64), o.args.value)
        #console.log o.args

    console.log 'building tree'
    network.tree.buildTree()

    console.log network.tree.root

    console.log 'getting proof'
    proof = network.tree.getProof(web3Utils.padLeft(web3Utils.toHex(genesisLog.args.logType),64), web3Utils.padLeft(web3Utils.toHex(genesisLog.args.nonce),64), genesisLog.args.path)
    console.log proof

    expectedRoot = web3Utils.soliditySha3(proof[1],proof[2],proof[3],proof[4])

    assert.equal expectedRoot, network.tree.root

    txn = await network.token.publishCollation(expectedRoot, "0x0")

    transactionBytes = []
    transactionBytes.push network.tree.getVar(web3Utils.soliditySha3("msg.sender"), web3Utils.padLeft(owner, 64))
    transactionBytes.push proof
    transactionBytes.push network.tree.getVar(web3Utils.soliditySha3("amount"), web3Utils.padLeft(web3Utils.toHex(1000), 64))
    transactionBytes.push network.tree.getVar(web3Utils.soliditySha3("destination"), web3Utils.padLeft(firstPayee,64))
    transactionBytes.push network.tree.getNullProof(web3Utils.soliditySha3(network.token.address,"balance", firstPayee))

    console.log transactionBytes
    myBytes = network.tree.proofBytes(transactionBytes)

    console.log myBytes

    console.log '["' + network.tree.proofBytesArray(myBytes).join('","') + '"]'

    txn = await network.token.vTransfer.call(myBytes)

    console.log ' trying second transaction ********'

    logs = network.tree.parseLogs txn

    console.log logs

    state1Tree = new DataLogTree.DataLogTree()


    for thisItem in logs
      state1Tree.addItem(thisItem[0], thisItem[1], thisItem[2], thisItem[3])

    console.log 'building state1Tree'

    state1Tree.buildTree()

    txn = await network.token.publishCollation(state1Tree.root, "0x0")

    console.log 'getting proof'
    proof = state1Tree.getProof(web3Utils.padLeft(web3Utils.toHex(1),64), web3Utils.padLeft(web3Utils.toHex(1),64), web3Utils.soliditySha3(network.token.address,"balance", firstPayee))
    console.log proof

    transactionBytes = []
    transactionBytes.push network.tree.getVar(web3Utils.soliditySha3("msg.sender"), web3Utils.padLeft(firstPayee, 64))
    transactionBytes.push proof
    transactionBytes.push network.tree.getVar(web3Utils.soliditySha3("amount"), web3Utils.padLeft(web3Utils.toHex(500), 64))
    transactionBytes.push network.tree.getVar(web3Utils.soliditySha3("destination"), web3Utils.padLeft(secondPayee,64))
    transactionBytes.push network.tree.getNullProof(web3Utils.soliditySha3(network.token.address,"balance", secondPayee))

    console.log 'transaction bytes for second transaction'
    console.log transactionBytes

    myBytes = network.tree.proofBytes(transactionBytes)


    txn = await network.token.vTransfer.call(myBytes)

    console.log 'second transaction results'

    logs = network.tree.parseLogs txn

    console.log logs

    console.log ' trying third transaction ******************************************'

    state2Tree = new DataLogTree.DataLogTree()

    for thisItem in logs
      state2Tree.addItem(thisItem[0], thisItem[1], thisItem[2], thisItem[3])

    console.log state2Tree.layers[0]

    state2Tree.buildTree()

    txn = await network.token.publishCollation(state2Tree.root, "0x0")

    proof = state2Tree.getProof(web3Utils.padLeft(web3Utils.toHex(1),64), web3Utils.padLeft(web3Utils.toHex(1),64), web3Utils.soliditySha3(network.token.address,"balance", secondPayee))
    console.log proof

    secondProof = state1Tree.getProof(web3Utils.padLeft(web3Utils.toHex(1),64), web3Utils.padLeft(web3Utils.toHex(1),64), web3Utils.soliditySha3(network.token.address,"balance", owner))


    transactionBytes = []
    transactionBytes.push network.tree.getVar(web3Utils.soliditySha3("msg.sender"), web3Utils.padLeft(secondPayee, 64))
    transactionBytes.push proof
    transactionBytes.push network.tree.getVar(web3Utils.soliditySha3("amount"), web3Utils.padLeft(web3Utils.toHex(250), 64))
    transactionBytes.push network.tree.getVar(web3Utils.soliditySha3("destination"), web3Utils.padLeft(owner,64))
    transactionBytes.push secondProof

    myBytes = network.tree.proofBytes(transactionBytes)


    txn = await network.token.vTransfer.call(myBytes)

    logs = network.tree.parseLogs txn

    console.log logs

    assert.equal 1, 0, "hello"

   

Apologies for the coffeescript...it is 2.0 now so you get all the ES6 goodies.

So far only the red highlighted areas are working:

Scalable Stateless Contracts on Today's EVM Diagram step1.png

 

There are obviously a ton of areas that have big questions:

1.  Can we create a viable consensus amongst collators?
2.  How do we incentivize them?
3.  How to handle no ops?
4.  Have we really just put ethereum inside of ethereum and erased a lot of the checks that ethereum puts on run away gas costs?

I'm open to suggestions for 1-3.

For number four I think it is valuable to keep pushing this string.  There are certainly times where one would want the current EVM to do bigger things.  For example:  Using this I could write an air drop contract that loads up 1,000 balances for way less gas than it would currently cost...if I can get the collators to validate the transaction for me.

*The proof format here is [length, path, type(1=add, 2=del, 3=null), nonce, value, left leaf0, right leaf0.... left leafn, right leafn] where odd layers have an 0x0 on the end but the hash for those odd layers is calculated as the hash of just the left item.  Open to better suggestions.

Bug Bounty Doubled - $400 - Truffle Tests Released

We are back from DevCon 3 in Cancun.  It was a great week.  We learned a lot and had some great conversations.  We are really excited about moving Catallax forward and getting our decaying currency up and running on the main net.

In the meantime, we've doubled the bug bounty on the Catallax Trust to $400.  We are getting closer to our first opportunity to do a withdrawal on November 16th.

I've also released the truffle tests source.  Reviewing these should give you some idea of how the contract works.

You can find the source code for the contracts here: https://github.com/skilesare/catallaxtrust

Pull down the repo and load them up in remix to interact with the contracts.

If this is interesting to you and you'd like to see where we are going with Catallax, please pick up my book Immortality (Purchase of a physical or kindle copy helps support this project).

Donations always accepted at:

BTC: 1AAfkhg1NEQwGmwW36dwDZjSAvNLtKECas

ETH and Tokens: 0x148311c647ec8a584d896c04f6492b5d9cb3a9b0

If you would like more code articles like this please consider becoming a patron on patreon.

You can discuss this article and more at our reddit page r/Catallax.

Testing With Time - Dev Log 7

Weekly Update

  1. I’m a Dad again(for the fourth time).  Weston James was born on Tuesday evening June the 20th.  Mom and baby are healthy.  Big Bro, Big Sis, and Big Sis 2 are super excited, and Dad is really proud.
  2. I guess I’m a professional solidity developer as someone paid me actual money(well ETH) for the contract we are discussing today.  Not a lot, but enough be significant.
  3. Work continues on the whitepaper for the first service offering that Catallax will be trying to launch.  Lawyers, accountants, legalese, etc., etc.

Today we are going to do some testing with time.  The is going to be very important once we start testing our demurrage code.  In our decaying token we used passing block numbers to calculate our decay periods.  In the example today we are going to use the actual time system in Ethereum.  This system isn’t an exact science since we are somewhat dependent on the Miners to give a truthful timestamp on each block.  You would not use this system if you were doing calculations on short time scales because a miner could attempt to cheat your contract.  Our example today uses month long periods so we should be fine using time.
 
The contract we are going to develop is called a DisciplineWallet.  Do you have lambo money from buying into the ethereum crowdsale two years ago?  Afraid you’re going to get spooked and sell?  Use the DisciplineWallet and restrict access to your ETH on a monthly basis.
 
When we first started talking about this contract the client wanted the payout to be consistent in USD.  We went back and forth and finally decided that we needed to do it in ETH because there isn’t really a good USD to ETH Oracle that is reliable over long periods of time.  If you know of one, please let me know and I’ll try to update this contract to do USD instead.
 
Our contract will do the following:

  • Be initialized with a term in months and an amount of ETH to pay out each month
  • Take Deposits in ETH
  • Expose a Withdraw function that can be called once a month to send the specified amount of ETH to the owner of the contract
  • Let us change the owner of the contract
  • Withdraw all the remaining ETH to a specified address once the term has run its course
  • Give us a safety function to move coins out of the contact if we accidentally send ERC20 tokens to the contract
  • Use the fallback function as a deposit function
  • Tell us when the next withdrawal is available

Once we write up our contract we are going to want to test it.  To do this we are going to need a blockchain environment that will let us make time go by quickly. Fortunately, the testrpc that is packaged with the truffle dev environment lets us do this with the following call:

web3.currentProvider.sendAsync({
       jsonrpc: "2.0",
       method: "evm_increaseTime",
       params: [86400 * 34],
       id: new Date().getTime()
     }


In the above example, we are moving the clock forward 34 days(86400 seconds in a day).  Our tests will do withdrawals and then move the clock forward and do withdrawals again.
 
Our contract will need to pass the following tests:
 
    ✓ should Deploy a wallet with owner (91ms)
    ✓ should be off when it starts and has no ether (69ms)
    ✓ should have a function Period that returns the length of a month (102ms)
    ✓ should have a function NextWithdraw that returns the first withdraw time (94ms)
    ✓ should turn on when sent ether (775ms)
    ✓ should fail if constructor sent ether
    ✓ should allow withdraw after 1 month and ether goes to owner (750ms)
    ✓ should fail on instant withdraw (255ms)
    ✓ should not allow more than term number of withdraws (955ms)
    ✓ should not allow withdraw if some time has passed, but not enough (382ms)
    ✓ should allow withdraw all after term is over (3389ms)
    ✓ should fail if withdrawAll is called before term is over (388ms)
    ✓ should reject payment if payout term is expired (2778ms)
    ✓ should deposit using deposit function (245ms)
    ✓ should allow for transfer of owner (1529ms)
    ✓ should reduce payout if payout would drain account before the end of the term (679ms)

The code and truffle set up can be found on GitHub here. 
Here is our contact.

pragma solidity ^0.4.11;

contract DisciplineWallet {

  /**
  * @dev term is the number of months that a wallet will pay out
  */
  uint16 public term;


  /**
  * @dev currentTerm tracks the number of withdraws that have occured
  */
  uint16 public currentTerm;

  /**
  * @dev contractStart stores the timestamp(number of seconds since 1/1/1970) that the contract was created
  */
  uint public contractStart;

  /**
  * @dev payout is the number of wei(smallest unit of ETH) to pay out
  */
  uint public payout;


  /**
  * @dev bActive is tracks if the wallet is open for business or not
  */
  bool public bActive;
  address public owner;

  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    if (msg.sender != owner) {
      throw;
    }
    _;
  }

  /**
   * @dev constructor
   * @param termLength number of months that the contract will be active
   * @param weiOutputPerPeriod of wei to pay out
   */
  function DisciplineWallet(uint16 termLength, uint weiOutputPerPeriod) {

    if(termLength < 1) throw; //term cant be 0
    if(weiOutputPerPeriod < 1) throw; //we cant pay out 0

    //the owner will start as the address creating the contract
    owner = msg.sender;

    //set the term
    term = termLength;

    //record the time of the start contract
    contractStart = now;

    //start with the current term as 1
    currentTerm = 1;

    //set the payout per period
    payout = weiOutputPerPeriod;
  }


  /**
   * @dev calculates the length of a month
   */
  function Period() constant returns (uint32 lengthOfMonth){
    //put this in a constant function so that we don't have to use storage
    //execution takes 302 gas which means calculation is more efficent in any contract under 66 months.
    //not sure about deployment costs
    //the period is about 30.66 days so that leap year is taken into account every 4 years.
    return (1 years / 12) + (1 days / 4);
  }

  /**
   * @dev tells us what date we can call the withdraw function
   */
  function NextWithdraw() constant returns(uint secondsAfter1970){
    return contractStart + (Period() * currentTerm);
  }

  /**
   * @dev the fallback function will make a deposit
   */
  function () payable{
    Deposit();
  }

  /**
   * @dev Put ETH into the contract, also called by the fallback function
   */
  function Deposit() payable {
    //don't accept ether if the term is over
    if(currentTerm > term) throw;
    if(bActive == false){
      //first time we get ether, turn on the contract
      bActive = true;
    }
  }

  /**
   * @dev Withdraw will send the payout to the owner if enough time has passed
   */
  function Withdraw() onlyOwner returns(bool ok){
    if(currentTerm <= term && now > NextWithdraw()){

      //payout may not be more than balance / term or the account has been underfunded
      //if it is then use the lower calculatio
      if(payout < (this.balance / (term - currentTerm + 1))){
        owner.transfer(payout);
      }
      else{
        owner.transfer(this.balance / term);
      }

      currentTerm = currentTerm + 1;

      if (currentTerm > term){
        bActive = false;
      }

      return true;
    } else{
      throw;
    }

  }


  /**
   * @dev once all terms have expired you can move any remaining ETH to another account
   * @param targetAddress The address to transfer remaining ETH to.
   */
  function WithdrawAll(address targetAddress) onlyOwner returns(bool ok){
    if (targetAddress == 0x0) throw; //dont accidentally burn ether
    if(currentTerm > term && this.balance > 0){
      targetAddress.transfer(this.balance);
      bActive = false;
      return true;
    } else {
      throw;
    }

  }

  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) onlyOwner {
    if (newOwner != address(0)) {
      owner = newOwner;
    }
  }

  /**
   * @dev Allows the owner to send tokens elsewhere that were accidentally sent to this account
   * @param Token The address of the ERC20 token
   * @param _to The address the tokens should end up at
   * @param _value The amount of tokens
   */
  function safetyTransferToken(address Token, address _to, uint _value) onlyOwner returns (bool ok){
    bytes4 sig = bytes4(sha3("transfer(address,uint256)"));
    return Token.call(sig, _to, _value);
  }

}


The tests are in CoffeeScript and can easily be compiled to js. The core test concept here is the passage of time.
 
In the "it should allow withdraw after 1 month and ether goes to owner" test we simulate the passing of 34 days via our testrpc evm_increaseTime function mentioned earlier.

Our test code needs to do this multiple time over a set amount of time.  I use a variant of the following function:

withdrawFunction = ()->
  return q.Promise (resolve, reject)->
    web3.currentProvider.sendAsync
      jsonrpc: "2.0",
      method: "evm_increaseTime",
      params: [86400 * 34],  # 86400 seconds in a day
      id: new Date().getTime()
    , (err)->
      i.Withdraw(from: accounts[0])
      .then (result)->
        resolve result
      .catch (err)->
        reject err

We can call it in a loop like this:

async.eachSeries [1..13], (item, done)->
        #console.log item
        withdrawFunction()
        .then (result)->
          i.currentTerm.call(from: accounts[0])
        .then (result)->
          #console.log result
          done()
        .catch (err)->
          #console.log err
          done(err)
    .then (result)->
      assert(false, "shouldnt be here")
    .catch (error)->
      if error.toString().indexOf("invalid op") > -1
        #console.log("We were expecting a Solidity throw (aka an invalid op), we got one. Test succeeded.")
        assert.equal error.toString().indexOf("invalid op") > -1, true, 'didnt find invalid op throw'
      else
        assert(false, error.toString())
  done()

In the above case we call withdraw 13 times and expect it to fail on the 13th try.
 
We now have a great way to test the passage of time when testing our solidity contracts.  Since Catallax will likely do monthly this avenue of calculation may work.  We could manipulate our Decay Token to use timestamps instead of block numbers.  On the other hand, using block numbers allows us more control if we need to do some emergency decay as a monetary lever.

If this is interesting to you and you'd like to see where we are going with Catallax, please pick up my book Immortality(Purchasing a hard or kindle copy helps support this project.

Donations always accepted at:

BTC: 1AAfkhg1NEQwGmwW36dwDZjSAvNLtKECas

ETH and Tokens: 0x148311c647ec8a584d896c04f6492b5d9cb3a9b0