Released: Wednesday, April 6th 12:00pm EST
Due: Tuesday, April 19th 11:59pm EST
Now that we're familiar with a blockchain's Layer 1 from Coin, it's time that we move on to a Layer 2 scaling solution! In class, you've learned about off-chain payment channels. Lightning is a scaling protocol for Bitcoin, and we've done our best to imitate that here.
While we did cover Lightning (very) briefly in class, we strongly recommend that you watch the following 20 min video to better understand what we're asking you to do. Without a strong conceptual understanding, this project will be a toughie. And that would suck :(
Learn More →
For this assignment, you will implement the functions listed below. We recommend that you tackle the functions in the following order:
pkg/node.go: BroadcastTransaction
pkg/server.go: GetWitnesses
pkg/server.go: ForwardTransaction
pkg/wallet.go: GenerateFundingTransaction
pkg/wallet.go: HandleRevokedOutput
lightning/server.go: OpenChannel
lightning/server.go: GetUpdatedTransactions
lightning/server.go: GetRevocationKey
lightning/channel.go: CreateChannel
lightning/lightningnode.go: UpdateState
lightning/watchtower.go: HandleBlock
Additionally, we recommend that you and your partner divide and conquer the work. While your functions may depend on something your partner's in charge of implementing, you can (and should) still work asynchronously! To see how difficult we think each function is, check out our grading breakdown.
The segregated witness protocol is one of the most beloved soft forks in Bitcoin. By segregating the witnesses from the transactions, miners can squeeze more transactions into each block. While they may or may not receive more in fees (long-term equilibrium anyone?), it certainly increases the throughput of the network. It's estimated that signatures take up ~65% of a block's capacity. By removing signatures from blocks, Bitcoin's scalability signifcantly improves!
This part of the assignment should be relatively quick: it's only updating a few lines of code here and there.
Stencil update:
We've added two new fields to transactions: segwit and witnesses. Segwit is a boolean flag letting a node know whether this transaction is employing the segregated witness protocol. Witnesses are a list of signatures on a transaction.
Unlike most normal hashing methods (which hash all of the fields), our transaction hashes do NOT include the slice of witnesses. Instead, they omit them, allowing signatories to see a transaction and later add their signature, WITHOUT altering the transaction's hash.
Before:
type Transaction struct {
Version uint32
Inputs []*TransactionInput
Outputs []*TransactionOutput
LockTime uint32
}
Now:
type Transaction struct {
Segwit bool
Version uint32
Inputs []*TransactionInput
Outputs []*TransactionOutput
Witnesses [][]byte
LockTime uint32
}
context.Context
:You do NOT need to worry about context for this assignment. Whenever you see ctx context.Context
as a method parameter for a server-side function, don't worry about it!
// BroadcastTransaction broadcasts transactions created by the
// wallet to other peers in the network.
func (n *Node) BroadcastTransaction(tx *block.Transaction)
// GetWitnesses is called by another SegWit node to get the witnesses (signatures) from you.
func (n *Node) GetWitnesses(ctx context.Context, in *pro.Transaction) (*pro.Witnesses, error)
pro.Witnesses
struct.status.Errorf(codes.Internal, "Your error message")
: Since this is an RPC, we use the status package when handling errors.// ForwardTransaction Handles forward transaction request (tx propagation)
func (n *Node) ForwardTransaction(ctx context.Context, in *pro.TransactionWithAddress) (*pro.Empty, error)
SeenTransactions
and returnSeenTransactions
with a count of 1.func (a *Address) GetWitnessesRPC(request *pro.Transaction) (*pro.Witnesses, error)
: for when we need the sender to get us the witnesses!We were introduced to the wallet in the last project, but now it has to form transactions for Lightning channels!
LockingScripts:
Bitcoin has its very own scripting language, which permits the creation of coins that have to be signed by multiple people. While we didn't create our own scripting language, we did our best to emulate Bitcoin Script with our 3 different types of locking scripts in pkg/script/lockingscript.go
.
PayToPublicKey: this is the most common type of lockingScript. If I want to send a coin to someone, they only need to prove that they are the owner of the public key to use it (validation requires their private key).
MultiParty: These are the types of scripts that we will be making use of for our Lightning channels. They contain the public keys for both members of a channel, as well as a revocation key and a number of additional blocks. No one can use the TransactionOutput
unless both parties have signed off on the Transaction
as witnesses!
HashedTimeLock: We (unfortunately) won't be using these for this assignment, but this is how we can create hops between multiple nodes and truly take advantage of the Lightning network!
// GenerateFundingTransaction is very similar to RequestTransaction, except it does NOT broadcast to the node.
// Also, the outputs are slightly different.
func (w *Wallet) GenerateFundingTransaction(amount uint32, fee uint32, counterparty []byte) *block.Transaction
fee
passed in to the function is actually worth 2 fees! Half is to cover the funding transaction, and the other half is to cover the refund transaction (which the Lightning node handles later on).amount + fee
. This is because we need to provide enough for both the funding and the refund transaction.generateTransactionOutputs
Won't work for this.amount
. The second is the coin for the counterparty, which always has an amount of 0
. If there is a third output, it's change that goes back to the channel's funder (since it's using their coins) once the channel is closed. Change needs to include the fee for the refund transaction.lockingScript
for all of the coins. We'll use a MultiParty
locking script to do so. In this case, there is no revocation key, so you can leave that field as []byte{}
. This script enforces that both parties have signed off on the funding transaction.proto.Marshal(m message) ([]byte, error)
: how to convert a pro.MultiParty
to a byte slice.generateTransactionInputs
: to create the inputs for our transaction// ForwardTransaction Handles forward transaction request (tx propagation)
// HandleRevokedOutput returns true if it successfully handles revoking
// the transaction
func (w *Wallet) HandleRevokedOutput(hash string, txo *block.TransactionOutput, outIndex uint32, secRevKey []byte, scriptType int) *block.Transaction
lockingScript
. If it isn't, we can just return nilPayToPublicKey
with our public key.w.Config.DefaultFee
.RevKeySuccessful(lockingScript []byte, secRevKey []byte, scriptType int) bool
: returns whether the secret revocation key validates the lockingScript
proto.Marshal(m message) ([]byte, error)
: how to convert a pro.PayToPublicKey
to a byte slice.utils.Sign(sk *ecdsa.PrivateKey, h []byte) ([]byte, error)
: how to sign something. A transaction's hash is a string, which we can quickly convert to bytes.A Lightning node runs its own server, which is how it receives requests from other lightning nodes and responds to them. You'll be implementing the server-side responsibilities of a lightning node: handling open channel requests, updated transaction requests, and revocation key requests.
Both Alice and Bob have their own versions of each state, since the revocable outputs are different for each side. Tx(1,A) is Alice's transaction for state 1. Rev(3,B) Is Bob's revocation key for his transaction at state 3.
In the example below, Alice and Bob have an existing channel set up. Alice wants to pay Bob, so she has to be the one to initiate the protocol. If Bob wanted to pay Alice, the process would be inverted.
// OpenChannel is called by another lightning node that wants to open a channel with us
func (ln *LightningNode) OpenChannel(ctx context.Context, in *pro.OpenChannelRequest) (*pro.OpenChannelResponse, error)
PeerDb
. If they aren't, we should stop this function's execution early and return.TheirTransactions
and MyTransactions
. That won't be the case in the future!MyRevocationKeys
, which we'll send over when we want to update state later.PeerDb.Get(string) *Peer
: returns nil if the address is not in our PeerDb
.func (ln *LightningNode) ValidateAndSign(tx *block.Transaction) error
: how to validate and sign a transactionGenerateRevocationKey()
: generates a public, private key pair (in byte form)func (ln *LightningNode) GetUpdatedTransactions(ctx context.Context, in *pro.TransactionWithAddress) (*pro.UpdatedTransactions, error)
TheirTransactions
and the secret key we just made to MyRevocationKeys
.func (ln *LightningNode) generateTransactionWithCorrectScripts(peer *peer.Peer, theirTx *block.Transaction, pubRevKey []byte) *block.Transaction
: creates a transaction with the correct locking scripts.func (ln *LightningNode) GetRevocationKey(ctx context.Context, in *pro.SignedTransactionWithKey) (*pro.RevocationKey, error)
MyTransactions
.RevocationInfo
that contains all of the information necessary to claim the counterparty's coin from the previous state.RevocationInfo
, we need to figure out what type of script they were using on their transaction and figure out which coin their revocation key belongs to.TheirRevocationKeys
.func DetermineScriptType(b []byte) (int, error)
: how to determine the type of a locking scriptNow we're taking on Alice's side of the protocol. An owner of a lightning node probably wants to establish a couple of channels, so that they have more opportunities to take part in multi-channel hops (not part of this assignment) and hopefully collect some routing fees along the way!
Here's the protocol from above, for easier reference.
// CreateChannel creates a channel with another lightning node
// fee must be enough to cover two transactions! You will get back change from first
func (ln *LightningNode) CreateChannel(peer *peer.Peer, theirPubKey []byte, amount uint32, fee uint32)
WalletRequest
to the wallet, so that it can use valid coins to create the funding transaction for us. The fee here has to be twice the fee provided as an argument, to cover both the funding and refund transaction.MyRevocationKeys
.func (ln *LightningNode) generateFundingTransaction(request WalletRequest) *block.Transaction
: How to get a funding transaction from the wallet.func (ln *LightningNode) generateRefundTransaction(theirPubKey []byte, fundingTx *block.Transaction, fee uint32, revKey []byte) *block.Transaction
: How to create a refund transaction given a funding transaction.func (a *Address) OpenChannelRPC(request *pro.OpenChannelRequest) (*pro.OpenChannelResponse, error)
: RPC call to request opening a channel with another lightning node.// UpdateState is called to update the state of a channel.
func (ln *LightningNode) UpdateState(peer *peer.Peer, tx *block.Transaction)
MyTransactions
, sign their transaction, and add their transaction to TheirTransactions
.RevocationInfo
in TheirRevocationKeys
.func (a *Address) GetUpdatedTransactionsRPC(request *pro.TransactionWithAddress) (*pro.UpdatedTransactions, error)
: making an RPC call on the address's server to get our transaction signed and their updated transaction.func (a *Address) GetRevocationKeyRPC(request *pro.SignedTransactionWithKey) (*pro.RevocationKey, error)
: making an RPC call on the address's server to get the revocation key for their transaction.//HandleBlock handles a block and figures out if we need to revoke a transaction
func (w *WatchTower) HandleBlock(block *block.Block) *RevocationInfo
This assignment is autograded, and you are able to run our test suite as many time as you like. While you can run all of your tests on Gradescope, we strongly recommend that you test locally. That way, you can take advantage of GoLand's debugger to see where your tests are failing.
In addition, you should write your own tests when things aren't working! We have provided several helper functions in test/mocking_utils.go
and test/testing_utils.go
.
To test your project, cd
into test
and run go test
. This will let you know which tests are failing, and why. It will likely be more convenient to run individual tests, which you can do using the GoLand UI.
Note: most of our tests will cause the debugger to panic because they rely on functions that have not yet been implemented.
go get ./...
to install the project's dependencies.Note: This assignment's feedback form is a REQUIRED part of the assignment, worth 5 points.
You can find that form here.
This assignment is out of 100 points. Your grade will be determined by completing the feedback form and passing the following test functions:
Test Function | Points |
---|---|
TestGetWitnessesRPC |
5 |
TestForwardTransaction |
5 |
TestGenerateFundingTransaction |
10 |
TestHandleRevokedTransaction |
10 |
TestOpenChannel |
10 |
TestGetUpdatedTransactions |
15 |
TestGetRevocationKey |
10 |
TestCreateChannel |
10 |
TestUpdateState |
15 |
TestWatchTowerHandleBlock |
5 |
Feedback Form Completion | 5 |
Total | 100 |