From 8147151fbf59d8d9c99fd8126d0efbe5d797c91e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sun, 16 Apr 2017 15:19:39 -0700 Subject: [PATCH] lnwire: add new 32-byte persistent/pending channel ID's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit does to things: moves the prior ShortChannelID struct into a new short_channel_id.go file, and also implements the new ChannelID’s currently used within he specification. These new ID’s are 32-bytes in length and used during initial channel funding as well as during normal channel updates. During initial channel funding, the ID is to be a random 32-byte string, while once normal channel operation has began, the ID is to be (txid XOR index), where index is the index of the funding outpoint. --- lnwire/channel_id.go | 104 +++++++++++++++++++++++--------- lnwire/channel_id_test.go | 77 ++++++++++++++--------- lnwire/short_channel_id.go | 39 ++++++++++++ lnwire/short_channel_id_test.go | 39 ++++++++++++ 4 files changed, 200 insertions(+), 59 deletions(-) create mode 100644 lnwire/short_channel_id.go create mode 100644 lnwire/short_channel_id_test.go diff --git a/lnwire/channel_id.go b/lnwire/channel_id.go index 6239cda7..cfcc481e 100644 --- a/lnwire/channel_id.go +++ b/lnwire/channel_id.go @@ -1,39 +1,85 @@ package lnwire -// ShortChannelID represent the set of data which is needed to retrieve all -// necessary data to validate the channel existence. -type ShortChannelID struct { - // BlockHeight is the height of the block where funding transaction - // located. - // - // NOTE: This field is limited to 3 bytes. - BlockHeight uint32 +import ( + "encoding/binary" + "encoding/hex" + "math" - // TxIndex is a position of funding transaction within a block. - // - // NOTE: This field is limited to 3 bytes. - TxIndex uint32 + "github.com/roasbeef/btcd/chaincfg/chainhash" + "github.com/roasbeef/btcd/wire" +) - // TxPosition indicating transaction output which pays to the channel. - TxPosition uint16 +const ( + // MaxFundingTxOutputs is the maximum number of allowed outputs on a + // funding transaction within the protocol. This is due to the fact + // that we use 2-bytes to encode the index within the funding output + // during the funding workflow. Funding transaction with more outputs + // than this are considered invalid within the protocol. + MaxFundingTxOutputs = math.MaxUint16 +) + +// ChannelID is a series of 32-bytes that uniquely identifies all channels +// within the network. The ChannelID is computed using the outpoint of the +// funding transaction (the txid, and output index). Given a funding output the +// ChannelID can be calculated by XOR'ing the big-endian serialization of the +type ChannelID [32]byte + +// String returns the string representation of the ChannelID. This is just the +// hex string encoding of the ChannelID itself. +func (c ChannelID) String() string { + return hex.EncodeToString(c[:]) } -// NewShortChanIDFromInt returns a new ShortChannelID which is the decoded -// version of the compact channel ID encoded within the uint64. The format of -// the compact channel ID is as follows: 3 bytes for the block height, 3 bytes -// for the transaction index, and 2 bytes for the output index. -func NewShortChanIDFromInt(chanID uint64) ShortChannelID { - return ShortChannelID{ - BlockHeight: uint32(chanID >> 40), - TxIndex: uint32(chanID>>16) & 0xFFFFFF, - TxPosition: uint16(chanID), +// NewChanIDFromOutPoint converts a target OutPoint into a ChannelID that is +// usable within the network. In order to covert the OutPoint into a ChannelID, +// we XOR the lower 2-bytes of the txid within the OutPoint with the big-endian +// serialization of the Index of the OutPoint, truncated to 2-bytes. +func NewChanIDFromOutPoint(op *wire.OutPoint) ChannelID { + // First we'll copy the txid of the outpoint into our channel ID slice. + var cid ChannelID + copy(cid[:], op.Hash[:]) + + // With the txid copied over, we'll now XOR the lower 2-bytes of the + // partial channelID with big-endian serialization of output index. + xorTxid(cid, uint16(op.Index)) + + return cid +} + +// xorTxid performs the transformation needed to transform an OutPoint into a +// ChannelID. To do this, we expect the cid parameter to contain the txid +// unaltered and the outputIndex to be the output index +func xorTxid(cid ChannelID, outputIndex uint16) { + var buf [32]byte + binary.BigEndian.PutUint16(buf[:30], outputIndex) + + buf[30] = cid[30] ^ buf[30] + buf[31] = cid[31] ^ buf[31] +} + +// GenPossibleOutPoints generates all the possible outputs given a channel ID. +// In order to generate these possible outpoints, we perform a brute-force +// search through the candidate output index space, performing a reverse +// mapping from channelID back to OutPoint. +func (c *ChannelID) GenPossibleOutPoints() [MaxFundingTxOutputs]wire.OutPoint { + var possiblePoints [MaxFundingTxOutputs]wire.OutPoint + for i := uint32(0); i < MaxFundingTxOutputs; i++ { + cidCopy := *c + xorTxid(cidCopy, uint16(i)) + + possiblePoints[i] = wire.OutPoint{ + Hash: chainhash.Hash(cidCopy), + Index: i, + } } + + return possiblePoints } -// ToUint64 converts the ShortChannelID into a compact format encoded within a -// uint64 (8 bytes). -func (c *ShortChannelID) ToUint64() uint64 { - // TODO(roasbeef): explicit error on overflow? - return ((uint64(c.BlockHeight) << 40) | (uint64(c.TxIndex) << 16) | - (uint64(c.TxPosition))) +// IsChanPoint returns true if the OutPoint passed corresponds to the target +// ChannelID. +func (c ChannelID) IsChanPoint(op *wire.OutPoint) bool { + candidateCid := NewChanIDFromOutPoint(op) + + return candidateCid == c } diff --git a/lnwire/channel_id_test.go b/lnwire/channel_id_test.go index 3abd3f7e..101e31c7 100644 --- a/lnwire/channel_id_test.go +++ b/lnwire/channel_id_test.go @@ -1,39 +1,56 @@ package lnwire -import ( - "reflect" - "testing" +import "testing" - "github.com/davecgh/go-spew/spew" -) +// TestChannelIDOutPointConversion ensures that the IsChanPoint always +// recognizes its seed OutPoint for all possible values of an output index. +func TestChannelIDOutPointConversion(t *testing.T) { + testChanPoint := *outpoint1 -func TestChannelIDEncoding(t *testing.T) { - var testCases = []ShortChannelID{ - { - BlockHeight: (1 << 24) - 1, - TxIndex: (1 << 24) - 1, - TxPosition: (1 << 16) - 1, - }, - { - BlockHeight: 2304934, - TxIndex: 2345, - TxPosition: 5, - }, - { - BlockHeight: 9304934, - TxIndex: 2345, - TxPosition: 5233, - }, - } + // For a given OutPoint, we'll run through all the possible output + // index values, mutating our test outpoint to match that output index. + for i := uint32(0); i < MaxFundingTxOutputs; i++ { + testChanPoint.Index = i - for _, testCase := range testCases { - chanInt := testCase.ToUint64() + // With the output index mutated, we'll convert it into a + // ChannelID. + cid := NewChanIDFromOutPoint(&testChanPoint) - newChanID := NewShortChanIDFromInt(chanInt) - - if !reflect.DeepEqual(testCase, newChanID) { - t.Fatalf("chan ID's don't match: expected %v got %v", - spew.Sdump(testCase), spew.Sdump(newChanID)) + // Once the channel point has been converted to a channelID, it + // should recognize its original outpoint. + if !cid.IsChanPoint(&testChanPoint) { + t.Fatalf("channelID not recognized as seed channel "+ + "point: cid=%v, op=%v", cid, testChanPoint) } } } + +// TestGenPossibleOutPoints ensures taht the GenPossibleOutPoints generates a +// vali set of outpoints for a channelID. A set of outpoints is valid iff, the +// root outpoint (the outpoint that generated the ChannelID) is included in the +// returned set of outpoints. +func TestGenPossibleOutPoints(t *testing.T) { + // We'll first convert out test outpoint into a ChannelID. + testChanPoint := *outpoint1 + chanID := NewChanIDFromOutPoint(&testChanPoint) + + // With the chan ID created, we'll generate all possible root outpoints + // given this channel ID. + possibleOutPoints := chanID.GenPossibleOutPoints() + + // If we run through the generated possible outpoints, the original + // root outpoint MUST be found in this generated set. + var opFound bool + for _, op := range possibleOutPoints { + if op == testChanPoint { + opFound = true + break + } + } + + // If we weren't able to locate the original outpoint in the set of + // possible outpoints, then we'll fail the test. + if !opFound { + t.Fatalf("possible outpoints did not contain the root outpoint") + } +} diff --git a/lnwire/short_channel_id.go b/lnwire/short_channel_id.go new file mode 100644 index 00000000..6239cda7 --- /dev/null +++ b/lnwire/short_channel_id.go @@ -0,0 +1,39 @@ +package lnwire + +// ShortChannelID represent the set of data which is needed to retrieve all +// necessary data to validate the channel existence. +type ShortChannelID struct { + // BlockHeight is the height of the block where funding transaction + // located. + // + // NOTE: This field is limited to 3 bytes. + BlockHeight uint32 + + // TxIndex is a position of funding transaction within a block. + // + // NOTE: This field is limited to 3 bytes. + TxIndex uint32 + + // TxPosition indicating transaction output which pays to the channel. + TxPosition uint16 +} + +// NewShortChanIDFromInt returns a new ShortChannelID which is the decoded +// version of the compact channel ID encoded within the uint64. The format of +// the compact channel ID is as follows: 3 bytes for the block height, 3 bytes +// for the transaction index, and 2 bytes for the output index. +func NewShortChanIDFromInt(chanID uint64) ShortChannelID { + return ShortChannelID{ + BlockHeight: uint32(chanID >> 40), + TxIndex: uint32(chanID>>16) & 0xFFFFFF, + TxPosition: uint16(chanID), + } +} + +// ToUint64 converts the ShortChannelID into a compact format encoded within a +// uint64 (8 bytes). +func (c *ShortChannelID) ToUint64() uint64 { + // TODO(roasbeef): explicit error on overflow? + return ((uint64(c.BlockHeight) << 40) | (uint64(c.TxIndex) << 16) | + (uint64(c.TxPosition))) +} diff --git a/lnwire/short_channel_id_test.go b/lnwire/short_channel_id_test.go new file mode 100644 index 00000000..a72af3a5 --- /dev/null +++ b/lnwire/short_channel_id_test.go @@ -0,0 +1,39 @@ +package lnwire + +import ( + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" +) + +func TestShortChannelIDEncoding(t *testing.T) { + var testCases = []ShortChannelID{ + { + BlockHeight: (1 << 24) - 1, + TxIndex: (1 << 24) - 1, + TxPosition: (1 << 16) - 1, + }, + { + BlockHeight: 2304934, + TxIndex: 2345, + TxPosition: 5, + }, + { + BlockHeight: 9304934, + TxIndex: 2345, + TxPosition: 5233, + }, + } + + for _, testCase := range testCases { + chanInt := testCase.ToUint64() + + newChanID := NewShortChanIDFromInt(chanInt) + + if !reflect.DeepEqual(testCase, newChanID) { + t.Fatalf("chan ID's don't match: expected %v got %v", + spew.Sdump(testCase), spew.Sdump(newChanID)) + } + } +}