π The Ethereum blockchain has great decentralization & security properties. These properties come at a price: transaction throughput is low, and transactions can be expensive (search term: blockchain trilemma). This makes many traditional web applications infeasible on a blockchain... or does it?
π° A number of approaches to scaling have been developed, collectively referred to as layer-2s (L2s). Among them is the concept of payment channels, state channels, and state channel networks. This tutorial walks through the creation of a simple state channel application, where users seeking a service lock collatoral on-chain with a single transaction, interact with their service provider entirely off-chain, and finalize the interaction with a second on-chain transaction.
π§βπ€βπ§ State channels really excel as a scaling solution in cases where a fixed set of participants want to exchange value-for-service at high frequency. The canonical example is in file sharing or media streaming: the server exchanges chunks of a file in exchange for micropayments.
π§ In our case, the service provider is a
Guru
who provides off-the-cuff wisdom to each clientRube
through a one-way chat box. Each character of text that is delivered is expected to be compensated with a payment of0.001 ETH
.
We will:
- π£οΈ Build a
Streamer.sol
contract that collects ETH from numerous client addresses using a payablefundChannel()
function and keeps track ofbalances
. - π΅ Exchange paid services off-chain between the
Streamer.sol
contract owner (the Guru) and rube clients with funded channels. The Guru provides the service in exchange for signed vouchers which can later be redeemed on-chain. - β± Create a Challenge mechanism with a timeout, so that rubes are protected from a Guru who goes offline while funds are locked on-chain (either by accident, or as a theft attempt).
- β Consider some security / usability holes in the current design.
// todo
π The final deliverable is deploying a Dapp that lets users send ether to a contract and stake if the conditions are met, then
yarn build
andyarn surge
your app to a public webserver. Submit the url on SpeedRunEthereum.com!
// todo
π¬ Meet other builders working on this challenge and get help in the Challenge 1 telegram!
git clone https://github.com/statechannels/speedrun-statechannels.git challenge-N-statechannels
cd challenge-N-statechannels
yarn install
Files that we'll be editing in this tutorial are:
00_deploy_streamer.js
inpackages/hardhat/deploy
Streamer.sol
inpackages/hardhat/contracts
App.jsx
inpackages/react-app/src
π Tip: entry points for each of the checkpoints that involve writing code can be located by searching these files for
Checkpoint N
(for whateverN
value)
You'll have three terminals up for:
yarn start (react app frontend)
yarn chain (hardhat backend)
yarn deploy (to compile, deploy, and publish your contracts to the frontend)
β οΈ Note:yarn deploy
will report an unexpected error. We will fix this in Checkpoint 2 and redeploy.
π» View your frontend at http://localhost:3000/
π©βπ» Rerun
yarn deploy --reset
whenever you want to deploy new contracts to the frontend.
Like the token vendor challenge, we'll be building an Ownable
contract. The contract owner is the Guru - the service provider in this application, and you will use multiple browser windows or tabs to assume the roles of Guru and rube (service provider & client).
π
contract Streamer
inheritsOwnable
with theis
keyword.Ownable
comes from openzeppelin-contracts - a collection of high quality smart contract library code.
π In
packages/hardhat/deploy/00_deploy_streamer.js
, supply an address from your frontend wallet to thestreamer.transferOwnership()
function.
You'll have to redeploy with yarn deploy --reset
.
We'll need another active address to act as the rube in our app. To do this,
- open
localhost:3000
in a new tab / window of the current browser - click the wallet icon (top right) to open the wallet
private key
->generate
will reload the page under a new address
The wallet icon now lets you move between accounts. Eventually you'll probably want a few wallets & windows open simultaneously.
(Note: previous challenges created alt addresses by opening the page with a private window or a different browser. This will not work for this challenge, because the off-chain application uses a very simple communication pipe that doesn't work between different browsers or private windows.)
- does your original frontend address recieve the
Hello Guru
UI? - do your alternate addresses recieve the
Hello Rube
UI?
Like the decentralized staking challenge, we'll track balances for individual channels / users in a mapping:
mapping (address => uint256) balances;
Rubes seeking wisdom will use a payable fundChannel()
function, which will update this mapping with the supplied balance.
π Edit Streamer.sol to complete the
fundChannel()
function
π Check App.jsx to see the frontend hook for this function. (ctrl-f fundChannel)
- does opening a channel (from Rube's tab, you may need some funds from the faucet) cause a
Recieved Wisdom
box to appear? - do opened channels appear on the guru's UI as well?
- using the Debug Contracts tab, does a repeated call to
fundChannel
fail?
Now that the channel is funded, and all participants have observed the funding via the emitted event, we can begin our off-chain exchange of service. We are now working in packages/react-app/src/App.jsx
.
Functions of note:
provideService
: the guru sends wisdom over the wire to the clientreimburseService
: the rube creates a voucher for the recieved service, signs it, and returns itprocessVoucher
: the service provider recieves and stores vouchers
The first two are complete - we will work on processVoucher
, where the service provider examines returned payments, confirms their authenticity, and stores them.
π Edit App.jsx to complete the
processVoucher()
function and secure this off-chain exchange. You'll need to recreate the encoded message that the client has signed, and then verify that the received signature was in fact produced by the client on that same data.
- secure your service! Validate the incoming voucher & signature according to instructions inside
processVoucher(v)
- with an open channel, start sending advice. Can you see the claimable balance update as service is rendered?
- can
provideService
be modified to prevent continued service to clients who don't keep up with their payments? (hint: you'll want to compare the size of your best voucher against the size of your provided wisdom. If there's too big a discrepency, cut them off!)
Now that we've collected some vouchers, we'd like to redeem them on-chain and move funds from the Streamer
contract's balances
map to the Guru's own address. The withdrawEarnings
function of Streamer.sol
takes a voucher (balance + signature) as input, and should:
- recover the signer using
ecrecover()
on theprefixedHashed
message and supplied signature- Hint:
ecrecover
takes the signature in its decomposed form withv,
,r
, ands
values. The string signature produced inApp.jsx
is just a concatenation of these values, which we split to create the on-chain friendly signature withethers.utils.splitSignature
- Hint:
- check that the signer has a running channel with balance greater than the voucher's
updatedBalance
- calculate the payout (
balances[signer] - updatedBalance
) - update the channel balance
- pay the contract owner
Reminders:
- changes to contracts must be redeployed to the local chain with
yarn deploy --reset
. - for troubleshooting / debugging, your contract can use hardhat's
console.log
, which will print to your console running the chain
π Edit Streamer.sol to complete the
withdrawEarnings()
function as described
π Edit App.jsx to enable the UI button for withdrawals
- Recover funds on-chain for services rendered! After the guru submits a voucher to chain, you should be able to see the wallet's ETH balance increase.
-
withdrawEarnings
is a function that only the service provider would be interested in calling. Should it be markedonlyOwner
? (theonlyOwner
modifier makes a function accessible only to the contract owner - anyone else who tries to call it will be immediately rejected).
So far so good:
- rubes can connect to the Guru via an on-chain deposit
- the pair can then transact off-chain with high throughput
- the Guru can recover earnings with their received vouchers
But what if a rube is unimpressed with the service, and wishes to close a channel and recover whatever funds remain? What if the guru is a no-show after the initial channel funding deposit?
A payment channel is a cryptoeconomic protocol - care needs to be taken so that everyone's financial interests are protected. We'll implement a two step challenge and close mechanism that allows rubes to recover unspent funds, while keeping the guru's earnings safe.
π Edit Streamer.sol to create a public
challengeChannel()
function
π Edit App.jsx to enable the challenge and closure buttons for service clients
The challengeChannel()
function should:
- check in the
balances
map that a channel is already open in the name ofmsg.sender
- declare this channel to be closing by setting
canCloseAt[msg.sender]
toblock.timestamp + 30 seconds
- emit a
Challenged
event with the sender's address
The emitted event gives notice to the Guru that the channel will soon be emptied, so they should apply whatever vouchers they have before the timeout period ends.
π Edit Streamer.sol to create a public
defundChannel()
function
The defundChannel()
function should:
- check that
msg.sender
has a closed channel, by ensuring a non-zerocanCloseAt[msg.sender]
is lower than the current timestamp - transfer
balances[msg.sender]
to the sender - emit a
Closed
event
- Launch a challenge as a channel client. The guru's UI should show an alert via their
Cash out latest voucher
button. - Recover the guru's best voucher before the channel closes.
- Close the channel and recover rube funds.
- Currently, the service provider has to manually submit their vouchers after a challenge is registered on chain. Should their channel wallet do that automatically? Can you implement that in this application?
- Suppose some rube enjoyed their first round of advice. Is it safe for them to open a new channel with
Streamer.sol
? (Hint: what data does the guru still hold?)
// todo: write tests
- Now is a good time to run
yarn test
to run the automated testing function. It will test that you hit the core checkpoints. You are looking for all green checkmarks and passing tests!
π‘ Edit the defaultNetwork
to your choice of public EVM networks in packages/hardhat/hardhat.config.js
π©βπ You will want to run yarn account
to see if you have a deployer address
π If you don't have one, run yarn generate
to create a mnemonic and save it locally for deploying.
β½οΈ You will need to send ETH to your deployer address with your wallet.
π If you plan on submitting this challenge, be sure to set your
deadline
to at leastblock.timestamp + 72 hours
π Run
yarn deploy
to deploy your smart contract to a public network (selected in hardhat.config.js)
π Edit the
targetNetwork
inApp.jsx
(inpackages/react-app/src
) to be the public network where you deployed your smart contract.
π» View your frontend at http://localhost:3000/
π‘ When you are ready to ship the frontend app...
π¦ Run yarn build
to package up your frontend.
π½ Upload your app to surge with yarn surge
(you could also yarn s3
or maybe even yarn ipfs
?)
π¬ Windows users beware! You may have to change the surge code in
packages/react-app/package.json
to just"surge": "surge ./build",
β If you get a permissions error yarn surge
again until you get a unique URL, or customize it in the command line.
π you will use this deploy URL to submit to SpeedRunEthereum.com.
π Traffic to your url might break the Infura rate limit, edit your key: constants.js
in packages/ract-app/src
.
Update the api-key in packages/hardhat/package.json file. You can get your key here.
Now you are ready to run the
yarn verify --network your_network
command to verify your contracts on etherscan π°
π Head to your next challenge here.
π¬ Problems, questions, comments on the stack? Post them to the π scaffold-eth developers chat