Merge pull request #3184 from wpaulino/wtclient-subserver

multi: add watchtower client RPC subserver
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2019-07-30 17:26:22 -07:00
commit 8c9c4b52e8
34 files changed
+3362 -243

No files matched your search

+221 -19
View File
@@ -7,6 +7,7 @@ import (
"math"
"net"
"github.com/btcsuite/btcd/btcec"
"github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/lnwire"
)
@@ -55,6 +56,11 @@ var (
// database.
ErrTowerNotFound = errors.New("tower not found")
// ErrTowerUnackedUpdates is an error returned when we attempt to mark a
// tower's sessions as inactive, but one of its sessions has unacked
// updates.
ErrTowerUnackedUpdates = errors.New("tower has unacked updates")
// ErrCorruptClientSession signals that the client session's on-disk
// structure deviates from what is expected.
ErrCorruptClientSession = errors.New("client session corrupted")
@@ -199,9 +205,11 @@ func (c *ClientDB) Close() error {
return c.db.Close()
}
// CreateTower initializes a database entry with the given lightning address. If
// the tower exists, the address is append to the list of all addresses used to
// that tower previously.
// CreateTower initialize an address record used to communicate with a
// watchtower. Each Tower is assigned a unique ID, that is used to amortize
// storage costs of the public key when used by multiple sessions. If the tower
// already exists, the address is appended to the list of all addresses used to
// that tower previously and its corresponding sessions are marked as active.
func (c *ClientDB) CreateTower(lnAddr *lnwire.NetAddress) (*Tower, error) {
var towerPubKey [33]byte
copy(towerPubKey[:], lnAddr.IdentityKey.SerializeCompressed())
@@ -233,6 +241,32 @@ func (c *ClientDB) CreateTower(lnAddr *lnwire.NetAddress) (*Tower, error) {
// address is a duplicate, this will result in no
// change.
tower.AddAddress(lnAddr.Address)
// If there are any client sessions that correspond to
// this tower, we'll mark them as active to ensure we
// load them upon restarts.
//
// TODO(wilmer): with an index of tower -> sessions we
// can avoid the linear lookup.
sessions := tx.Bucket(cSessionBkt)
if sessions == nil {
return ErrUninitializedDB
}
towerID := TowerIDFromBytes(towerIDBytes)
towerSessions, err := listClientSessions(
sessions, &towerID,
)
if err != nil {
return err
}
for _, session := range towerSessions {
err := markSessionStatus(
sessions, session, CSessionActive,
)
if err != nil {
return err
}
}
} else {
// No such tower exists, create a new tower id for our
// new tower. The error is unhandled since NextSequence
@@ -265,8 +299,89 @@ func (c *ClientDB) CreateTower(lnAddr *lnwire.NetAddress) (*Tower, error) {
return tower, nil
}
// LoadTower retrieves a tower by its tower ID.
func (c *ClientDB) LoadTower(towerID TowerID) (*Tower, error) {
// RemoveTower modifies a tower's record within the database. If an address is
// provided, then _only_ the address record should be removed from the tower's
// persisted state. Otherwise, we'll attempt to mark the tower as inactive by
// marking all of its sessions inactive. If any of its sessions has unacked
// updates, then ErrTowerUnackedUpdates is returned. If the tower doesn't have
// any sessions at all, it'll be completely removed from the database.
//
// NOTE: An error is not returned if the tower doesn't exist.
func (c *ClientDB) RemoveTower(pubKey *btcec.PublicKey, addr net.Addr) error {
return c.db.Update(func(tx *bbolt.Tx) error {
towers := tx.Bucket(cTowerBkt)
if towers == nil {
return ErrUninitializedDB
}
towerIndex := tx.Bucket(cTowerIndexBkt)
if towerIndex == nil {
return ErrUninitializedDB
}
// Don't return an error if the watchtower doesn't exist to act
// as a NOP.
pubKeyBytes := pubKey.SerializeCompressed()
towerIDBytes := towerIndex.Get(pubKeyBytes)
if towerIDBytes == nil {
return nil
}
// If an address is provided, then we should _only_ remove the
// address record from the database.
if addr != nil {
tower, err := getTower(towers, towerIDBytes)
if err != nil {
return err
}
tower.RemoveAddress(addr)
return putTower(towers, tower)
}
// Otherwise, we should attempt to mark the tower's sessions as
// inactive.
//
// TODO(wilmer): with an index of tower -> sessions we can avoid
// the linear lookup.
sessions := tx.Bucket(cSessionBkt)
if sessions == nil {
return ErrUninitializedDB
}
towerID := TowerIDFromBytes(towerIDBytes)
towerSessions, err := listClientSessions(sessions, &towerID)
if err != nil {
return err
}
// If it doesn't have any, we can completely remove it from the
// database.
if len(towerSessions) == 0 {
if err := towerIndex.Delete(pubKeyBytes); err != nil {
return err
}
return towers.Delete(towerIDBytes)
}
// We'll mark its sessions as inactive as long as they don't
// have any pending updates to ensure we don't load them upon
// restarts.
for _, session := range towerSessions {
if len(session.CommittedUpdates) > 0 {
return ErrTowerUnackedUpdates
}
err := markSessionStatus(
sessions, session, CSessionInactive,
)
if err != nil {
return err
}
}
return nil
})
}
// LoadTowerByID retrieves a tower by its tower ID.
func (c *ClientDB) LoadTowerByID(towerID TowerID) (*Tower, error) {
var tower *Tower
err := c.db.View(func(tx *bbolt.Tx) error {
towers := tx.Bucket(cTowerBkt)
@@ -285,6 +400,60 @@ func (c *ClientDB) LoadTower(towerID TowerID) (*Tower, error) {
return tower, nil
}
// LoadTower retrieves a tower by its public key.
func (c *ClientDB) LoadTower(pubKey *btcec.PublicKey) (*Tower, error) {
var tower *Tower
err := c.db.View(func(tx *bbolt.Tx) error {
towers := tx.Bucket(cTowerBkt)
if towers == nil {
return ErrUninitializedDB
}
towerIndex := tx.Bucket(cTowerIndexBkt)
if towerIndex == nil {
return ErrUninitializedDB
}
towerIDBytes := towerIndex.Get(pubKey.SerializeCompressed())
if towerIDBytes == nil {
return ErrTowerNotFound
}
var err error
tower, err = getTower(towers, towerIDBytes)
return err
})
if err != nil {
return nil, err
}
return tower, nil
}
// ListTowers retrieves the list of towers available within the database.
func (c *ClientDB) ListTowers() ([]*Tower, error) {
var towers []*Tower
err := c.db.View(func(tx *bbolt.Tx) error {
towerBucket := tx.Bucket(cTowerBkt)
if towerBucket == nil {
return ErrUninitializedDB
}
return towerBucket.ForEach(func(towerIDBytes, _ []byte) error {
tower, err := getTower(towerBucket, towerIDBytes)
if err != nil {
return err
}
towers = append(towers, tower)
return nil
})
})
if err != nil {
return nil, err
}
return towers, nil
}
// NextSessionKeyIndex reserves a new session key derivation index for a
// particular tower id. The index is reserved for that tower until
// CreateClientSession is invoked for that tower and index, at which point a new
@@ -384,29 +553,53 @@ func (c *ClientDB) CreateClientSession(session *ClientSession) error {
})
}
// ListClientSessions returns the set of all client sessions known to the db.
func (c *ClientDB) ListClientSessions() (map[SessionID]*ClientSession, error) {
clientSessions := make(map[SessionID]*ClientSession)
// ListClientSessions returns the set of all client sessions known to the db. An
// optional tower ID can be used to filter out any client sessions in the
// response that do not correspond to this tower.
func (c *ClientDB) ListClientSessions(id *TowerID) (map[SessionID]*ClientSession, error) {
var clientSessions map[SessionID]*ClientSession
err := c.db.View(func(tx *bbolt.Tx) error {
sessions := tx.Bucket(cSessionBkt)
if sessions == nil {
return ErrUninitializedDB
}
var err error
clientSessions, err = listClientSessions(sessions, id)
return err
})
if err != nil {
return nil, err
}
return sessions.ForEach(func(k, _ []byte) error {
// We'll load the full client session since the client
// will need the CommittedUpdates and AckedUpdates on
// startup to resume committed updates and compute the
// highest known commit height for each channel.
session, err := getClientSession(sessions, k)
if err != nil {
return err
}
return clientSessions, nil
}
clientSessions[session.ID] = session
// listClientSessions returns the set of all client sessions known to the db. An
// optional tower ID can be used to filter out any client sessions in the
// response that do not correspond to this tower.
func listClientSessions(sessions *bbolt.Bucket,
id *TowerID) (map[SessionID]*ClientSession, error) {
clientSessions := make(map[SessionID]*ClientSession)
err := sessions.ForEach(func(k, _ []byte) error {
// We'll load the full client session since the client will need
// the CommittedUpdates and AckedUpdates on startup to resume
// committed updates and compute the highest known commit height
// for each channel.
session, err := getClientSession(sessions, k)
if err != nil {
return err
}
// Filter out any sessions that don't correspond to the given
// tower if one was set.
if id != nil && session.TowerID != *id {
return nil
})
}
clientSessions[session.ID] = session
return nil
})
if err != nil {
return nil, err
@@ -847,6 +1040,15 @@ func putClientSessionBody(sessions *bbolt.Bucket,
return sessionBkt.Put(cSessionBody, b.Bytes())
}
// markSessionStatus updates the persisted state of the session to the new
// status.
func markSessionStatus(sessions *bbolt.Bucket, session *ClientSession,
status CSessionStatus) error {
session.Status = status
return putClientSessionBody(sessions, session)
}
// getChanSummary loads a ClientChanSummary for the passed chanID.
func getChanSummary(chanSummaries *bbolt.Bucket,
chanID lnwire.ChannelID) (*ClientChanSummary, error) {
+223 -15
View File
@@ -10,6 +10,7 @@ import (
"reflect"
"testing"
"github.com/btcsuite/btcd/btcec"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/lightningnetwork/lnd/watchtower/wtclient"
@@ -48,10 +49,10 @@ func (h *clientDBHarness) insertSession(session *wtdb.ClientSession, expErr erro
}
}
func (h *clientDBHarness) listSessions() map[wtdb.SessionID]*wtdb.ClientSession {
func (h *clientDBHarness) listSessions(id *wtdb.TowerID) map[wtdb.SessionID]*wtdb.ClientSession {
h.t.Helper()
sessions, err := h.db.ListClientSessions()
sessions, err := h.db.ListClientSessions(id)
if err != nil {
h.t.Fatalf("unable to list client sessions: %v", err)
}
@@ -89,13 +90,81 @@ func (h *clientDBHarness) createTower(lnAddr *lnwire.NetAddress,
h.t.Fatalf("tower id should never be 0")
}
for _, session := range h.listSessions(&tower.ID) {
if session.Status != wtdb.CSessionActive {
h.t.Fatalf("expected status for session %v to be %v, "+
"got %v", session.ID, wtdb.CSessionActive,
session.Status)
}
}
return tower
}
func (h *clientDBHarness) loadTower(id wtdb.TowerID, expErr error) *wtdb.Tower {
func (h *clientDBHarness) removeTower(pubKey *btcec.PublicKey, addr net.Addr,
hasSessions bool, expErr error) {
h.t.Helper()
tower, err := h.db.LoadTower(id)
if err := h.db.RemoveTower(pubKey, addr); err != expErr {
h.t.Fatalf("expected remove tower error: %v, got %v", expErr, err)
}
if expErr != nil {
return
}
if addr != nil {
tower, err := h.db.LoadTower(pubKey)
if err != nil {
h.t.Fatalf("expected tower %x to still exist",
pubKey.SerializeCompressed())
}
removedAddr := addr.String()
for _, towerAddr := range tower.Addresses {
if towerAddr.String() == removedAddr {
h.t.Fatalf("address %v not removed for tower %x",
removedAddr, pubKey.SerializeCompressed())
}
}
} else {
tower, err := h.db.LoadTower(pubKey)
if hasSessions && err != nil {
h.t.Fatalf("expected tower %x with sessions to still "+
"exist", pubKey.SerializeCompressed())
}
if !hasSessions && err == nil {
h.t.Fatalf("expected tower %x with no sessions to not "+
"exist", pubKey.SerializeCompressed())
}
if !hasSessions {
return
}
for _, session := range h.listSessions(&tower.ID) {
if session.Status != wtdb.CSessionInactive {
h.t.Fatalf("expected status for session %v to "+
"be %v, got %v", session.ID,
wtdb.CSessionInactive, session.Status)
}
}
}
}
func (h *clientDBHarness) loadTower(pubKey *btcec.PublicKey, expErr error) *wtdb.Tower {
h.t.Helper()
tower, err := h.db.LoadTower(pubKey)
if err != expErr {
h.t.Fatalf("expected load tower error: %v, got: %v", expErr, err)
}
return tower
}
func (h *clientDBHarness) loadTowerByID(id wtdb.TowerID, expErr error) *wtdb.Tower {
h.t.Helper()
tower, err := h.db.LoadTowerByID(id)
if err != expErr {
h.t.Fatalf("expected load tower error: %v, got: %v", expErr, err)
}
@@ -172,7 +241,7 @@ func testCreateClientSession(h *clientDBHarness) {
// First, assert that this session is not already present in the
// database.
if _, ok := h.listSessions()[session.ID]; ok {
if _, ok := h.listSessions(nil)[session.ID]; ok {
h.t.Fatalf("session for id %x should not exist yet", session.ID)
}
@@ -202,7 +271,7 @@ func testCreateClientSession(h *clientDBHarness) {
h.insertSession(session, nil)
// Verify that the session now exists in the database.
if _, ok := h.listSessions()[session.ID]; !ok {
if _, ok := h.listSessions(nil)[session.ID]; !ok {
h.t.Fatalf("session for id %x should exist now", session.ID)
}
@@ -218,12 +287,57 @@ func testCreateClientSession(h *clientDBHarness) {
}
}
// testFilterClientSessions asserts that we can correctly filter client sessions
// for a specific tower.
func testFilterClientSessions(h *clientDBHarness) {
// We'll create three client sessions, the first two belonging to one
// tower, and the last belonging to another one.
const numSessions = 3
towerSessions := make(map[wtdb.TowerID][]wtdb.SessionID)
for i := 0; i < numSessions; i++ {
towerID := wtdb.TowerID(1)
if i == numSessions-1 {
towerID = wtdb.TowerID(2)
}
keyIndex := h.nextKeyIndex(towerID, nil)
sessionID := wtdb.SessionID([33]byte{byte(i)})
h.insertSession(&wtdb.ClientSession{
ClientSessionBody: wtdb.ClientSessionBody{
TowerID: towerID,
Policy: wtpolicy.Policy{
MaxUpdates: 100,
},
RewardPkScript: []byte{0x01, 0x02, 0x03},
KeyIndex: keyIndex,
},
ID: sessionID,
}, nil)
towerSessions[towerID] = append(towerSessions[towerID], sessionID)
}
// We should see the expected sessions for each tower when filtering
// them.
for towerID, expectedSessions := range towerSessions {
sessions := h.listSessions(&towerID)
if len(sessions) != len(expectedSessions) {
h.t.Fatalf("expected %v sessions for tower %v, got %v",
len(expectedSessions), towerID, len(sessions))
}
for _, expectedSession := range expectedSessions {
if _, ok := sessions[expectedSession]; !ok {
h.t.Fatalf("expected session %v for tower %v",
expectedSession, towerID)
}
}
}
}
// testCreateTower asserts the behavior of creating new Tower objects within the
// database, and that the latest address is always prepended to the list of
// known addresses for the tower.
func testCreateTower(h *clientDBHarness) {
// Test that loading a tower with an arbitrary tower id fails.
h.loadTower(20, wtdb.ErrTowerNotFound)
h.loadTowerByID(20, wtdb.ErrTowerNotFound)
pk, err := randPubKey()
if err != nil {
@@ -241,7 +355,12 @@ func testCreateTower(h *clientDBHarness) {
// Load the tower from the database and assert that it matches the tower
// we created.
tower2 := h.loadTower(tower.ID, nil)
tower2 := h.loadTowerByID(tower.ID, nil)
if !reflect.DeepEqual(tower, tower2) {
h.t.Fatalf("loaded tower mismatch, want: %v, got: %v",
tower, tower2)
}
tower2 = h.loadTower(pk, err)
if !reflect.DeepEqual(tower, tower2) {
h.t.Fatalf("loaded tower mismatch, want: %v, got: %v",
tower, tower2)
@@ -272,7 +391,12 @@ func testCreateTower(h *clientDBHarness) {
// Load the tower from the database, and assert that it matches the
// tower returned from creation.
towerNewAddr2 := h.loadTower(tower.ID, nil)
towerNewAddr2 := h.loadTowerByID(tower.ID, nil)
if !reflect.DeepEqual(towerNewAddr, towerNewAddr2) {
h.t.Fatalf("loaded tower mismatch, want: %v, got: %v",
towerNewAddr, towerNewAddr2)
}
towerNewAddr2 = h.loadTower(pk, nil)
if !reflect.DeepEqual(towerNewAddr, towerNewAddr2) {
h.t.Fatalf("loaded tower mismatch, want: %v, got: %v",
towerNewAddr, towerNewAddr2)
@@ -290,6 +414,82 @@ func testCreateTower(h *clientDBHarness) {
}
}
// testRemoveTower asserts the behavior of removing Tower objects as a whole and
// removing addresses from Tower objects within the database.
func testRemoveTower(h *clientDBHarness) {
// Generate a random public key we'll use for our tower.
pk, err := randPubKey()
if err != nil {
h.t.Fatalf("unable to generate pubkey: %v", err)
}
// Removing a tower that does not exist within the database should
// result in a NOP.
h.removeTower(pk, nil, false, nil)
// We'll create a tower with two addresses.
addr1 := &net.TCPAddr{IP: []byte{0x01, 0x00, 0x00, 0x00}, Port: 9911}
addr2 := &net.TCPAddr{IP: []byte{0x02, 0x00, 0x00, 0x00}, Port: 9911}
h.createTower(&lnwire.NetAddress{
IdentityKey: pk,
Address: addr1,
}, nil)
h.createTower(&lnwire.NetAddress{
IdentityKey: pk,
Address: addr2,
}, nil)
// We'll then remove the second address. We should now only see the
// first.
h.removeTower(pk, addr2, false, nil)
// We'll then remove the first address. We should now see that the tower
// has no addresses left.
h.removeTower(pk, addr1, false, nil)
// Removing the tower as a whole from the database should succeed since
// there aren't any active sessions for it.
h.removeTower(pk, nil, false, nil)
// We'll then recreate the tower, but this time we'll create a session
// for it.
tower := h.createTower(&lnwire.NetAddress{
IdentityKey: pk,
Address: addr1,
}, nil)
session := &wtdb.ClientSession{
ClientSessionBody: wtdb.ClientSessionBody{
TowerID: tower.ID,
Policy: wtpolicy.Policy{
MaxUpdates: 100,
},
RewardPkScript: []byte{0x01, 0x02, 0x03},
KeyIndex: h.nextKeyIndex(tower.ID, nil),
},
ID: wtdb.SessionID([33]byte{0x01}),
}
h.insertSession(session, nil)
update := randCommittedUpdate(h.t, 1)
h.commitUpdate(&session.ID, update, nil)
// We should not be able to fully remove it from the database since
// there's a session and it has unacked updates.
h.removeTower(pk, nil, true, wtdb.ErrTowerUnackedUpdates)
// Removing the tower after all sessions no longer have unacked updates
// should result in the sessions becoming inactive.
h.ackUpdate(&session.ID, 1, 1, nil)
h.removeTower(pk, nil, true, nil)
// Creating the tower again should mark all of the sessions active once
// again.
h.createTower(&lnwire.NetAddress{
IdentityKey: pk,
Address: addr1,
}, nil)
}
// testChanSummaries tests the process of a registering a channel and its
// associated sweep pkscript.
func testChanSummaries(h *clientDBHarness) {
@@ -357,7 +557,7 @@ func testCommitUpdate(h *clientDBHarness) {
// Assert that the committed update appears in the client session's
// CommittedUpdates map when loaded from disk and that there are no
// AckedUpdates.
dbSession := h.listSessions()[session.ID]
dbSession := h.listSessions(nil)[session.ID]
checkCommittedUpdates(h.t, dbSession, []wtdb.CommittedUpdate{
*update1,
})
@@ -374,7 +574,7 @@ func testCommitUpdate(h *clientDBHarness) {
}
// Assert that the loaded ClientSession is the same as before.
dbSession = h.listSessions()[session.ID]
dbSession = h.listSessions(nil)[session.ID]
checkCommittedUpdates(h.t, dbSession, []wtdb.CommittedUpdate{
*update1,
})
@@ -396,7 +596,7 @@ func testCommitUpdate(h *clientDBHarness) {
// Check that both updates now appear as committed on the ClientSession
// loaded from disk.
dbSession = h.listSessions()[session.ID]
dbSession = h.listSessions(nil)[session.ID]
checkCommittedUpdates(h.t, dbSession, []wtdb.CommittedUpdate{
*update1,
*update2,
@@ -410,7 +610,7 @@ func testCommitUpdate(h *clientDBHarness) {
h.commitUpdate(&session.ID, update4, wtdb.ErrCommitUnorderedUpdate)
// Assert that the ClientSession loaded from disk remains unchanged.
dbSession = h.listSessions()[session.ID]
dbSession = h.listSessions(nil)[session.ID]
checkCommittedUpdates(h.t, dbSession, []wtdb.CommittedUpdate{
*update1,
*update2,
@@ -467,7 +667,7 @@ func testAckUpdate(h *clientDBHarness) {
// Assert that the ClientSession loaded from disk has one update in it's
// AckedUpdates map, and that the committed update has been removed.
dbSession := h.listSessions()[session.ID]
dbSession := h.listSessions(nil)[session.ID]
checkCommittedUpdates(h.t, dbSession, nil)
checkAckedUpdates(h.t, dbSession, map[uint16]wtdb.BackupID{
1: update1.BackupID,
@@ -487,7 +687,7 @@ func testAckUpdate(h *clientDBHarness) {
h.ackUpdate(&session.ID, 2, 2, nil)
// Assert that both updates exist as AckedUpdates when loaded from disk.
dbSession = h.listSessions()[session.ID]
dbSession = h.listSessions(nil)[session.ID]
checkCommittedUpdates(h.t, dbSession, nil)
checkAckedUpdates(h.t, dbSession, map[uint16]wtdb.BackupID{
1: update1.BackupID,
@@ -620,10 +820,18 @@ func TestClientDB(t *testing.T) {
name: "create client session",
run: testCreateClientSession,
},
{
name: "filter client sessions",
run: testFilterClientSessions,
},
{
name: "create tower",
run: testCreateTower,
},
{
name: "remove tower",
run: testRemoveTower,
},
{
name: "chan summaries",
run: testChanSummaries,
+4
View File
@@ -18,6 +18,10 @@ const (
// CSessionActive indicates that the ClientSession is active and can be
// used for backups.
CSessionActive CSessionStatus = 0
// CSessionInactive indicates that the ClientSession is inactive and
// cannot be used for backups.
CSessionInactive CSessionStatus = 1
)
// ClientSession encapsulates a SessionInfo returned from a successful
+24
View File
@@ -1,6 +1,8 @@
package wtdb
import (
"encoding/hex"
"fmt"
"io"
"net"
@@ -62,6 +64,19 @@ func (t *Tower) AddAddress(addr net.Addr) {
t.Addresses = append([]net.Addr{addr}, t.Addresses...)
}
// RemoveAddress removes the given address from the tower's in-memory list of
// addresses. If the address doesn't exist, then this will act as a NOP.
func (t *Tower) RemoveAddress(addr net.Addr) {
addrStr := addr.String()
for i, address := range t.Addresses {
if address.String() != addrStr {
continue
}
t.Addresses = append(t.Addresses[:i], t.Addresses[i+1:]...)
return
}
}
// LNAddrs generates a list of lnwire.NetAddress from a Tower instance's
// addresses. This can be used to have a client try multiple addresses for the
// same Tower.
@@ -79,6 +94,15 @@ func (t *Tower) LNAddrs() []*lnwire.NetAddress {
return addrs
}
// String returns a user-friendly identifier of the tower.
func (t *Tower) String() string {
pubKey := hex.EncodeToString(t.IdentityKey.SerializeCompressed())
if len(t.Addresses) == 0 {
return pubKey
}
return fmt.Sprintf("%v@%v", pubKey, t.Addresses[0])
}
// Encode writes the Tower to the passed io.Writer. The TowerID is not
// serialized, since it acts as the key.
func (t *Tower) Encode(w io.Writer) error {