diff --git a/lnwire/ESCROWED_HTLC_NOTES b/lnwire/ESCROWED_HTLC_NOTES new file mode 100644 index 00000000..2beb510f --- /dev/null +++ b/lnwire/ESCROWED_HTLC_NOTES @@ -0,0 +1,105 @@ +NOTE: Not implemented in code, only included as part of the wire protocol for +future implementation! + +There are multiple R-value hashes supported in HTLCs in the wire protocol. This +is to support conditional multiparty payments, e.g. 2-of-3 "escrow", which is +one of the biggest use cases of bitcoin scripting today. An example use case is +a 3rd party "escrow" verifies whether a seller should be paid. This design is +such that the "escrow" is not a normal escrow which holds custody, but +determines who should get the money in the event of non-cooperation. + +In this implementation, we are including *future protocol support* but not +writing code yet for 2-of-3, it is to be implemented later. Arbitrary N-of-M +can be supported, but let's keep this simple for now! + +How it works: Require 2-of-3 R-value preimages (from 3 hashes) in order for the +HTLC to be fulfilled. For each hop in the payment, it requires this 2-of-3 +condition. The timeout minimum for each hop in the path is at least the minimum +agreed contractual escrow timeout. This means each hop consumes a higher amount +of time-value (due to much longer timeouts along all channels in the path), +which does have greater pressure towards lower hop-distances compared to +straight payments. + +This is a slightly different way of thinking about things. It's not signatures +that the escrow produces (or for that matters any of the 3-parties in the +2-of-3). It's some secret which is revealed to authorize payment. So if the +Escrow wants the payment to go through, they disclose the secret (R-value) to +the recipient. If the recipient is unable to produce 2-of-3, after the agreed +timeout, the sender will be refunded. Sender and receiver can agree to authorize +payment in most cases where there is cooperation, escrow is only contacted if +there is non-cooperation. + +Supported in the wire protocol for the unit8: +1: 1-of-1 +2: 1-of-2 +3: 2-of-2 +4: 1-of-3 +5: 2-of-3 +6: 3-of-3 + +I think the only ones that really matter are 1-of-1, 2-of-3, and maybe 2-of-2 +and 3-of-3 (in that order of declining importance). + +Assume the order in the stack is Sender, Recipient, Escrow. + +For PAID 2-of-3 Recipient+Escrow, the HTLC stack is: + <0> <0> + +If it's REFUND because 2-of-3 has not been redeemed in time: + <1> + +Script (we use OP_1/OP_0 to distinctly show computed true/false. 0/1 is for +supplied data as part of the sigScript/redeemScript stack): +------------------------------------------------------------------------------- +//Paid +OP_IF + //Optional... OP_CSV + + OP_HASH160 OP_EQUAL + //Stack: <0> + OP_SWAP + //Stack: <0> + OP_HASH160 OP_EQUAL + //Stack: <0> + OP_ADD + //Stack: <0> + OP_SWAP + //Stack: <0> + OP_HASH160 OP_EQUAL + //Stack: + OP_ADD + //Stack: + <2> OP_EQUALVERIFY + + + //Stack: +//Refund +OP_ELSE + //Optional... OP_CSV + + OP_CLTV + + //Stack: +OP_ENDIF +OP_CHECKSIG +------------------------------------------------------------------------------- + +Note: It is possible that Alice and Bob may not be Sender, Recipient, nor +Escrow! + +If we have all 3 R-values, we only include 2 and include a dummy zero on the +third. + +The result? We can do 2-of-3 escrow payments which refund to the sender after a +timeout! The Buyer and Seller can agree to redeem and they only need to go to +the Escrow if there's a dispute. Each node along the path gets paid or refunded +atomically, the same as a single-HTLC payment on Lightning. + +Ta-da! "Smart Contract(TM)" maymay. + +Immediately refundable payments (2-of-3 can immediately cancel a payment) are +also possible but requires another payment in the opposite direction with the +R-value hashed twice (the R value becomes the H), but that's kind of annoying to +write... it's easier to just assume immediate refund can only occur if both +buyer+seller agree to cancel the payment immediately (otherwise it will wait +until the timeout). diff --git a/lnwire/funding_request.go b/lnwire/funding_request.go index aa8c5527..4396258b 100644 --- a/lnwire/funding_request.go +++ b/lnwire/funding_request.go @@ -31,6 +31,7 @@ type FundingRequest struct { RevocationHash [20]byte Pubkey *btcec.PublicKey DeliveryPkScript PkScript //*MUST* be either P2PKH or P2SH + //FIXME: Need a ChangePkScript PkScript for dual-funder CLTV? Inputs []*wire.TxIn } @@ -137,21 +138,21 @@ func (c *FundingRequest) Validate() error { } //PkScript is either P2SH or P2PKH - //P2PKH if len(c.DeliveryPkScript) == 25 { + //P2PKH //Begins with OP_DUP OP_HASH160 PUSHDATA(20) - if !(bytes.Equal(c.DeliveryPkScript[0:3], []byte{118, 169, 20}) && + if !bytes.Equal(c.DeliveryPkScript[0:3], []byte{118, 169, 20}) && //Ends with OP_EQUALVERIFY OP_CHECKSIG - bytes.Equal(c.DeliveryPkScript[23:25], []byte{136, 172})) { + !bytes.Equal(c.DeliveryPkScript[23:25], []byte{136, 172}) { //If it's not correct, return error return fmt.Errorf("PkScript only allows P2SH or P2PKH") } - //P2SH } else if len(c.DeliveryPkScript) == 23 { + //P2SH //Begins with OP_HASH160 PUSHDATA(20) - if !(bytes.Equal(c.DeliveryPkScript[0:2], []byte{169, 20}) && + if !bytes.Equal(c.DeliveryPkScript[0:2], []byte{169, 20}) && //Ends with OP_EQUAL - bytes.Equal(c.DeliveryPkScript[22:23], []byte{135})) { + !bytes.Equal(c.DeliveryPkScript[22:23], []byte{135}) { //If it's not correct, return error return fmt.Errorf("PkScript only allows P2SH or P2PKH") } diff --git a/lnwire/message.go b/lnwire/message.go index e87dc04e..e88d901d 100644 --- a/lnwire/message.go +++ b/lnwire/message.go @@ -9,7 +9,6 @@ import ( ) //4-byte network + 4-byte message id + payload-length 4-byte -//Maybe add checksum or something? const MessageHeaderSize = 12 const MaxMessagePayload = 1024 * 1024 * 32 // 32MB @@ -48,7 +47,6 @@ type messageHeader struct { magic wire.BitcoinNet command uint32 length uint32 - //Do we need a checksum here? } func readMessageHeader(r io.Reader) (int, *messageHeader, error) { @@ -122,7 +120,6 @@ func WriteMessage(w io.Writer, msg Message, pver uint32, btcnet wire.BitcoinNet) hdr.magic = btcnet hdr.command = cmd hdr.length = uint32(lenp) - //Checksum goes here if needed... and also add to writeElements // Encode the header for the message. This is done to a buffer // rather than directly to the writer since writeElements doesn't @@ -189,8 +186,6 @@ func ReadMessage(r io.Reader, pver uint32, btcnet wire.BitcoinNet) (int, Message return totalBytes, nil, nil, err } - //If we want to use checksums, test it here - //Unmarshal message pr := bytes.NewBuffer(payload) err = msg.Decode(pr, pver)