From 39b100b865ce705ef1641459d66b383f5f654dcd Mon Sep 17 00:00:00 2001 From: Joseph Poon Date: Fri, 18 Dec 2015 11:29:35 -0800 Subject: [PATCH] Serialize Funding Request and script for CLTV-only --- lnwallet/channel.go | 57 ++++++++++ lnwallet/reservation.go | 205 +++++++++++++++++++++++++++++++++++- revocation/shachain_test.go | 2 + 3 files changed, 261 insertions(+), 3 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index d557ca59..be207b15 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -27,6 +27,63 @@ func P2SHify(scriptBytes []byte) ([]byte, error) { return bldr.Script() } +//TODO(j): Creates a CLTV-only funding Tx (reserve is *REQUIRED*) +//This works for only CLTV soft-fork (no CSV/segwit soft-fork in yet) +// +//Commit funds to Funding Tx, will timeout after the fundingTimeLock and refund +//back using CLTV. As there is no way to enforce HTLCs, we rely upon a reserve +//and have each party's HTLCs in-transit be less than their Commitment reserve. +//In the event that someone incorrectly broadcasts an old Commitment TX, then +//the counterparty claims the full reserve. It may be possible for either party +//to claim the HTLC(!!! But it's okay because the "honest" party is made whole +//via the reserve). If it's two-funder there are two outputs and the +//Commitments spends from both outputs in the Funding Tx. Two-funder requires +//the ourKey/theirKey sig positions to be swapped (should be in 1 funding tx). +// +//Quick note before I forget: The revocation hash is used in CLTV-only for +//single-funder (without an initial payment) *as part of an additional output +//in the Commitment Tx for the reserve*. This is to establish a unidirectional +//channel UNITL the recipient has sufficient funds. When the recipient has +//sufficient funds, the revocation is exchanged and allows the recipient to +//claim the full reserve as penalty if the incorrect Commitment is broadcast +//(otherwise it's timelocked refunded back to the sender). From then on, there +//is no additional output in Commitment Txes. [side caveat, first payment must +//be above minimum UTXO output size in single-funder] For now, let's keep it +//simple and assume dual funder (with both funding above reserve) +func createCLTVFundingTx(fundingTimeLock int64, ourKey *btcec.PublicKey, theirKey *btcec.PublicKey) (*wire.MsgTx, error) { + script := txscript.NewScriptBuilder() + //See how many entries there are + //2: it's a 2-of-2 multisig + //anything else: assume it's a CLTV-timeout 1-sig only + script.AddOp(txscript.OP_DEPTH) + script.AddInt64(2) + script.AddOp(txscript.OP_EQUAL) + + //If this is a 2-of-2 multisig, read the first sig + script.AddOp(txscript.OP_IF) + //Sig2 (not P2PKH, the pubkey is in the redeemScript) + script.AddData(ourKey.SerializeCompressed()) + script.AddOp(txscript.OP_CHECKSIGVERIFY) //gotta be verify! + + //If this is timed out + script.AddOp(txscript.OP_ELSE) + script.AddInt64(fundingTimeLock) + script.AddOp(txscript.OP_NOP2) //CLTV + //Sig (not P2PKH, the pubkey is in the redeemScript) + script.AddOp(txscript.OP_CHECKSIG) + script.AddOp(txscript.OP_DROP) + script.AddOp(txscript.OP_ENDIF) + + //Read the other sig if it's 2-of-2, only one if it's timed out + script.AddData(theirKey.SerializeCompressed()) + script.AddOp(txscript.OP_CHECKSIG) + + fundingTx := wire.NewMsgTx() + //TODO(j) Add the inputs/outputs + + return fundingTx, nil +} + // createCommitTx... func createCommitTx(fundingOutput *wire.TxIn, ourKey, theirKey *btcec.PublicKey, revokeHash [32]byte, csvTimeout int64, amtToUs, diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index ad36a04b..c58dd81b 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -1,8 +1,11 @@ package wallet import ( + "fmt" "sync" + "bytes" + "encoding/binary" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" @@ -12,15 +15,26 @@ import ( type ChannelReservation struct { FundingType FundingType - FundingAmount btcutil.Amount - ReserveAmount btcutil.Amount - MinFeePerKb btcutil.Amount + //All of these are *our* values/requirements + //Their requirements can be the same or lower + FundingAmount btcutil.Amount //The amount we want to fund with + ReserveAmount btcutil.Amount //Our reserve. assume symmetric reserve amounts + MinFeePerKb btcutil.Amount + MinTotalFundingAmount btcutil.Amount //Our minimum value for the entire channel + + //for CLTV it is nLockTime, for CSV it's nSequence, for segwit it's not needed + FundingLockTime uint32 sync.RWMutex // All fields below owned by the lnwallet. + //Current state of the channel, progesses through until complete + //Makes sure we can't go backwards and only accept messages once + channelState uint8 + theirInputs []*wire.TxIn ourInputs []*wire.TxIn + //NOTE(j): FundRequest assumes there is only one change (see ChangePkScript) theirChange []*wire.TxOut ourChange []*wire.TxOut @@ -43,9 +57,194 @@ type ChannelReservation struct { reservationID uint64 wallet *LightningWallet + //For CSV/CLTV revocation + ourRevocation []byte + theirRevocation []byte + theirRevocationHash []byte + + //Final delivery address (P2PKH for now) + ourDeliveryAddress []byte + theirDeliveryAddress []byte + chanOpen chan *LightningChannel } +//FundRequest serialize +//(reading from ChannelReservation directly to reduce the amount of copies) +//We can move this stuff to another file too if it's too big... +func (r *ChannelReservation) SerializeFundRequest() ([]byte, error) { + var err error + + //Buffer to dump in the serialized data + b := new(bytes.Buffer) + + //Fund Request + err = b.WriteByte(0x30) + if err != nil { + return nil, err + } + + //ChannelType (1) + //Default to current type + err = b.WriteByte(uint8(0)) + if err != nil { + return nil, err + } + + //RequesterFundingAmount - The amount we are going to fund (8) + //check for positive values + err = binary.Write(b, binary.BigEndian, r.FundingAmount) + if err != nil { + return nil, err + } + + //RequesterChannelMinCapacity (8) + //The amount needed to accept and sign the channel commit later + err = binary.Write(b, binary.BigEndian, r.MinTotalFundingAmount) + if err != nil { + return nil, err + } + + //RevocationHash (20) + //Our revocation hash being contributed (for CLTV/CSV) + _, err = b.Write(btcutil.Hash160(r.ourRevocation)) + if err != nil { + return nil, err + } + + //CommitPubkey (33) + //Our public key being used for the commitment + ourPubKey := r.ourKey.PubKey().SerializeCompressed() + if len(ourPubKey) != 33 { //validation, can remove later? (NO UNCOMPRESSED KEYS!) + return nil, fmt.Errorf("Serialize FundReq: our Pubkey length incorrect") + } + _, err = b.Write(ourPubKey) + if err != nil { + return nil, err + } + + //DeliveryPkHash (20) + //For now it's a P2PKH, but we will add an extra byte later for the + //option for P2SH + //This is the address to send funds to when complete or refunded + _, err = b.Write(r.ourDeliveryAddress) + if err != nil { + return nil, err + } + + //ReserveAmount (8) + //Our own reserve amount + err = binary.Write(b, binary.BigEndian, r.ReserveAmount) + if err != nil { + return nil, err + } + + //Minimum transaction fee per kb (8) + err = binary.Write(b, binary.BigEndian, r.MinFeePerKb) + if err != nil { + return nil, err + } + + //LockTime (4) + err = binary.Write(b, binary.BigEndian, r.FundingLockTime) + if err != nil { + return nil, err + } + + //Fee payer (default to split currently) (1) + err = binary.Write(b, binary.BigEndian, 0) + if err != nil { + return nil, err + } + + //ChangePkScript + //Length (1) + changeScriptLength := len(r.ourChange[0].PkScript) + if changeScriptLength > 255 { + return nil, fmt.Errorf("Your changeScriptLength is too long!") + } + err = binary.Write(b, binary.BigEndian, uint8(changeScriptLength)) + if err != nil { + return nil, err + } + //For now it's a P2PKH, but we will add an extra byte later for the + //option for P2SH + //This is the address to send change to (only allow one) + //ChangePkScript (length of script) + _, err = b.Write(r.ourChange[0].PkScript) + if err != nil { + return nil, err + } + + //Append the unsigned(!!) txins + //First one byte for the amount of txins (1) + if len(r.ourInputs) > 127 { + return nil, fmt.Errorf("Too many txins") + } + err = b.WriteByte(uint8(len(r.ourInputs))) + if err != nil { + return nil, err + } + //Append the actual Txins (NumOfTxins * 36) + //Do not include the sequence number to eliminate funny business + for _, in := range r.ourInputs { + //Hash + _, err = b.Write(in.PreviousOutPoint.Hash.Bytes()) + if err != nil { + return nil, err + } + //Index + err = binary.Write(b, binary.BigEndian, in.PreviousOutPoint.Index) + if err != nil { + return nil, err + } + } + + return b.Bytes(), err +} + +func (r *ChannelReservation) DeserializeFundRequest(wireMsg []byte) error { + //Make sure we're not overwriting stuff... + //Update the channelState to 1 before progressing if you want to re-do it. + //Assumes only one thread is writing at a time + if r.channelState > 1 { + return fmt.Errorf("FundRequest: Channel State Mismatch") + } + + var err error + + b := bytes.NewBuffer(wireMsg) + msgid, _ := b.ReadByte() + if msgid != 0x30 { + return fmt.Errorf("Cannot deserialize: not a funding request") + } + + if err != nil { + return err + } + //Update the channel state as complete + r.channelState = 2 + + return nil +} + +//Validation on the data being supplied from the fund request +func (r *ChannelReservation) ValidateFundRequest(wireMsg []byte) error { + return nil +} + +//Serialize CSV Revocation +//After the Commitment Transaction has been created, send a message to revoke this tx +func (r *ChannelReservation) SerializeCSVRefundRevocation() ([]byte, error) { + return nil, nil +} + +//Deserialize CSV Revocation +//Validate the revocation, after this step, the channel is fully set up +func (r *ChannelReservation) DeserializeCSVRefundRevocation() error { + return nil +} + // newChannelReservation... func newChannelReservation(t FundingType, fundingAmt btcutil.Amount, minFeeRate btcutil.Amount, wallet *LightningWallet, id uint64) *ChannelReservation { diff --git a/revocation/shachain_test.go b/revocation/shachain_test.go index e66dd379..39f8849e 100644 --- a/revocation/shachain_test.go +++ b/revocation/shachain_test.go @@ -1 +1,3 @@ package revocation + +import "testing"