The goal is to set up a local Raiden network running in Docker containers, allowing for easier exploration and experimentation than using the Ropsten testnet version.
Advantages:
- No need to keep a synced Ropsten node up and running.
- No need to get hold of Ropsten test Ether.
- Faster, more predictable blocktimes.
- No tricky NAT issues to deal with.
- Easy tear-down and restart - it's not permanent :-)
The set-up is a single Docker container representing the Ethereum blockchain, in the form of a geth --dev
node, and N containers for Raiden clients that communicate with each other and the blockchain.
Raiden is at the "Developer Preview" stage, and comes with a disclaimer and notes. The official Dev Preview version is 0.2.0, but that has a bug around closing channels. For this reason we are using a more recent commit. Unfortunately, that breaks the nice Web GUI... we'll have to live without it for now.
Background reading on Raiden:
- Raiden Network: Vision, Challenges and Roadmap
- A gentle introduction to the key points
- Raiden FAQ
- Lots of ELI5 stuff.
- What is the Raiden Network?
- A 101 on the technology. The next level down.
- The official documentation
- The Raiden GitHub
There are some prerequisites:
- Docker (with docker-compose), and
- Node.js, with web3 installed. web3 must be version 1.0.
- The Solidity compiler
My environment:
> uname -a
Linux Lubuntu-Nov17 4.13.0-21-generic #24-Ubuntu SMP Mon Dec 18 17:29:16 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
> docker --version
Docker version 17.12.0-ce, build c97c6d6
> node --version
v8.9.3
> npm --version
5.6.0
> npm list web3
/home/ben/Everything/Ethereum/Raiden/Local-Raiden
└── web3@1.0.0-beta.26
> /usr/local/bin/solc --version
solc, the solidity compiler commandline interface
Version: 0.4.19-develop.2018.1.3+commit.c4cbbb05.Linux.g++
Since we include the Raiden repository as a submodule, you will likely need to do the following after cloning (or do a recursive clone).
git submodule update --init
We use the official Docker image for Geth, which you can get with,
docker pull ethereum/client-go
We need to build an image for the Raiden client. Do the following in the Local-Raiden directory. This will take a while, but needs doing only once.
docker build -t my/raiden .
Raiden essentially consists of two components.
-
The Raiden client that we built above. This communicates with other Raiden clients to make payments, and occasionally with the blockchain to open, close and settle payment channels.
-
A set of smart contracts that need to be deployed.
So, we need to deploy the contracts to our test network. Raiden provides a Python script for this, but my Python-fu is weak and I ended up with all sorts of dependency issues. So I reverted to something more familiar and made the deploy.js Node.js script instead.
Working in the Local-Raiden directory, first clear out any old Geth or Raiden data. Do not do this on your Mainnet configuration! This deletes the keystore.
rm -rf keystore geth raiden_data geth.ipc
Now check the configuration options near the top of the deploy.js file and adjust anything that needs adjusting. For example, you may need to edit the path to the Solidity compiler, SOLC
.
Start a Geth Docker container. All the config is already set up in the docker-compose.yml file, so we can just do the following.
docker-compose run -u $UID geth
Finally, run the deployment script.
DEBUG=1 node deploy.js
This will output progress, and a summary of the final status. It will also create a .env file containing environment variables to be passed to docker-compose.
Finally, stop Geth (Ctrl-C
will do it). All being well, everything is now set up on the blockchain within the geth directory. Don't delete this directory if you want to keep the blockchain state.
The output should resemble the following, different addresses for accounts and contracts.
Summary
=======
Deployment account: 0x78910ad1D145B20fdcd31B20D43D82dd998C194A
Account_0: 0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A
balance: 2000000000000000000000
tokens: 250000
Account_1: 0x1563915e194D8CfBA1943570603F7606A3115508
balance: 2000000000000000000000
tokens: 250000
Account_2: 0x5CbDd86a2FA8Dc4bDdd8a8f69dBa48572EeC07FB
balance: 2000000000000000000000
tokens: 250000
Account_3: 0x7564105E977516C53bE337314c7E53838967bDaC
balance: 2000000000000000000000
tokens: 250000
Discovery contract: 0x019Ae5c6C16C1356ccDe9cc2DB5415a259a0F2C5
Registry contract: 0xd1AFb72FFA57e4163175EFB9179bB63b500BB3b0
Token contract: 0x2e8EdB207922794aEcB1A2cDC5a730612eefF034
The deployment script performs the following tasks.
-
Transfers (test) Ether from Geth's pre-funded account to the four accounts that we have set up in keystore/.
-
Deploys contracts as follows,
a. the Discovery contract, EndpointRegistry.sol
b. the Netting Channel Library contract, NettingChannelLibrary.sol
c. the Channel Manager Library contract, ChannelManagerLibrary.sol
d. the Registry contract, Registry.sol
e. an ERC20 Token contract, Token.sol
-
For all of the above, and in addition ChannelManagerContract.sol and NettingChannelContract.sol, the ABI is saved in the abis folder for later use.
-
The tokens in the ERC20 token contract are split equally between the four accounts.
-
Values required by docker-compose are written to the .env file. This .env file is also a useful reference for looking up account and contract addresses later.
Now all we need to do to start the whole network is,
docker-compose up -d
To shut it all down,
docker compose down
Geth and Raiden will store their intermediate states in the geth and raiden_data directories respectively, so you can do this repeatedly.
This sets up two Raiden containers along with a Geth container, configured as follows.
- Geth
- RPC interface:
172.13.0.2:8545
- Mapped to
localhost:8545
(optional: for convenience)
- Mapped to
- RPC interface:
- Raiden 0
- RPC interface:
172.13.0.3:5001
- Mapped to
localhost:5001
(optional: for convenience)
- Mapped to
- Raiden Network interface:
172.13.0.3:40001
- RPC interface:
- Raiden 1
- RPC interface:
172.13.0.4:5001
- Mapped to
localhost:5002
(optional: for convenience)
- Mapped to
- Raiden Network interface:
172.13.0.4:40001
- RPC interface:
You can see the console output of each container with docker logs
. The container names are output by docker-compose, or by executing docker ps
.
The below is a just "getting started". I hoping to work on some articles digging deeper into what's going on "under-the-hood", including mediated transfers that involve multiple nodes. Watch this space...
Now that our Raiden nodes are running, it is possible to interact with them via their RPC interfaces directly from the shell command line:
> curl http://172.13.0.3:5001/api/1/address
{"our_address": "0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a"}
> curl http://172.13.0.4:5001/api/1/address
{"our_address": "0x1563915e194d8cfba1943570603f7606a3115508"}
But this quickly becomes tedious, especially for the more complex operations. I've created some JavaScript classes in the modules/ directory to make this easier. See its README for more info. We use the Node REPL:
> node
First set some useful variables to keep from having to copy/paste long strings: the Ethereum addresses of our two nodes and the token contract. Insert the addresses from the deployment in the below: they will differ from the ones here.
> var acct0 = '0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A'
> var acct1 = '0x1563915e194D8CfBA1943570603F7606A3115508'
> var token_address = '0x02cAf13e4b645b3dBf27f6Ae1647356A2410210F'
Now we import the Raiden interface and make an instance for each node.
> var Rdn = require('./modules/raiden.js')
> var r0 = new Rdn('http://172.13.0.3:5001')
> var r1 = new Rdn('http://172.13.0.4:5001')
We can call methods on these Raiden node objects. Everything is asynchronous (this is JavaScript), and all methods return Promises, hence the clunky .then(console.log)
part. I've also edited the output to clean up all the Promise junk that gets printed. [If anyone knows how to (nicely) make synchronous calls to async functions from the REPL (await
is not available), please get in touch!]
> r0.address().then(console.log)
{ our_address: '0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a' }
> r1.address().then(console.log)
{ our_address: '0x1563915e194d8cfba1943570603f7606a3115508' }
Now, let's register the token. The Registry contract will create a Channel Manager contract that will oversee all channels that exchange this token. This interacts with the blockchain, so takes a few seconds to resolve.
> r0.tokens.register(token_address).then(console.log)
{ channel_manager_address: '0x7f799b2c9fc03f10e8cabdb06bf916402bab1a8f' }
With that done we can create a channel between Node0 and Node1 to allow tokens to be exchanged off-chain. This creates another smart contract called a Netting Channel that is responsible only for transfers of this token between these two nodes. It also makes a token transfer into the Netting Contract from Node0's balance (the deposit, 100 tokens in this case). Once again, it takes a few seconds.
> r0.channels.open(acct1, token_address, 100, 30).then(console.log)
{ partner_address: '0x1563915e194d8cfba1943570603f7606a3115508',
balance: 100,
reveal_timeout: 10,
settle_timeout: 30,
state: 'opened',
channel_address: '0x6432ed34e54ccc4e3219e5ec65f398d02cff2b89',
token_address: '0x02caf13e4b645b3dbf27f6ae1647356a2410210f' }
The settle_timeout
is the last parameter and is the number of blocks that the settlement window remains open for challenges after a channel is closed.
We can now make some transfers!
Node0 sends 50 tokens to Node1. We check balances before and after:
> var channel = '0x6432ed34e54ccc4e3219e5ec65f398d02cff2b89'
> r0.channels.balance(channel).then(console.log)
100
> r1.channels.balance(channel).then(console.log)
0
> r0.transfer(token_address, acct1, 50).then(console.log)
> { target_address: '0x1563915e194d8cfba1943570603f7606a3115508',
amount: 50,
identifier: 6889929806137958000,
token_address: '0x02caf13e4b645b3dbf27f6ae1647356a2410210f',
initiator_address: '0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a' }
> r0.channels.balance(channel).then(console.log)
50
> r1.channels.balance(channel).then(console.log)
50
This is instantaneous! The blockchain is not involved. We can check the token balance on the actual blockchain using a different module provided in the modules/ directory. This uses Web3 to access the Raiden contracts directly.
> var Contracts = require('./modules/raiden-contracts.js')
> var contracts = new Contracts('http://172.13.0.2:8545')
> var token = new contracts.interface(token_address, 'abis/Token.json')
> token.balanceOf(acct0).then(console.log)
249900
> token.balanceOf(acct1).then(console.log)
250000
Node0's actual token balance has decreased from the initial 250000 by the 100 tokens initially lodged as a deposit. Node1's balance is unchanged. The 50 token Raiden transfer has not yet been reflected back to the blockchain, and won't be until the channel is settled.
We can continue to send tokens back and forth between the nodes, subject to a cap of original deposit + net tokens received in this channel. However, to release the tokens back to the blockchain and make the transfers "real", we need to close the channel down. There is (currently) no way to keep the channel open and extract some of the balance.
Node1 will initiate the channel closing process. This involves a call to the Netting Contract, so takes a few seconds.
> r1.channels.close(channel).then(console.log)
{ partner_address: '0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a',
balance: 50,
reveal_timeout: 10,
settle_timeout: 30,
state: 'opened',
channel_address: '0x6432ed34e54ccc4e3219e5ec65f398d02cff2b89',
token_address: '0x02caf13e4b645b3dbf27f6ae1647356a2410210f' }
Eventually, after waiting for settle_timeout
blocks, the channel state is marked 'settled'
and we can see the token balances of each account have been correctly updated on the blockchain. It is no longer possible to make transfers on this channel.
> r1.channels.info(channel).then(console.log)
{ partner_address: '0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a',
balance: 50,
reveal_timeout: 10,
settle_timeout: 30,
state: 'settled',
channel_address: '0x6432ed34e54ccc4e3219e5ec65f398d02cff2b89',
token_address: '0x02caf13e4b645b3dbf27f6ae1647356a2410210f' }
> token.balanceOf(acct0).then(console.log)
249950
> token.balanceOf(acct1).then(console.log)
250050
The configuration here uses a fixed block time of 3 seconds. You can decrease that to make things quicker, or increase it to more closely match real network times. This line in docker-compose.yml,
--dev.period 3
Although dev.period
can be set to zero, meaning that Geth will produce blocks only on demand, I don't recommend it. Raiden channel settlement times are measured in blocks: if no blocks are produced, channels will never settle.