channeldb+lnwallet: replace elkrem with shachain

In this commit the initial implementation of revocation hash
generation 'elkrem' was replaced with 'shachain' Rusty Russel
implementation which currently enshrined in the spec. This alghoritm has
the same asymptotic characteristics but has more complex scheme
to determine wish hash we can drop and what needs to be stored
in order to be able to achive full compression.
This commit is contained in:
Andrey Samokhvalov authored and Olaoluwa Osuntokun committed 2017-02-22 16:49:29 -08:00
1 parent b40afeaa08
commit f86557c3e4
11 files changed
+206 -573

No files matched your search

+63 -54
View File
@@ -9,9 +9,8 @@ import (
"time"
"github.com/boltdb/bolt"
"github.com/lightningnetwork/lnd/elkrem"
"github.com/lightningnetwork/lnd/shachain"
"github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcd/chaincfg/chainhash"
"github.com/roasbeef/btcd/wire"
"github.com/roasbeef/btcutil"
)
@@ -89,9 +88,9 @@ var (
// and finally 2-of-2 multisig redeem script.
fundingTxnKey = []byte("fsk")
// elkremStateKey stores their current revocation hash, and our elkrem
// sender, and their elkrem receiver.
elkremStateKey = []byte("esk")
// preimageStateKey stores their current revocation hash, our
// preimage producer and their preimage store.
preimageStateKey = []byte("esk")
// deliveryScriptsKey stores the scripts for the final delivery in the
// case of a cooperative closure.
@@ -225,8 +224,17 @@ type OpenChannel struct {
// aren't yet able to verify that it's actually in the hash chain.
TheirCurrentRevocation *btcec.PublicKey
TheirCurrentRevocationHash [32]byte
LocalElkrem *elkrem.ElkremSender
RemoteElkrem *elkrem.ElkremReceiver
// RevocationProducer is used to generate the revocation in such a way
// that remote side might store it efficiently and have the ability to
// restore the revocation by index if needed. Current implementation of
// secret producer is shachain producer.
RevocationProducer shachain.Producer
// RevocationStore is used to efficiently store the revocations for
// previous channels states sent to us by remote side. Current
// implementation of secret store is shachain store.
RevocationStore shachain.Store
// OurDeliveryScript is the script to be used to pay to us in
// cooperative closes.
@@ -480,15 +488,16 @@ func (c *OpenChannel) AppendToRevocationLog(delta *ChannelDelta) error {
return err
}
// Persist the latest elkrem state to disk as the remote peer
// has just added to our local elkrem receiver, and given us a
// new pending revocation key.
if err := putChanElkremState(nodeChanBucket, c); err != nil {
// Persist the latest preimage state to disk as the remote peer
// has just added to our local preimage store, and
// given us a new pending revocation key.
if err := putChanPreimageState(nodeChanBucket, c); err != nil {
return err
}
// With the current elkrem state updated, append a new log
// entry recording this the delta of this state transition.
// With the current preimage producer/store state updated,
// append a new log entry recording this the delta of this state
// transition.
// TODO(roasbeef): could make the deltas relative, would save
// space, but then tradeoff for more disk-seeks to recover the
// full state.
@@ -737,7 +746,7 @@ func putOpenChannel(openChanBucket *bolt.Bucket, nodeChanBucket *bolt.Bucket,
if err := putChanFundingInfo(nodeChanBucket, channel); err != nil {
return err
}
if err := putChanElkremState(nodeChanBucket, channel); err != nil {
if err := putChanPreimageState(nodeChanBucket, channel); err != nil {
return err
}
if err := putChanDeliveryScripts(nodeChanBucket, channel); err != nil {
@@ -774,8 +783,8 @@ func fetchOpenChannel(openChanBucket *bolt.Bucket, nodeChanBucket *bolt.Bucket,
if err = fetchChanFundingInfo(nodeChanBucket, channel); err != nil {
return nil, fmt.Errorf("unable to read funding info: %v", err)
}
if err = fetchChanElkremState(nodeChanBucket, channel); err != nil {
return nil, fmt.Errorf("uable to read elkrem state: %v", err)
if err = fetchChanPreimageState(nodeChanBucket, channel); err != nil {
return nil, err
}
if err = fetchChanDeliveryScripts(nodeChanBucket, channel); err != nil {
return nil, fmt.Errorf("unable to read delivery scripts: %v", err)
@@ -849,7 +858,7 @@ func deleteOpenChannel(openChanBucket *bolt.Bucket, nodeChanBucket *bolt.Bucket,
if err := deleteChanFundingInfo(nodeChanBucket, channelID); err != nil {
return err
}
if err := deleteChanElkremState(nodeChanBucket, channelID); err != nil {
if err := deleteChanPreimageState(nodeChanBucket, channelID); err != nil {
return err
}
if err := deleteChanDeliveryScripts(nodeChanBucket, channelID); err != nil {
@@ -1459,16 +1468,7 @@ func fetchChanFundingInfo(nodeChanBucket *bolt.Bucket, channel *OpenChannel) err
return nil
}
func putChanElkremState(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error {
var bc bytes.Buffer
if err := writeOutpoint(&bc, channel.ChanID); err != nil {
return err
}
elkremKey := make([]byte, len(elkremStateKey)+bc.Len())
copy(elkremKey[:3], elkremStateKey)
copy(elkremKey[3:], bc.Bytes())
func putChanPreimageState(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error {
var b bytes.Buffer
revKey := channel.TheirCurrentRevocation.SerializeCompressed()
@@ -1482,16 +1482,19 @@ func putChanElkremState(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error
// TODO(roasbeef): shouldn't be storing on disk, should re-derive as
// needed
senderBytes := channel.LocalElkrem.ToBytes()
if err := wire.WriteVarBytes(&b, 0, senderBytes); err != nil {
return err
}
reciverBytes, err := channel.RemoteElkrem.ToBytes()
data, err := channel.RevocationProducer.ToBytes()
if err != nil {
return err
}
if err := wire.WriteVarBytes(&b, 0, reciverBytes); err != nil {
if err := wire.WriteVarBytes(&b, 0, data); err != nil {
return err
}
data, err = channel.RevocationStore.ToBytes()
if err != nil {
return err
}
if err := wire.WriteVarBytes(&b, 0, data); err != nil {
return err
}
@@ -1499,28 +1502,36 @@ func putChanElkremState(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error
return err
}
return nodeChanBucket.Put(elkremKey, b.Bytes())
var bc bytes.Buffer
if err := writeOutpoint(&bc, channel.ChanID); err != nil {
return err
}
preimageKey := make([]byte, len(preimageStateKey)+bc.Len())
copy(preimageKey[:3], preimageStateKey)
copy(preimageKey[3:], bc.Bytes())
return nodeChanBucket.Put(preimageKey, b.Bytes())
}
func deleteChanElkremState(nodeChanBucket *bolt.Bucket, chanID []byte) error {
elkremKey := make([]byte, len(elkremStateKey)+len(chanID))
copy(elkremKey[:3], elkremStateKey)
copy(elkremKey[3:], chanID)
return nodeChanBucket.Delete(elkremKey)
func deleteChanPreimageState(nodeChanBucket *bolt.Bucket, chanID []byte) error {
preimageKey := make([]byte, len(preimageStateKey)+len(chanID))
copy(preimageKey[:3], preimageStateKey)
copy(preimageKey[3:], chanID)
return nodeChanBucket.Delete(preimageKey)
}
func fetchChanElkremState(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error {
func fetchChanPreimageState(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error {
var b bytes.Buffer
if err := writeOutpoint(&b, channel.ChanID); err != nil {
return err
}
elkremKey := make([]byte, len(elkremStateKey)+b.Len())
copy(elkremKey[:3], elkremStateKey)
copy(elkremKey[3:], b.Bytes())
preimageKey := make([]byte, len(preimageStateKey)+b.Len())
copy(preimageKey[:3], preimageStateKey)
copy(preimageKey[3:], b.Bytes())
elkremStateBytes := bytes.NewReader(nodeChanBucket.Get(elkremKey))
reader := bytes.NewReader(nodeChanBucket.Get(preimageKey))
revKeyBytes, err := wire.ReadVarBytes(elkremStateBytes, 0, 1000, "")
revKeyBytes, err := wire.ReadVarBytes(reader, 0, 1000, "")
if err != nil {
return err
}
@@ -1529,32 +1540,30 @@ func fetchChanElkremState(nodeChanBucket *bolt.Bucket, channel *OpenChannel) err
return err
}
if _, err := elkremStateBytes.Read(channel.TheirCurrentRevocationHash[:]); err != nil {
if _, err := reader.Read(channel.TheirCurrentRevocationHash[:]); err != nil {
return err
}
// TODO(roasbeef): should be rederiving on fly, or encrypting on disk.
senderBytes, err := wire.ReadVarBytes(elkremStateBytes, 0, 1000, "")
producerBytes, err := wire.ReadVarBytes(reader, 0, 1000, "")
if err != nil {
return err
}
elkremRoot, err := chainhash.NewHash(senderBytes)
channel.RevocationProducer, err = shachain.NewRevocationProducerFromBytes(producerBytes)
if err != nil {
return err
}
channel.LocalElkrem = elkrem.NewElkremSender(*elkremRoot)
reciverBytes, err := wire.ReadVarBytes(elkremStateBytes, 0, 1000, "")
storeBytes, err := wire.ReadVarBytes(reader, 0, 1000, "")
if err != nil {
return err
}
remoteE, err := elkrem.ElkremReceiverFromBytes(reciverBytes)
channel.RevocationStore, err = shachain.NewRevocationStoreFromBytes(storeBytes)
if err != nil {
return err
}
channel.RemoteElkrem = remoteE
_, err = io.ReadFull(elkremStateBytes, channel.StateHintObsfucator[:])
_, err = io.ReadFull(reader, channel.StateHintObsfucator[:])
if err != nil {
return err
}
+62 -51
View File
@@ -9,7 +9,7 @@ import (
"time"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/elkrem"
"github.com/lightningnetwork/lnd/shachain"
"github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcd/chaincfg"
"github.com/roasbeef/btcd/chaincfg/chainhash"
@@ -117,17 +117,16 @@ func createTestChannelState(cdb *DB) (*OpenChannel, error) {
return nil, err
}
// Simulate 1000 channel updates via progression of the elkrem
// revocation trees.
sender := elkrem.NewElkremSender(key)
receiver := &elkrem.ElkremReceiver{}
// Simulate 1000 channel updates.
producer := shachain.NewRevocationProducer((*chainhash.Hash)(&key))
store := shachain.NewRevocationStore()
for i := 0; i < 1000; i++ {
preImage, err := sender.AtIndex(uint64(i))
preImage, err := producer.AtIndex(uint64(i))
if err != nil {
return nil, err
}
if receiver.AddNext(preImage); err != nil {
if store.Store(preImage); err != nil {
return nil, err
}
}
@@ -150,8 +149,8 @@ func createTestChannelState(cdb *DB) (*OpenChannel, error) {
TheirBalance: btcutil.Amount(9000),
OurCommitTx: testTx,
OurCommitSig: bytes.Repeat([]byte{1}, 71),
LocalElkrem: sender,
RemoteElkrem: receiver,
RevocationProducer: producer,
RevocationStore: store,
StateHintObsfucator: obsfucator,
FundingOutpoint: testOutpoint,
OurMultiSigKey: privKey.PubKey(),
@@ -207,34 +206,34 @@ func TestOpenChannelPutGetDelete(t *testing.T) {
// The decoded channel state should be identical to what we stored
// above.
if !state.IdentityPub.IsEqual(newState.IdentityPub) {
t.Fatalf("their id doesn't match")
t.Fatal("their id doesn't match")
}
if !reflect.DeepEqual(state.ChanID, newState.ChanID) {
t.Fatalf("chan id's don't match")
t.Fatal("chan id's don't match")
}
if state.MinFeePerKb != newState.MinFeePerKb {
t.Fatalf("fee/kb doesn't match")
t.Fatal("fee/kb doesn't match")
}
if state.TheirDustLimit != newState.TheirDustLimit {
t.Fatalf("their dust limit doesn't match")
t.Fatal("their dust limit doesn't match")
}
if state.OurDustLimit != newState.OurDustLimit {
t.Fatalf("our dust limit doesn't match")
t.Fatal("our dust limit doesn't match")
}
if state.IsInitiator != newState.IsInitiator {
t.Fatalf("initiator status doesn't match")
t.Fatal("initiator status doesn't match")
}
if state.ChanType != newState.ChanType {
t.Fatalf("channel type doesn't match")
t.Fatal("channel type doesn't match")
}
if !bytes.Equal(state.OurCommitKey.SerializeCompressed(),
newState.OurCommitKey.SerializeCompressed()) {
t.Fatalf("our commit key doesn't match")
t.Fatal("our commit key doesn't match")
}
if !bytes.Equal(state.TheirCommitKey.SerializeCompressed(),
newState.TheirCommitKey.SerializeCompressed()) {
t.Fatalf("their commit key doesn't match")
t.Fatal("their commit key doesn't match")
}
if state.Capacity != newState.Capacity {
@@ -242,49 +241,49 @@ func TestOpenChannelPutGetDelete(t *testing.T) {
newState.Capacity)
}
if state.OurBalance != newState.OurBalance {
t.Fatalf("our balance doesn't match")
t.Fatal("our balance doesn't match")
}
if state.TheirBalance != newState.TheirBalance {
t.Fatalf("their balance doesn't match")
t.Fatal("their balance doesn't match")
}
var b1, b2 bytes.Buffer
if err := state.OurCommitTx.Serialize(&b1); err != nil {
t.Fatalf("unable to serialize transaction")
t.Fatal("unable to serialize transaction")
}
if err := newState.OurCommitTx.Serialize(&b2); err != nil {
t.Fatalf("unable to serialize transaction")
t.Fatal("unable to serialize transaction")
}
if !bytes.Equal(b1.Bytes(), b2.Bytes()) {
t.Fatalf("ourCommitTx doesn't match")
t.Fatal("ourCommitTx doesn't match")
}
if !bytes.Equal(newState.OurCommitSig, state.OurCommitSig) {
t.Fatalf("commit sigs don't match")
t.Fatal("commit sigs don't match")
}
// TODO(roasbeef): replace with a single equal?
if !reflect.DeepEqual(state.FundingOutpoint, newState.FundingOutpoint) {
t.Fatalf("funding outpoint doesn't match")
t.Fatal("funding outpoint doesn't match")
}
if !bytes.Equal(state.OurMultiSigKey.SerializeCompressed(),
newState.OurMultiSigKey.SerializeCompressed()) {
t.Fatalf("our multisig key doesn't match")
t.Fatal("our multisig key doesn't match")
}
if !bytes.Equal(state.TheirMultiSigKey.SerializeCompressed(),
newState.TheirMultiSigKey.SerializeCompressed()) {
t.Fatalf("their multisig key doesn't match")
t.Fatal("their multisig key doesn't match")
}
if !bytes.Equal(state.FundingWitnessScript, newState.FundingWitnessScript) {
t.Fatalf("redeem script doesn't match")
t.Fatal("redeem script doesn't match")
}
// The local and remote delivery scripts should be identical.
if !bytes.Equal(state.OurDeliveryScript, newState.OurDeliveryScript) {
t.Fatalf("our delivery address doesn't match")
t.Fatal("our delivery address doesn't match")
}
if !bytes.Equal(state.TheirDeliveryScript, newState.TheirDeliveryScript) {
t.Fatalf("their delivery address doesn't match")
t.Fatal("their delivery address doesn't match")
}
if state.NumUpdates != newState.NumUpdates {
@@ -304,33 +303,45 @@ func TestOpenChannelPutGetDelete(t *testing.T) {
state.TotalSatoshisSent, newState.TotalSatoshisSent)
}
if state.TotalSatoshisReceived != newState.TotalSatoshisReceived {
t.Fatalf("satoshis received doesn't match")
t.Fatal("satoshis received doesn't match")
}
if state.CreationTime.Unix() != newState.CreationTime.Unix() {
t.Fatalf("creation time doesn't match")
t.Fatal("creation time doesn't match")
}
// The local and remote elkrems should be identical.
if !bytes.Equal(state.LocalElkrem.ToBytes(), newState.LocalElkrem.ToBytes()) {
t.Fatalf("local elkrems don't match")
}
oldRemoteElkrem, err := state.RemoteElkrem.ToBytes()
// The local and remote producers should be identical.
oldProducer, err := state.RevocationProducer.ToBytes()
if err != nil {
t.Fatalf("unable to serialize old remote elkrem: %v", err)
t.Fatalf("can't convert old revocation producer to bytes: %v",
err)
}
newRemoteElkrem, err := newState.RemoteElkrem.ToBytes()
newProducer, err := newState.RevocationProducer.ToBytes()
if err != nil {
t.Fatalf("unable to serialize new remote elkrem: %v", err)
t.Fatalf("can't convert new revocation producer to bytes: %v",
err)
}
if !bytes.Equal(oldRemoteElkrem, newRemoteElkrem) {
t.Fatalf("remote elkrems don't match")
if !bytes.Equal(oldProducer, newProducer) {
t.Fatal("local producer don't match")
}
oldStore, err := state.RevocationStore.ToBytes()
if err != nil {
t.Fatalf("unable to serialize old remote store: %v", err)
}
newStore, err := newState.RevocationStore.ToBytes()
if err != nil {
t.Fatalf("unable to serialize new remote store: %v", err)
}
if !bytes.Equal(oldStore, newStore) {
t.Fatal("remote store don't match")
}
if !newState.TheirCurrentRevocation.IsEqual(state.TheirCurrentRevocation) {
t.Fatalf("revocation keys don't match")
t.Fatal("revocation keys don't match")
}
if !bytes.Equal(newState.TheirCurrentRevocationHash[:], state.TheirCurrentRevocationHash[:]) {
t.Fatalf("revocation hashes don't match")
t.Fatal("revocation hashes don't match")
}
if !reflect.DeepEqual(state.Htlcs[0], newState.Htlcs[0]) {
t.Fatalf("htlcs don't match: %v vs %v", spew.Sdump(state.Htlcs[0]),
@@ -338,7 +349,7 @@ func TestOpenChannelPutGetDelete(t *testing.T) {
}
if !bytes.Equal(state.StateHintObsfucator[:],
newState.StateHintObsfucator[:]) {
t.Fatalf("obsfuctators don't match")
t.Fatal("obsfuctators don't match")
}
// Finally to wrap up the test, delete the state of the channel within
@@ -363,7 +374,7 @@ func TestOpenChannelPutGetDelete(t *testing.T) {
// should yield no results.
openChans, err = cdb.FetchAllChannels()
if err != nil {
t.Fatalf("unable to fetch all open chans")
t.Fatal("unable to fetch all open chans")
}
if len(openChans) != 0 {
t.Fatalf("all channels not deleted, found %v", len(openChans))
@@ -494,13 +505,13 @@ func TestChannelStateTransition(t *testing.T) {
// The two deltas (the original vs the on-disk version) should
// identical, and all HTLC data should properly be retained.
if delta.LocalBalance != diskDelta.LocalBalance {
t.Fatalf("local balances don't match")
t.Fatal("local balances don't match")
}
if delta.RemoteBalance != diskDelta.RemoteBalance {
t.Fatalf("remote balances don't match")
t.Fatal("remote balances don't match")
}
if delta.UpdateNum != diskDelta.UpdateNum {
t.Fatalf("update number doesn't match")
t.Fatal("update number doesn't match")
}
for i := 0; i < len(delta.Htlcs); i++ {
originalHTLC := delta.Htlcs[i]
@@ -546,7 +557,7 @@ func TestChannelStateTransition(t *testing.T) {
}
if !bytes.Equal(updatedChannel[0].TheirCurrentRevocationHash[:],
newRevocation) {
t.Fatalf("revocation state wasn't synced!")
t.Fatal("revocation state wasn't synced!")
}
// Now attempt to delete the channel from the database.
@@ -569,6 +580,6 @@ func TestChannelStateTransition(t *testing.T) {
// revocation log has been deleted.
_, err = updatedChannel[0].FindPreviousState(uint64(delta.UpdateNum))
if err == nil {
t.Fatalf("revocation log search should've failed")
t.Fatal("revocation log search should've failed")
}
}