Merge pull request #2439 from cfromknecht/wtpolicy-blob-type
wtpolicy: migrate to using wtpolicy.Policy + blob bitvector flags
This commit is contained in:
commit
53a181ce49
@ -17,12 +17,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// MinVersion is the minimum blob version supported by this package.
|
|
||||||
MinVersion = 0
|
|
||||||
|
|
||||||
// MaxVersion is the maximumm blob version supported by this package.
|
|
||||||
MaxVersion = 0
|
|
||||||
|
|
||||||
// NonceSize is the length of a chacha20poly1305 nonce, 24 bytes.
|
// NonceSize is the length of a chacha20poly1305 nonce, 24 bytes.
|
||||||
NonceSize = chacha20poly1305.NonceSizeX
|
NonceSize = chacha20poly1305.NonceSizeX
|
||||||
|
|
||||||
@ -53,14 +47,14 @@ const (
|
|||||||
// nonce: 24 bytes
|
// nonce: 24 bytes
|
||||||
// enciphered plaintext: n bytes
|
// enciphered plaintext: n bytes
|
||||||
// MAC: 16 bytes
|
// MAC: 16 bytes
|
||||||
func Size(ver uint16) int {
|
func Size(blobType Type) int {
|
||||||
return NonceSize + PlaintextSize(ver) + CiphertextExpansion
|
return NonceSize + PlaintextSize(blobType) + CiphertextExpansion
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlaintextSize returns the size of the encoded-but-unencrypted blob in bytes.
|
// PlaintextSize returns the size of the encoded-but-unencrypted blob in bytes.
|
||||||
func PlaintextSize(ver uint16) int {
|
func PlaintextSize(blobType Type) int {
|
||||||
switch ver {
|
switch {
|
||||||
case 0:
|
case blobType.Has(FlagCommitOutputs):
|
||||||
return V0PlaintextSize
|
return V0PlaintextSize
|
||||||
default:
|
default:
|
||||||
return 0
|
return 0
|
||||||
@ -71,9 +65,9 @@ var (
|
|||||||
// byteOrder specifies a big-endian encoding of all integer values.
|
// byteOrder specifies a big-endian encoding of all integer values.
|
||||||
byteOrder = binary.BigEndian
|
byteOrder = binary.BigEndian
|
||||||
|
|
||||||
// ErrUnknownBlobVersion signals that we don't understand the requested
|
// ErrUnknownBlobType signals that we don't understand the requested
|
||||||
// blob encoding scheme.
|
// blob encoding scheme.
|
||||||
ErrUnknownBlobVersion = errors.New("unknown blob version")
|
ErrUnknownBlobType = errors.New("unknown blob type")
|
||||||
|
|
||||||
// ErrCiphertextTooSmall is a decryption error signaling that the
|
// ErrCiphertextTooSmall is a decryption error signaling that the
|
||||||
// ciphertext is smaller than the ciphertext expansion factor.
|
// ciphertext is smaller than the ciphertext expansion factor.
|
||||||
@ -229,7 +223,7 @@ func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) {
|
|||||||
//
|
//
|
||||||
// NOTE: It is the caller's responsibility to ensure that this method is only
|
// NOTE: It is the caller's responsibility to ensure that this method is only
|
||||||
// called once for a given (nonce, key) pair.
|
// called once for a given (nonce, key) pair.
|
||||||
func (b *JusticeKit) Encrypt(key []byte, version uint16) ([]byte, error) {
|
func (b *JusticeKit) Encrypt(key []byte, blobType Type) ([]byte, error) {
|
||||||
// Fail if the nonce is not 32-bytes.
|
// Fail if the nonce is not 32-bytes.
|
||||||
if len(key) != KeySize {
|
if len(key) != KeySize {
|
||||||
return nil, ErrKeySize
|
return nil, ErrKeySize
|
||||||
@ -238,7 +232,7 @@ func (b *JusticeKit) Encrypt(key []byte, version uint16) ([]byte, error) {
|
|||||||
// Encode the plaintext using the provided version, to obtain the
|
// Encode the plaintext using the provided version, to obtain the
|
||||||
// plaintext bytes.
|
// plaintext bytes.
|
||||||
var ptxtBuf bytes.Buffer
|
var ptxtBuf bytes.Buffer
|
||||||
err := b.encode(&ptxtBuf, version)
|
err := b.encode(&ptxtBuf, blobType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -252,7 +246,7 @@ func (b *JusticeKit) Encrypt(key []byte, version uint16) ([]byte, error) {
|
|||||||
// Allocate the ciphertext, which will contain the nonce, encrypted
|
// Allocate the ciphertext, which will contain the nonce, encrypted
|
||||||
// plaintext and MAC.
|
// plaintext and MAC.
|
||||||
plaintext := ptxtBuf.Bytes()
|
plaintext := ptxtBuf.Bytes()
|
||||||
ciphertext := make([]byte, Size(version))
|
ciphertext := make([]byte, Size(blobType))
|
||||||
|
|
||||||
// Generate a random 24-byte nonce in the ciphertext's prefix.
|
// Generate a random 24-byte nonce in the ciphertext's prefix.
|
||||||
nonce := ciphertext[:NonceSize]
|
nonce := ciphertext[:NonceSize]
|
||||||
@ -270,7 +264,7 @@ func (b *JusticeKit) Encrypt(key []byte, version uint16) ([]byte, error) {
|
|||||||
// Decrypt unenciphers a blob of justice by decrypting the ciphertext using
|
// Decrypt unenciphers a blob of justice by decrypting the ciphertext using
|
||||||
// chacha20poly1305 with the chosen (nonce, key) pair. The internal plaintext is
|
// chacha20poly1305 with the chosen (nonce, key) pair. The internal plaintext is
|
||||||
// then deserialized using the given encoding version.
|
// then deserialized using the given encoding version.
|
||||||
func Decrypt(key, ciphertext []byte, version uint16) (*JusticeKit, error) {
|
func Decrypt(key, ciphertext []byte, blobType Type) (*JusticeKit, error) {
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
// Fail if the blob's overall length is less than required for the nonce
|
// Fail if the blob's overall length is less than required for the nonce
|
||||||
@ -305,7 +299,7 @@ func Decrypt(key, ciphertext []byte, version uint16) (*JusticeKit, error) {
|
|||||||
// If decryption succeeded, we will then decode the plaintext bytes
|
// If decryption succeeded, we will then decode the plaintext bytes
|
||||||
// using the specified blob version.
|
// using the specified blob version.
|
||||||
boj := &JusticeKit{}
|
boj := &JusticeKit{}
|
||||||
err = boj.decode(bytes.NewReader(plaintext), version)
|
err = boj.decode(bytes.NewReader(plaintext), blobType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -315,23 +309,23 @@ func Decrypt(key, ciphertext []byte, version uint16) (*JusticeKit, error) {
|
|||||||
|
|
||||||
// encode serializes the JusticeKit according to the version, returning an
|
// encode serializes the JusticeKit according to the version, returning an
|
||||||
// error if the version is unknown.
|
// error if the version is unknown.
|
||||||
func (b *JusticeKit) encode(w io.Writer, ver uint16) error {
|
func (b *JusticeKit) encode(w io.Writer, blobType Type) error {
|
||||||
switch ver {
|
switch {
|
||||||
case 0:
|
case blobType.Has(FlagCommitOutputs):
|
||||||
return b.encodeV0(w)
|
return b.encodeV0(w)
|
||||||
default:
|
default:
|
||||||
return ErrUnknownBlobVersion
|
return ErrUnknownBlobType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode deserializes the JusticeKit according to the version, returning an
|
// decode deserializes the JusticeKit according to the version, returning an
|
||||||
// error if the version is unknown.
|
// error if the version is unknown.
|
||||||
func (b *JusticeKit) decode(r io.Reader, ver uint16) error {
|
func (b *JusticeKit) decode(r io.Reader, blobType Type) error {
|
||||||
switch ver {
|
switch {
|
||||||
case 0:
|
case blobType.Has(FlagCommitOutputs):
|
||||||
return b.decodeV0(r)
|
return b.decodeV0(r)
|
||||||
default:
|
default:
|
||||||
return ErrUnknownBlobVersion
|
return ErrUnknownBlobType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package blob_test
|
package blob_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/blob"
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||||
)
|
)
|
||||||
@ -38,8 +42,8 @@ func makeAddr(size int) []byte {
|
|||||||
|
|
||||||
type descriptorTest struct {
|
type descriptorTest struct {
|
||||||
name string
|
name string
|
||||||
encVersion uint16
|
encVersion blob.Type
|
||||||
decVersion uint16
|
decVersion blob.Type
|
||||||
sweepAddr []byte
|
sweepAddr []byte
|
||||||
revPubKey blob.PubKey
|
revPubKey blob.PubKey
|
||||||
delayPubKey blob.PubKey
|
delayPubKey blob.PubKey
|
||||||
@ -52,11 +56,15 @@ type descriptorTest struct {
|
|||||||
decErr error
|
decErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rewardAndCommitType = blob.TypeFromFlags(
|
||||||
|
blob.FlagReward, blob.FlagCommitOutputs,
|
||||||
|
)
|
||||||
|
|
||||||
var descriptorTests = []descriptorTest{
|
var descriptorTests = []descriptorTest{
|
||||||
{
|
{
|
||||||
name: "to-local only",
|
name: "to-local only",
|
||||||
encVersion: 0,
|
encVersion: blob.TypeDefault,
|
||||||
decVersion: 0,
|
decVersion: blob.TypeDefault,
|
||||||
sweepAddr: makeAddr(22),
|
sweepAddr: makeAddr(22),
|
||||||
revPubKey: makePubKey(0),
|
revPubKey: makePubKey(0),
|
||||||
delayPubKey: makePubKey(1),
|
delayPubKey: makePubKey(1),
|
||||||
@ -65,8 +73,8 @@ var descriptorTests = []descriptorTest{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "to-local and p2wkh",
|
name: "to-local and p2wkh",
|
||||||
encVersion: 0,
|
encVersion: rewardAndCommitType,
|
||||||
decVersion: 0,
|
decVersion: rewardAndCommitType,
|
||||||
sweepAddr: makeAddr(22),
|
sweepAddr: makeAddr(22),
|
||||||
revPubKey: makePubKey(0),
|
revPubKey: makePubKey(0),
|
||||||
delayPubKey: makePubKey(1),
|
delayPubKey: makePubKey(1),
|
||||||
@ -78,30 +86,30 @@ var descriptorTests = []descriptorTest{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unknown encrypt version",
|
name: "unknown encrypt version",
|
||||||
encVersion: 1,
|
encVersion: 0,
|
||||||
decVersion: 0,
|
decVersion: blob.TypeDefault,
|
||||||
sweepAddr: makeAddr(34),
|
sweepAddr: makeAddr(34),
|
||||||
revPubKey: makePubKey(0),
|
revPubKey: makePubKey(0),
|
||||||
delayPubKey: makePubKey(1),
|
delayPubKey: makePubKey(1),
|
||||||
csvDelay: 144,
|
csvDelay: 144,
|
||||||
commitToLocalSig: makeSig(1),
|
commitToLocalSig: makeSig(1),
|
||||||
encErr: blob.ErrUnknownBlobVersion,
|
encErr: blob.ErrUnknownBlobType,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unknown decrypt version",
|
name: "unknown decrypt version",
|
||||||
encVersion: 0,
|
encVersion: blob.TypeDefault,
|
||||||
decVersion: 1,
|
decVersion: 0,
|
||||||
sweepAddr: makeAddr(34),
|
sweepAddr: makeAddr(34),
|
||||||
revPubKey: makePubKey(0),
|
revPubKey: makePubKey(0),
|
||||||
delayPubKey: makePubKey(1),
|
delayPubKey: makePubKey(1),
|
||||||
csvDelay: 144,
|
csvDelay: 144,
|
||||||
commitToLocalSig: makeSig(1),
|
commitToLocalSig: makeSig(1),
|
||||||
decErr: blob.ErrUnknownBlobVersion,
|
decErr: blob.ErrUnknownBlobType,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "sweep addr length zero",
|
name: "sweep addr length zero",
|
||||||
encVersion: 0,
|
encVersion: blob.TypeDefault,
|
||||||
decVersion: 0,
|
decVersion: blob.TypeDefault,
|
||||||
sweepAddr: makeAddr(0),
|
sweepAddr: makeAddr(0),
|
||||||
revPubKey: makePubKey(0),
|
revPubKey: makePubKey(0),
|
||||||
delayPubKey: makePubKey(1),
|
delayPubKey: makePubKey(1),
|
||||||
@ -110,8 +118,8 @@ var descriptorTests = []descriptorTest{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "sweep addr max size",
|
name: "sweep addr max size",
|
||||||
encVersion: 0,
|
encVersion: blob.TypeDefault,
|
||||||
decVersion: 0,
|
decVersion: blob.TypeDefault,
|
||||||
sweepAddr: makeAddr(blob.MaxSweepAddrSize),
|
sweepAddr: makeAddr(blob.MaxSweepAddrSize),
|
||||||
revPubKey: makePubKey(0),
|
revPubKey: makePubKey(0),
|
||||||
delayPubKey: makePubKey(1),
|
delayPubKey: makePubKey(1),
|
||||||
@ -120,8 +128,8 @@ var descriptorTests = []descriptorTest{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "sweep addr too long",
|
name: "sweep addr too long",
|
||||||
encVersion: 0,
|
encVersion: blob.TypeDefault,
|
||||||
decVersion: 0,
|
decVersion: blob.TypeDefault,
|
||||||
sweepAddr: makeAddr(blob.MaxSweepAddrSize + 1),
|
sweepAddr: makeAddr(blob.MaxSweepAddrSize + 1),
|
||||||
revPubKey: makePubKey(0),
|
revPubKey: makePubKey(0),
|
||||||
delayPubKey: makePubKey(1),
|
delayPubKey: makePubKey(1),
|
||||||
@ -208,3 +216,195 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
|
|||||||
"want: %v, got %v", boj, boj2)
|
"want: %v, got %v", boj, boj2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestJusticeKitRemoteWitnessConstruction tests that a JusticeKit returns the
|
||||||
|
// proper to-remote witnes script and to-remote witness stack. This should be
|
||||||
|
// equivalent to p2wkh spend.
|
||||||
|
func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
|
||||||
|
// Generate the to-remote pubkey.
|
||||||
|
toRemotePrivKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate to-remote priv key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the to-remote pubkey into the format expected by our justice
|
||||||
|
// kit.
|
||||||
|
var toRemotePubKey blob.PubKey
|
||||||
|
copy(toRemotePubKey[:], toRemotePrivKey.PubKey().SerializeCompressed())
|
||||||
|
|
||||||
|
// Sign a message using the to-remote private key. The exact message
|
||||||
|
// doesn't matter as we won't be validating the signature's validity.
|
||||||
|
digest := bytes.Repeat([]byte("a"), 32)
|
||||||
|
rawToRemoteSig, err := toRemotePrivKey.Sign(digest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate to-remote signature: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the DER-encoded signature into a fixed-size sig.
|
||||||
|
commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to convert raw to-remote signature to "+
|
||||||
|
"Sig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the justice kit fields relevant to the to-remote output.
|
||||||
|
justiceKit := &blob.JusticeKit{
|
||||||
|
CommitToRemotePubKey: toRemotePubKey,
|
||||||
|
CommitToRemoteSig: commitToRemoteSig,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, compute the to-remote witness script returned by the justice
|
||||||
|
// kit.
|
||||||
|
toRemoteScript, err := justiceKit.CommitToRemoteWitnessScript()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to compute to-remote witness script: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert this is exactly the to-remote, compressed pubkey.
|
||||||
|
if !bytes.Equal(toRemoteScript, toRemotePubKey[:]) {
|
||||||
|
t.Fatalf("to-remote witness script should be equal to "+
|
||||||
|
"to-remote pubkey, want: %x, got %x",
|
||||||
|
toRemotePubKey[:], toRemoteScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, compute the to-remote witness stack, which should be a p2wkh
|
||||||
|
// witness stack consisting solely of a signature.
|
||||||
|
toRemoteWitnessStack, err := justiceKit.CommitToRemoteWitnessStack()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to compute to-remote witness stack: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the witness stack only has one element.
|
||||||
|
if len(toRemoteWitnessStack) != 1 {
|
||||||
|
t.Fatalf("to-remote witness stack should be of length 1, is %d",
|
||||||
|
len(toRemoteWitnessStack))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the expected first element, by appending a sighash all byte
|
||||||
|
// to our raw DER-encoded signature.
|
||||||
|
rawToRemoteSigWithSigHash := append(
|
||||||
|
rawToRemoteSig.Serialize(), byte(txscript.SigHashAll),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert that the expected signature matches the first element in the
|
||||||
|
// witness stack.
|
||||||
|
if !bytes.Equal(rawToRemoteSigWithSigHash, toRemoteWitnessStack[0]) {
|
||||||
|
t.Fatalf("mismatched sig in to-remote witness stack, want: %v, "+
|
||||||
|
"got: %v", rawToRemoteSigWithSigHash,
|
||||||
|
toRemoteWitnessStack[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, set the CommitToRemotePubKey to be a blank value.
|
||||||
|
justiceKit.CommitToRemotePubKey = blob.PubKey{}
|
||||||
|
|
||||||
|
// When trying to compute the witness script, this should now return
|
||||||
|
// ErrNoCommitToRemoteOutput since a valid pubkey could not be parsed
|
||||||
|
// from CommitToRemotePubKey.
|
||||||
|
_, err = justiceKit.CommitToRemoteWitnessScript()
|
||||||
|
if err != blob.ErrNoCommitToRemoteOutput {
|
||||||
|
t.Fatalf("expected ErrNoCommitToRemoteOutput, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJusticeKitToLocalWitnessConstruction tests that a JusticeKit returns the
|
||||||
|
// proper to-local witness script and to-local witness stack for spending the
|
||||||
|
// revocation path.
|
||||||
|
func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
|
||||||
|
csvDelay := uint32(144)
|
||||||
|
|
||||||
|
// Generate the revocation and delay private keys.
|
||||||
|
revPrivKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate revocation priv key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
delayPrivKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate delay priv key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the revocation and delay pubkeys into the format expected by our
|
||||||
|
// justice kit.
|
||||||
|
var revPubKey blob.PubKey
|
||||||
|
copy(revPubKey[:], revPrivKey.PubKey().SerializeCompressed())
|
||||||
|
|
||||||
|
var delayPubKey blob.PubKey
|
||||||
|
copy(delayPubKey[:], delayPrivKey.PubKey().SerializeCompressed())
|
||||||
|
|
||||||
|
// Sign a message using the revocation private key. The exact message
|
||||||
|
// doesn't matter as we won't be validating the signature's validity.
|
||||||
|
digest := bytes.Repeat([]byte("a"), 32)
|
||||||
|
rawRevSig, err := revPrivKey.Sign(digest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate revocation signature: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the DER-encoded signature into a fixed-size sig.
|
||||||
|
commitToLocalSig, err := lnwire.NewSigFromSignature(rawRevSig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to convert raw revocation signature to "+
|
||||||
|
"Sig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the justice kit with fields relevant to the to-local output.
|
||||||
|
justiceKit := &blob.JusticeKit{
|
||||||
|
CSVDelay: csvDelay,
|
||||||
|
RevocationPubKey: revPubKey,
|
||||||
|
LocalDelayPubKey: delayPubKey,
|
||||||
|
CommitToLocalSig: commitToLocalSig,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the expected to-local script, which is a function of the CSV
|
||||||
|
// delay, revocation pubkey and delay pubkey.
|
||||||
|
expToLocalScript, err := lnwallet.CommitScriptToSelf(
|
||||||
|
csvDelay, delayPrivKey.PubKey(), revPrivKey.PubKey(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate expected to-local script: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the to-local script that is returned by the justice kit.
|
||||||
|
toLocalScript, err := justiceKit.CommitToLocalWitnessScript()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to compute to-local witness script: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the expected to-local script matches the actual script.
|
||||||
|
if !bytes.Equal(expToLocalScript, toLocalScript) {
|
||||||
|
t.Fatalf("mismatched to-local witness script, want: %v, got %v",
|
||||||
|
expToLocalScript, toLocalScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, compute the to-local witness stack returned by the justice kit.
|
||||||
|
toLocalWitnessStack, err := justiceKit.CommitToLocalRevokeWitnessStack()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to compute to-local witness stack: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A valid witness that spends the revocation path should have exactly
|
||||||
|
// two elements on the stack.
|
||||||
|
if len(toLocalWitnessStack) != 2 {
|
||||||
|
t.Fatalf("to-local witness stack should be of length 2, is %d",
|
||||||
|
len(toLocalWitnessStack))
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, we'll verify that the top element is 0x01, which triggers the
|
||||||
|
// revocation path within the to-local witness script.
|
||||||
|
if !bytes.Equal(toLocalWitnessStack[1], []byte{0x01}) {
|
||||||
|
t.Fatalf("top item on witness stack should be 0x01, found: %v",
|
||||||
|
toLocalWitnessStack[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, compute the expected signature in the bottom element of the
|
||||||
|
// stack, by appending a sighash all flag to the raw DER signature.
|
||||||
|
rawRevSigWithSigHash := append(
|
||||||
|
rawRevSig.Serialize(), byte(txscript.SigHashAll),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert that the second element on the stack matches our expected
|
||||||
|
// signature under the revocation pubkey.
|
||||||
|
if !bytes.Equal(rawRevSigWithSigHash, toLocalWitnessStack[0]) {
|
||||||
|
t.Fatalf("mismatched sig in to-local witness stack, want: %v, "+
|
||||||
|
"got: %v", rawRevSigWithSigHash, toLocalWitnessStack[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
134
watchtower/blob/type.go
Normal file
134
watchtower/blob/type.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Flag represents a specify option that can be present in a Type.
|
||||||
|
type Flag uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FlagReward signals that the justice transaction should contain an
|
||||||
|
// additional output for itself. Signatures sent by the client should
|
||||||
|
// include the reward script negotiated during session creation. Without
|
||||||
|
// the flag, there is only one output sweeping clients funds back to
|
||||||
|
// them solely.
|
||||||
|
FlagReward Flag = 1 << iota
|
||||||
|
|
||||||
|
// FlagCommitOutputs signals that the blob contains the information
|
||||||
|
// required to sweep commitment outputs.
|
||||||
|
FlagCommitOutputs
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type returns a Type consisting solely of this flag enabled.
|
||||||
|
func (f Flag) Type() Type {
|
||||||
|
return Type(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the name of the flag.
|
||||||
|
func (f Flag) String() string {
|
||||||
|
switch f {
|
||||||
|
case FlagReward:
|
||||||
|
return "FlagReward"
|
||||||
|
case FlagCommitOutputs:
|
||||||
|
return "FlagCommitOutputs"
|
||||||
|
default:
|
||||||
|
return "FlagUnknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type is a bit vector composed of Flags that govern various aspects of
|
||||||
|
// reconstructing the justice transaction from an encrypted blob. The flags can
|
||||||
|
// be used to signal behaviors such as which inputs are being swept, which
|
||||||
|
// outputs should be added to the justice transaction, or modify serialization
|
||||||
|
// of the blob itself.
|
||||||
|
type Type uint16
|
||||||
|
|
||||||
|
// TypeDefault sweeps only commitment outputs to a sweep address controlled by
|
||||||
|
// the user, and does not give the tower a reward.
|
||||||
|
const TypeDefault = Type(FlagCommitOutputs)
|
||||||
|
|
||||||
|
// Has returns true if the Type has the passed flag enabled.
|
||||||
|
func (t Type) Has(flag Flag) bool {
|
||||||
|
return Flag(t)&flag == flag
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeFromFlags creates a single Type from an arbitrary list of flags.
|
||||||
|
func TypeFromFlags(flags ...Flag) Type {
|
||||||
|
var typ Type
|
||||||
|
for _, flag := range flags {
|
||||||
|
typ |= Type(flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return typ
|
||||||
|
}
|
||||||
|
|
||||||
|
// knownFlags maps the supported flags to their name.
|
||||||
|
var knownFlags = map[Flag]struct{}{
|
||||||
|
FlagReward: {},
|
||||||
|
FlagCommitOutputs: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a human readable description of a Type.
|
||||||
|
func (t Type) String() string {
|
||||||
|
var (
|
||||||
|
hrPieces []string
|
||||||
|
hasUnknownFlags bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// Iterate through the possible flags from highest to lowest. This will
|
||||||
|
// ensure that the human readable names will be in the same order as the
|
||||||
|
// bits (left to right) if the type were to be printed in big-endian
|
||||||
|
// byte order.
|
||||||
|
for f := Flag(1 << 15); f != 0; f >>= 1 {
|
||||||
|
// If this flag is known, we'll add a human-readable name or its
|
||||||
|
// inverse depending on whether the type has this flag set.
|
||||||
|
if _, ok := knownFlags[f]; ok {
|
||||||
|
if t.Has(f) {
|
||||||
|
hrPieces = append(hrPieces, f.String())
|
||||||
|
} else {
|
||||||
|
hrPieces = append(hrPieces, "No-"+f.String())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Make note of any unknown flags that this type has
|
||||||
|
// set. If any are present, we'll prepend the bit-wise
|
||||||
|
// representation of the type in the final string.
|
||||||
|
if t.Has(f) {
|
||||||
|
hasUnknownFlags = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there were no unknown flags, we'll simply return the list of human
|
||||||
|
// readable pieces.
|
||||||
|
if !hasUnknownFlags {
|
||||||
|
return fmt.Sprintf("[%s]", strings.Join(hrPieces, "|"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we'll prepend the bit-wise representation to the human
|
||||||
|
// readable names.
|
||||||
|
return fmt.Sprintf("%016b[%s]", t, strings.Join(hrPieces, "|"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// supportedTypes is the set of all configurations known to be supported by the
|
||||||
|
// package.
|
||||||
|
var supportedTypes = map[Type]struct{}{
|
||||||
|
FlagCommitOutputs.Type(): {},
|
||||||
|
(FlagCommitOutputs | FlagReward).Type(): {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSupportedType returns true if the given type is supported by the package.
|
||||||
|
func IsSupportedType(blobType Type) bool {
|
||||||
|
_, ok := supportedTypes[blobType]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedTypes returns a list of all supported blob types.
|
||||||
|
func SupportedTypes() []Type {
|
||||||
|
supported := make([]Type, 0, len(supportedTypes))
|
||||||
|
for t := range supportedTypes {
|
||||||
|
supported = append(supported, t)
|
||||||
|
}
|
||||||
|
return supported
|
||||||
|
}
|
135
watchtower/blob/type_test.go
Normal file
135
watchtower/blob/type_test.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package blob_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||||
|
)
|
||||||
|
|
||||||
|
var unknownFlag = blob.Flag(16)
|
||||||
|
|
||||||
|
type typeStringTest struct {
|
||||||
|
name string
|
||||||
|
typ blob.Type
|
||||||
|
expStr string
|
||||||
|
}
|
||||||
|
|
||||||
|
var typeStringTests = []typeStringTest{
|
||||||
|
{
|
||||||
|
name: "commit no-reward",
|
||||||
|
typ: blob.TypeDefault,
|
||||||
|
expStr: "[FlagCommitOutputs|No-FlagReward]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "commit reward",
|
||||||
|
typ: (blob.FlagCommitOutputs | blob.FlagReward).Type(),
|
||||||
|
expStr: "[FlagCommitOutputs|FlagReward]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown flag",
|
||||||
|
typ: unknownFlag.Type(),
|
||||||
|
expStr: "0000000000010000[No-FlagCommitOutputs|No-FlagReward]",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTypeStrings asserts that the proper human-readable string is returned for
|
||||||
|
// various blob.Types
|
||||||
|
func TestTypeStrings(t *testing.T) {
|
||||||
|
for _, test := range typeStringTests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
typeStr := test.typ.String()
|
||||||
|
if typeStr != test.expStr {
|
||||||
|
t.Fatalf("mismatched type string, want: %v, "+
|
||||||
|
"got %v", test.expStr, typeStr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnknownFlagString asserts that the proper string is returned from
|
||||||
|
// unallocated flags.
|
||||||
|
func TestUnknownFlagString(t *testing.T) {
|
||||||
|
if unknownFlag.String() != "FlagUnknown" {
|
||||||
|
t.Fatalf("unknown flags should return FlagUnknown, instead "+
|
||||||
|
"got: %v", unknownFlag.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type typeFromFlagTest struct {
|
||||||
|
name string
|
||||||
|
flags []blob.Flag
|
||||||
|
expType blob.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
var typeFromFlagTests = []typeFromFlagTest{
|
||||||
|
{
|
||||||
|
name: "no flags",
|
||||||
|
flags: nil,
|
||||||
|
expType: blob.Type(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single flag",
|
||||||
|
flags: []blob.Flag{blob.FlagReward},
|
||||||
|
expType: blob.Type(blob.FlagReward),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple flags",
|
||||||
|
flags: []blob.Flag{blob.FlagReward, blob.FlagCommitOutputs},
|
||||||
|
expType: blob.Type(blob.FlagReward | blob.FlagCommitOutputs),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate flag",
|
||||||
|
flags: []blob.Flag{blob.FlagReward, blob.FlagReward},
|
||||||
|
expType: blob.Type(blob.FlagReward),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTypeFromFlags asserts that blob.Types constructed using
|
||||||
|
// blob.TypeFromFlags are correct, and properly deduplicate flags. We also
|
||||||
|
// assert that Has returns true for the generated blob.Type for all of the flags
|
||||||
|
// that were used to create it.
|
||||||
|
func TestTypeFromFlags(t *testing.T) {
|
||||||
|
for _, test := range typeFromFlagTests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
blobType := blob.TypeFromFlags(test.flags...)
|
||||||
|
|
||||||
|
// Assert that the constructed type matches our
|
||||||
|
// expectation.
|
||||||
|
if blobType != test.expType {
|
||||||
|
t.Fatalf("mismatch, expected blob type %s, "+
|
||||||
|
"got %s", test.expType, blobType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that Has returns true for all flags used to
|
||||||
|
// construct the type.
|
||||||
|
for _, flag := range test.flags {
|
||||||
|
if blobType.Has(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("expected type to have flag %s, "+
|
||||||
|
"but didn't", flag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSupportedTypes verifies that blob.IsSupported returns true for all
|
||||||
|
// blob.Types returned from blob.SupportedTypes. It also asserts that the
|
||||||
|
// blob.DefaultType returns true.
|
||||||
|
func TestSupportedTypes(t *testing.T) {
|
||||||
|
// Assert that the package's default type is supported.
|
||||||
|
if !blob.IsSupportedType(blob.TypeDefault) {
|
||||||
|
t.Fatalf("default type %s is not supported", blob.TypeDefault)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that all claimed supported types are actually supported.
|
||||||
|
for _, supType := range blob.SupportedTypes() {
|
||||||
|
if blob.IsSupportedType(supType) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("supposedly supported type %s is not supported",
|
||||||
|
supType)
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/watchtower/blob"
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/lookout"
|
"github.com/lightningnetwork/lnd/watchtower/lookout"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||||
|
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
|
||||||
)
|
)
|
||||||
|
|
||||||
const csvDelay uint32 = 144
|
const csvDelay uint32 = 144
|
||||||
@ -170,8 +171,10 @@ func TestJusticeDescriptor(t *testing.T) {
|
|||||||
// parameters that should be used in constructing the justice
|
// parameters that should be used in constructing the justice
|
||||||
// transaction.
|
// transaction.
|
||||||
sessionInfo := &wtdb.SessionInfo{
|
sessionInfo := &wtdb.SessionInfo{
|
||||||
|
Policy: wtpolicy.Policy{
|
||||||
SweepFeeRate: 2000,
|
SweepFeeRate: 2000,
|
||||||
RewardRate: 900000,
|
RewardRate: 900000,
|
||||||
|
},
|
||||||
RewardAddress: makeAddrSlice(22),
|
RewardAddress: makeAddrSlice(22),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ func (l *Lookout) processEpoch(epoch *chainntnfs.BlockEpoch,
|
|||||||
// sweep the breached commitment outputs.
|
// sweep the breached commitment outputs.
|
||||||
justiceKit, err := blob.Decrypt(
|
justiceKit, err := blob.Decrypt(
|
||||||
commitTxID[:], match.EncryptedBlob,
|
commitTxID[:], match.EncryptedBlob,
|
||||||
match.SessionInfo.Version,
|
match.SessionInfo.Policy.BlobType,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If the decryption fails, this implies either that the
|
// If the decryption fails, this implies either that the
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/watchtower/blob"
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/lookout"
|
"github.com/lightningnetwork/lnd/watchtower/lookout"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||||
|
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockPunisher struct {
|
type mockPunisher struct {
|
||||||
@ -86,15 +87,25 @@ func TestLookoutBreachMatching(t *testing.T) {
|
|||||||
t.Fatalf("unable to start watcher: %v", err)
|
t.Fatalf("unable to start watcher: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rewardAndCommitType := blob.TypeFromFlags(
|
||||||
|
blob.FlagReward, blob.FlagCommitOutputs,
|
||||||
|
)
|
||||||
|
|
||||||
// Create two sessions, representing two distinct clients.
|
// Create two sessions, representing two distinct clients.
|
||||||
sessionInfo1 := &wtdb.SessionInfo{
|
sessionInfo1 := &wtdb.SessionInfo{
|
||||||
ID: makeArray33(1),
|
ID: makeArray33(1),
|
||||||
|
Policy: wtpolicy.Policy{
|
||||||
|
BlobType: rewardAndCommitType,
|
||||||
MaxUpdates: 10,
|
MaxUpdates: 10,
|
||||||
|
},
|
||||||
RewardAddress: makeAddrSlice(22),
|
RewardAddress: makeAddrSlice(22),
|
||||||
}
|
}
|
||||||
sessionInfo2 := &wtdb.SessionInfo{
|
sessionInfo2 := &wtdb.SessionInfo{
|
||||||
ID: makeArray33(2),
|
ID: makeArray33(2),
|
||||||
|
Policy: wtpolicy.Policy{
|
||||||
|
BlobType: rewardAndCommitType,
|
||||||
MaxUpdates: 10,
|
MaxUpdates: 10,
|
||||||
|
},
|
||||||
RewardAddress: makeAddrSlice(22),
|
RewardAddress: makeAddrSlice(22),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,13 +148,13 @@ func TestLookoutBreachMatching(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the first justice kit under the txid of the first txn.
|
// Encrypt the first justice kit under the txid of the first txn.
|
||||||
encBlob1, err := blob1.Encrypt(hash1[:], 0)
|
encBlob1, err := blob1.Encrypt(hash1[:], blob.FlagCommitOutputs.Type())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to encrypt sweep detail 1: %v", err)
|
t.Fatalf("unable to encrypt sweep detail 1: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the second justice kit under the txid of the second txn.
|
// Encrypt the second justice kit under the txid of the second txn.
|
||||||
encBlob2, err := blob2.Encrypt(hash2[:], 0)
|
encBlob2, err := blob2.Encrypt(hash2[:], blob.FlagCommitOutputs.Type())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to encrypt sweep detail 2: %v", err)
|
t.Fatalf("unable to encrypt sweep detail 2: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -49,12 +49,8 @@ type SessionInfo struct {
|
|||||||
// ID is the remote public key of the watchtower client.
|
// ID is the remote public key of the watchtower client.
|
||||||
ID SessionID
|
ID SessionID
|
||||||
|
|
||||||
// Version specifies the plaintext blob encoding of all state updates.
|
// Policy holds the negotiated session parameters.
|
||||||
Version uint16
|
Policy wtpolicy.Policy
|
||||||
|
|
||||||
// MaxUpdates is the total number of updates the client can send for
|
|
||||||
// this session.
|
|
||||||
MaxUpdates uint16
|
|
||||||
|
|
||||||
// LastApplied the sequence number of the last successful state update.
|
// LastApplied the sequence number of the last successful state update.
|
||||||
LastApplied uint16
|
LastApplied uint16
|
||||||
@ -62,14 +58,6 @@ type SessionInfo struct {
|
|||||||
// ClientLastApplied the last last-applied the client has echoed back.
|
// ClientLastApplied the last last-applied the client has echoed back.
|
||||||
ClientLastApplied uint16
|
ClientLastApplied uint16
|
||||||
|
|
||||||
// RewardRate the fraction of the swept amount that goes to the tower,
|
|
||||||
// expressed in millionths of the swept balance.
|
|
||||||
RewardRate uint32
|
|
||||||
|
|
||||||
// SweepFeeRate is the agreed upon fee rate used to sign any sweep
|
|
||||||
// transactions.
|
|
||||||
SweepFeeRate lnwallet.SatPerKWeight
|
|
||||||
|
|
||||||
// RewardAddress the address that the tower's reward will be deposited
|
// RewardAddress the address that the tower's reward will be deposited
|
||||||
// to if a sweep transaction confirms.
|
// to if a sweep transaction confirms.
|
||||||
RewardAddress []byte
|
RewardAddress []byte
|
||||||
@ -96,7 +84,7 @@ func (s *SessionInfo) AcceptUpdateSequence(seqNum, lastApplied uint16) error {
|
|||||||
return ErrLastAppliedReversion
|
return ErrLastAppliedReversion
|
||||||
|
|
||||||
// Client update exceeds capacity of session.
|
// Client update exceeds capacity of session.
|
||||||
case seqNum > s.MaxUpdates:
|
case seqNum > s.Policy.MaxUpdates:
|
||||||
return ErrSessionConsumed
|
return ErrSessionConsumed
|
||||||
|
|
||||||
// Client update does not match our expected next seqnum.
|
// Client update does not match our expected next seqnum.
|
||||||
@ -117,7 +105,7 @@ func (s *SessionInfo) AcceptUpdateSequence(seqNum, lastApplied uint16) error {
|
|||||||
func (s *SessionInfo) ComputeSweepOutputs(totalAmt btcutil.Amount,
|
func (s *SessionInfo) ComputeSweepOutputs(totalAmt btcutil.Amount,
|
||||||
txVSize int64) (btcutil.Amount, btcutil.Amount, error) {
|
txVSize int64) (btcutil.Amount, btcutil.Amount, error) {
|
||||||
|
|
||||||
txFee := s.SweepFeeRate.FeeForWeight(txVSize)
|
txFee := s.Policy.SweepFeeRate.FeeForWeight(txVSize)
|
||||||
if txFee > totalAmt {
|
if txFee > totalAmt {
|
||||||
return 0, 0, ErrFeeExceedsInputs
|
return 0, 0, ErrFeeExceedsInputs
|
||||||
}
|
}
|
||||||
@ -126,7 +114,8 @@ func (s *SessionInfo) ComputeSweepOutputs(totalAmt btcutil.Amount,
|
|||||||
|
|
||||||
// Apply the reward rate to the remaining total, specified in millionths
|
// Apply the reward rate to the remaining total, specified in millionths
|
||||||
// of the available balance.
|
// of the available balance.
|
||||||
rewardAmt := (totalAmt*btcutil.Amount(s.RewardRate) + 999999) / 1000000
|
rewardRate := btcutil.Amount(s.Policy.RewardRate)
|
||||||
|
rewardAmt := (totalAmt*rewardRate + 999999) / 1000000
|
||||||
sweepAmt := totalAmt - rewardAmt
|
sweepAmt := totalAmt - rewardAmt
|
||||||
|
|
||||||
// TODO(conner): check dustiness
|
// TODO(conner): check dustiness
|
||||||
|
68
watchtower/wtpolicy/policy.go
Normal file
68
watchtower/wtpolicy/policy.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package wtpolicy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultMaxUpdates specifies the number of encrypted blobs a client
|
||||||
|
// can send to the tower in a single session.
|
||||||
|
DefaultMaxUpdates = 1024
|
||||||
|
|
||||||
|
// DefaultRewardRate specifies the fraction of the channel that the
|
||||||
|
// tower takes if it successfully sweeps a breach. The value is
|
||||||
|
// expressed in millionths of the channel capacity.
|
||||||
|
DefaultRewardRate = 10000
|
||||||
|
|
||||||
|
// DefaultSweepFeeRate specifies the fee rate used to construct justice
|
||||||
|
// transactions. The value is expressed in satoshis per kilo-weight.
|
||||||
|
DefaultSweepFeeRate = 3000
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultPolicy returns a Policy containing the default parameters that can be
|
||||||
|
// used by clients or servers.
|
||||||
|
func DefaultPolicy() Policy {
|
||||||
|
return Policy{
|
||||||
|
BlobType: blob.TypeDefault,
|
||||||
|
MaxUpdates: DefaultMaxUpdates,
|
||||||
|
RewardRate: DefaultRewardRate,
|
||||||
|
SweepFeeRate: lnwallet.SatPerKWeight(
|
||||||
|
DefaultSweepFeeRate,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Policy defines the negotiated parameters for a session between a client and
|
||||||
|
// server. The parameters specify the format of encrypted blobs sent to the
|
||||||
|
// tower, the reward schedule for the tower, and the number of encrypted blobs a
|
||||||
|
// client can send in one session.
|
||||||
|
type Policy struct {
|
||||||
|
// BlobType specifies the blob format that must be used by all updates sent
|
||||||
|
// under the session key used to negotiate this session.
|
||||||
|
BlobType blob.Type
|
||||||
|
|
||||||
|
// MaxUpdates is the maximum number of updates the watchtower will honor
|
||||||
|
// for this session.
|
||||||
|
MaxUpdates uint16
|
||||||
|
|
||||||
|
// RewardRate is the fraction of the total balance of the revoked
|
||||||
|
// commitment that the watchtower is entitled to. This value is
|
||||||
|
// expressed in millionths of the total balance.
|
||||||
|
RewardRate uint32
|
||||||
|
|
||||||
|
// SweepFeeRate expresses the intended fee rate to be used when
|
||||||
|
// constructing the justice transaction. All sweep transactions created
|
||||||
|
// for this session must use this value during construction, and the
|
||||||
|
// signatures must implicitly commit to the resulting output values.
|
||||||
|
SweepFeeRate lnwallet.SatPerKWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a human-readable description of the current policy.
|
||||||
|
func (p Policy) String() string {
|
||||||
|
return fmt.Sprintf("(blob-type=%b max-updates=%d reward-rate=%d "+
|
||||||
|
"sweep-fee-rate=%d)", p.BlobType, p.MaxUpdates, p.RewardRate,
|
||||||
|
p.SweepFeeRate)
|
||||||
|
}
|
@ -13,7 +13,9 @@ import (
|
|||||||
"github.com/btcsuite/btcd/connmgr"
|
"github.com/btcsuite/btcd/connmgr"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||||
|
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/wtwire"
|
"github.com/lightningnetwork/lnd/watchtower/wtwire"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -246,14 +248,14 @@ func (s *Server) handleClient(peer Peer) {
|
|||||||
|
|
||||||
log.Infof("Received CreateSession from %s, "+
|
log.Infof("Received CreateSession from %s, "+
|
||||||
"version=%d nupdates=%d rewardrate=%d "+
|
"version=%d nupdates=%d rewardrate=%d "+
|
||||||
"sweepfeerate=%d", id, msg.BlobVersion,
|
"sweepfeerate=%d", id, msg.BlobType,
|
||||||
msg.MaxUpdates, msg.RewardRate,
|
msg.MaxUpdates, msg.RewardRate,
|
||||||
msg.SweepFeeRate)
|
msg.SweepFeeRate)
|
||||||
|
|
||||||
// Attempt to open a new session for this client.
|
// Attempt to open a new session for this client.
|
||||||
err := s.handleCreateSession(peer, &id, msg)
|
err := s.handleCreateSession(peer, &id, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("unable to handle CreateSession "+
|
log.Errorf("Unable to handle CreateSession "+
|
||||||
"from %s: %v", id, err)
|
"from %s: %v", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,7 +329,7 @@ func (s *Server) handleInit(localInit, remoteInit *wtwire.Init) error {
|
|||||||
// session info is known about the session id. If an existing session is found,
|
// session info is known about the session id. If an existing session is found,
|
||||||
// the reward address is returned in case the client lost our reply.
|
// the reward address is returned in case the client lost our reply.
|
||||||
func (s *Server) handleCreateSession(peer Peer, id *wtdb.SessionID,
|
func (s *Server) handleCreateSession(peer Peer, id *wtdb.SessionID,
|
||||||
init *wtwire.CreateSession) error {
|
req *wtwire.CreateSession) error {
|
||||||
|
|
||||||
// TODO(conner): validate accept against policy
|
// TODO(conner): validate accept against policy
|
||||||
|
|
||||||
@ -369,17 +371,28 @@ func (s *Server) handleCreateSession(peer Peer, id *wtdb.SessionID,
|
|||||||
|
|
||||||
rewardAddrBytes := rewardAddress.ScriptAddress()
|
rewardAddrBytes := rewardAddress.ScriptAddress()
|
||||||
|
|
||||||
|
// Ensure that the requested blob type is supported by our tower.
|
||||||
|
if !blob.IsSupportedType(req.BlobType) {
|
||||||
|
log.Debugf("Rejecting CreateSession from %s, unsupported blob "+
|
||||||
|
"type %s", id, req.BlobType)
|
||||||
|
return s.replyCreateSession(
|
||||||
|
peer, id, wtwire.CreateSessionCodeRejectBlobType, nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(conner): create invoice for upfront payment
|
// TODO(conner): create invoice for upfront payment
|
||||||
|
|
||||||
// Assemble the session info using the agreed upon parameters, reward
|
// Assemble the session info using the agreed upon parameters, reward
|
||||||
// address, and session id.
|
// address, and session id.
|
||||||
info := wtdb.SessionInfo{
|
info := wtdb.SessionInfo{
|
||||||
ID: *id,
|
ID: *id,
|
||||||
Version: init.BlobVersion,
|
|
||||||
MaxUpdates: init.MaxUpdates,
|
|
||||||
RewardRate: init.RewardRate,
|
|
||||||
SweepFeeRate: init.SweepFeeRate,
|
|
||||||
RewardAddress: rewardAddrBytes,
|
RewardAddress: rewardAddrBytes,
|
||||||
|
Policy: wtpolicy.Policy{
|
||||||
|
BlobType: req.BlobType,
|
||||||
|
MaxUpdates: req.MaxUpdates,
|
||||||
|
RewardRate: req.RewardRate,
|
||||||
|
SweepFeeRate: req.SweepFeeRate,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the session info into the watchtower's database. If
|
// Insert the session info into the watchtower's database. If
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/wtserver"
|
"github.com/lightningnetwork/lnd/watchtower/wtserver"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/wtwire"
|
"github.com/lightningnetwork/lnd/watchtower/wtwire"
|
||||||
@ -155,7 +156,7 @@ var createSessionTests = []createSessionTestCase{
|
|||||||
lnwire.NewRawFeatureVector(),
|
lnwire.NewRawFeatureVector(),
|
||||||
),
|
),
|
||||||
createMsg: &wtwire.CreateSession{
|
createMsg: &wtwire.CreateSession{
|
||||||
BlobVersion: 0,
|
BlobType: blob.TypeDefault,
|
||||||
MaxUpdates: 1000,
|
MaxUpdates: 1000,
|
||||||
RewardRate: 0,
|
RewardRate: 0,
|
||||||
SweepFeeRate: 1,
|
SweepFeeRate: 1,
|
||||||
@ -169,6 +170,23 @@ var createSessionTests = []createSessionTestCase{
|
|||||||
Data: []byte(addr.ScriptAddress()),
|
Data: []byte(addr.ScriptAddress()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "reject unsupported blob type",
|
||||||
|
initMsg: wtwire.NewInitMessage(
|
||||||
|
lnwire.NewRawFeatureVector(),
|
||||||
|
lnwire.NewRawFeatureVector(),
|
||||||
|
),
|
||||||
|
createMsg: &wtwire.CreateSession{
|
||||||
|
BlobType: 0,
|
||||||
|
MaxUpdates: 1000,
|
||||||
|
RewardRate: 0,
|
||||||
|
SweepFeeRate: 1,
|
||||||
|
},
|
||||||
|
expReply: &wtwire.CreateSessionReply{
|
||||||
|
Code: wtwire.CreateSessionCodeRejectBlobType,
|
||||||
|
Data: []byte{},
|
||||||
|
},
|
||||||
|
},
|
||||||
// TODO(conner): add policy rejection tests
|
// TODO(conner): add policy rejection tests
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,7 +276,7 @@ var stateUpdateTests = []stateUpdateTestCase{
|
|||||||
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
||||||
}},
|
}},
|
||||||
createMsg: &wtwire.CreateSession{
|
createMsg: &wtwire.CreateSession{
|
||||||
BlobVersion: 0,
|
BlobType: blob.TypeDefault,
|
||||||
MaxUpdates: 3,
|
MaxUpdates: 3,
|
||||||
RewardRate: 0,
|
RewardRate: 0,
|
||||||
SweepFeeRate: 1,
|
SweepFeeRate: 1,
|
||||||
@ -287,7 +305,7 @@ var stateUpdateTests = []stateUpdateTestCase{
|
|||||||
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
||||||
}},
|
}},
|
||||||
createMsg: &wtwire.CreateSession{
|
createMsg: &wtwire.CreateSession{
|
||||||
BlobVersion: 0,
|
BlobType: blob.TypeDefault,
|
||||||
MaxUpdates: 4,
|
MaxUpdates: 4,
|
||||||
RewardRate: 0,
|
RewardRate: 0,
|
||||||
SweepFeeRate: 1,
|
SweepFeeRate: 1,
|
||||||
@ -310,7 +328,7 @@ var stateUpdateTests = []stateUpdateTestCase{
|
|||||||
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
||||||
}},
|
}},
|
||||||
createMsg: &wtwire.CreateSession{
|
createMsg: &wtwire.CreateSession{
|
||||||
BlobVersion: 0,
|
BlobType: blob.TypeDefault,
|
||||||
MaxUpdates: 4,
|
MaxUpdates: 4,
|
||||||
RewardRate: 0,
|
RewardRate: 0,
|
||||||
SweepFeeRate: 1,
|
SweepFeeRate: 1,
|
||||||
@ -337,7 +355,7 @@ var stateUpdateTests = []stateUpdateTestCase{
|
|||||||
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
||||||
}},
|
}},
|
||||||
createMsg: &wtwire.CreateSession{
|
createMsg: &wtwire.CreateSession{
|
||||||
BlobVersion: 0,
|
BlobType: blob.TypeDefault,
|
||||||
MaxUpdates: 4,
|
MaxUpdates: 4,
|
||||||
RewardRate: 0,
|
RewardRate: 0,
|
||||||
SweepFeeRate: 1,
|
SweepFeeRate: 1,
|
||||||
@ -364,7 +382,7 @@ var stateUpdateTests = []stateUpdateTestCase{
|
|||||||
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
||||||
}},
|
}},
|
||||||
createMsg: &wtwire.CreateSession{
|
createMsg: &wtwire.CreateSession{
|
||||||
BlobVersion: 0,
|
BlobType: blob.TypeDefault,
|
||||||
MaxUpdates: 4,
|
MaxUpdates: 4,
|
||||||
RewardRate: 0,
|
RewardRate: 0,
|
||||||
SweepFeeRate: 1,
|
SweepFeeRate: 1,
|
||||||
@ -393,7 +411,7 @@ var stateUpdateTests = []stateUpdateTestCase{
|
|||||||
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
||||||
}},
|
}},
|
||||||
createMsg: &wtwire.CreateSession{
|
createMsg: &wtwire.CreateSession{
|
||||||
BlobVersion: 0,
|
BlobType: blob.TypeDefault,
|
||||||
MaxUpdates: 4,
|
MaxUpdates: 4,
|
||||||
RewardRate: 0,
|
RewardRate: 0,
|
||||||
SweepFeeRate: 1,
|
SweepFeeRate: 1,
|
||||||
@ -421,7 +439,7 @@ var stateUpdateTests = []stateUpdateTestCase{
|
|||||||
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
||||||
}},
|
}},
|
||||||
createMsg: &wtwire.CreateSession{
|
createMsg: &wtwire.CreateSession{
|
||||||
BlobVersion: 0,
|
BlobType: blob.TypeDefault,
|
||||||
MaxUpdates: 3,
|
MaxUpdates: 3,
|
||||||
RewardRate: 0,
|
RewardRate: 0,
|
||||||
SweepFeeRate: 1,
|
SweepFeeRate: 1,
|
||||||
@ -450,7 +468,7 @@ var stateUpdateTests = []stateUpdateTestCase{
|
|||||||
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
GlobalFeatures: lnwire.NewRawFeatureVector(),
|
||||||
}},
|
}},
|
||||||
createMsg: &wtwire.CreateSession{
|
createMsg: &wtwire.CreateSession{
|
||||||
BlobVersion: 0,
|
BlobType: blob.TypeDefault,
|
||||||
MaxUpdates: 3,
|
MaxUpdates: 3,
|
||||||
RewardRate: 0,
|
RewardRate: 0,
|
||||||
SweepFeeRate: 1,
|
SweepFeeRate: 1,
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateSession is sent from a client to tower when to negotiate a session, which
|
// CreateSession is sent from a client to tower when to negotiate a session, which
|
||||||
@ -11,9 +12,9 @@ import (
|
|||||||
// An update is consumed by uploading an encrypted blob that contains
|
// An update is consumed by uploading an encrypted blob that contains
|
||||||
// information required to sweep a revoked commitment transaction.
|
// information required to sweep a revoked commitment transaction.
|
||||||
type CreateSession struct {
|
type CreateSession struct {
|
||||||
// BlobVersion specifies the blob format that must be used by all
|
// BlobType specifies the blob format that must be used by all updates sent
|
||||||
// updates sent under the session key used to negotiate this session.
|
// under the session key used to negotiate this session.
|
||||||
BlobVersion uint16
|
BlobType blob.Type
|
||||||
|
|
||||||
// MaxUpdates is the maximum number of updates the watchtower will honor
|
// MaxUpdates is the maximum number of updates the watchtower will honor
|
||||||
// for this session.
|
// for this session.
|
||||||
@ -41,7 +42,7 @@ var _ Message = (*CreateSession)(nil)
|
|||||||
// This is part of the wtwire.Message interface.
|
// This is part of the wtwire.Message interface.
|
||||||
func (m *CreateSession) Decode(r io.Reader, pver uint32) error {
|
func (m *CreateSession) Decode(r io.Reader, pver uint32) error {
|
||||||
return ReadElements(r,
|
return ReadElements(r,
|
||||||
&m.BlobVersion,
|
&m.BlobType,
|
||||||
&m.MaxUpdates,
|
&m.MaxUpdates,
|
||||||
&m.RewardRate,
|
&m.RewardRate,
|
||||||
&m.SweepFeeRate,
|
&m.SweepFeeRate,
|
||||||
@ -54,7 +55,7 @@ func (m *CreateSession) Decode(r io.Reader, pver uint32) error {
|
|||||||
// This is part of the wtwire.Message interface.
|
// This is part of the wtwire.Message interface.
|
||||||
func (m *CreateSession) Encode(w io.Writer, pver uint32) error {
|
func (m *CreateSession) Encode(w io.Writer, pver uint32) error {
|
||||||
return WriteElements(w,
|
return WriteElements(w,
|
||||||
m.BlobVersion,
|
m.BlobType,
|
||||||
m.MaxUpdates,
|
m.MaxUpdates,
|
||||||
m.RewardRate,
|
m.RewardRate,
|
||||||
m.SweepFeeRate,
|
m.SweepFeeRate,
|
||||||
|
@ -25,6 +25,10 @@ const (
|
|||||||
// CreateSessionCodeRejectSweepFeeRate the tower rejected the sweep fee
|
// CreateSessionCodeRejectSweepFeeRate the tower rejected the sweep fee
|
||||||
// rate proposed by the client.
|
// rate proposed by the client.
|
||||||
CreateSessionCodeRejectSweepFeeRate CreateSessionCode = 63
|
CreateSessionCodeRejectSweepFeeRate CreateSessionCode = 63
|
||||||
|
|
||||||
|
// CreateSessionCodeRejectBlobType is returned when the tower does not
|
||||||
|
// support the proposed blob type.
|
||||||
|
CreateSessionCodeRejectBlobType CreateSessionCode = 64
|
||||||
)
|
)
|
||||||
|
|
||||||
// MaxCreateSessionReplyDataLength is the maximum size of the Data payload
|
// MaxCreateSessionReplyDataLength is the maximum size of the Data payload
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteElement is a one-stop shop to write the big endian representation of
|
// WriteElement is a one-stop shop to write the big endian representation of
|
||||||
@ -30,6 +31,13 @@ func WriteElement(w io.Writer, element interface{}) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case blob.Type:
|
||||||
|
var b [2]byte
|
||||||
|
binary.BigEndian.PutUint16(b[:], uint16(e))
|
||||||
|
if _, err := w.Write(b[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
case uint32:
|
case uint32:
|
||||||
var b [4]byte
|
var b [4]byte
|
||||||
binary.BigEndian.PutUint32(b[:], e)
|
binary.BigEndian.PutUint32(b[:], e)
|
||||||
@ -127,6 +135,13 @@ func ReadElement(r io.Reader, element interface{}) error {
|
|||||||
}
|
}
|
||||||
*e = binary.BigEndian.Uint16(b[:])
|
*e = binary.BigEndian.Uint16(b[:])
|
||||||
|
|
||||||
|
case *blob.Type:
|
||||||
|
var b [2]byte
|
||||||
|
if _, err := io.ReadFull(r, b[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*e = blob.Type(binary.BigEndian.Uint16(b[:]))
|
||||||
|
|
||||||
case *uint32:
|
case *uint32:
|
||||||
var b [4]byte
|
var b [4]byte
|
||||||
if _, err := io.ReadFull(r, b[:]); err != nil {
|
if _, err := io.ReadFull(r, b[:]); err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user