From b746bf86c27a67370494ef76c95206713e3600c1 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Thu, 10 Jan 2019 15:35:11 -0800 Subject: [PATCH] watchtower/multi: switch over to wtpolicy migrate to using wtpolicy.Policy in wtwire messages and wtserver --- watchtower/blob/justice_kit.go | 46 ++++++++----------- watchtower/blob/justice_kit_test.go | 40 ++++++++-------- watchtower/lookout/justice_descriptor_test.go | 7 ++- watchtower/lookout/lookout.go | 2 +- watchtower/lookout/lookout_test.go | 23 +++++++--- watchtower/wtdb/session_info.go | 25 +++------- watchtower/wtserver/server.go | 17 ++++--- watchtower/wtserver/server_test.go | 19 ++++---- watchtower/wtwire/create_session.go | 11 +++-- watchtower/wtwire/wtwire.go | 15 ++++++ 10 files changed, 113 insertions(+), 92 deletions(-) diff --git a/watchtower/blob/justice_kit.go b/watchtower/blob/justice_kit.go index 0eec7092..952d812a 100644 --- a/watchtower/blob/justice_kit.go +++ b/watchtower/blob/justice_kit.go @@ -17,12 +17,6 @@ import ( ) 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 = chacha20poly1305.NonceSizeX @@ -53,14 +47,14 @@ const ( // nonce: 24 bytes // enciphered plaintext: n bytes // MAC: 16 bytes -func Size(ver uint16) int { - return NonceSize + PlaintextSize(ver) + CiphertextExpansion +func Size(blobType Type) int { + return NonceSize + PlaintextSize(blobType) + CiphertextExpansion } // PlaintextSize returns the size of the encoded-but-unencrypted blob in bytes. -func PlaintextSize(ver uint16) int { - switch ver { - case 0: +func PlaintextSize(blobType Type) int { + switch { + case blobType.Has(FlagCommitOutputs): return V0PlaintextSize default: return 0 @@ -71,9 +65,9 @@ var ( // byteOrder specifies a big-endian encoding of all integer values. byteOrder = binary.BigEndian - // ErrUnknownBlobVersion signals that we don't understand the requested + // ErrUnknownBlobType signals that we don't understand the requested // blob encoding scheme. - ErrUnknownBlobVersion = errors.New("unknown blob version") + ErrUnknownBlobType = errors.New("unknown blob type") // ErrCiphertextTooSmall is a decryption error signaling that the // 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 // 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. if len(key) != KeySize { 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 // plaintext bytes. var ptxtBuf bytes.Buffer - err := b.encode(&ptxtBuf, version) + err := b.encode(&ptxtBuf, blobType) if err != nil { 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 // plaintext and MAC. plaintext := ptxtBuf.Bytes() - ciphertext := make([]byte, Size(version)) + ciphertext := make([]byte, Size(blobType)) // Generate a random 24-byte nonce in the ciphertext's prefix. 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 // chacha20poly1305 with the chosen (nonce, key) pair. The internal plaintext is // 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 { // 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 // using the specified blob version. boj := &JusticeKit{} - err = boj.decode(bytes.NewReader(plaintext), version) + err = boj.decode(bytes.NewReader(plaintext), blobType) if err != nil { 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 // error if the version is unknown. -func (b *JusticeKit) encode(w io.Writer, ver uint16) error { - switch ver { - case 0: +func (b *JusticeKit) encode(w io.Writer, blobType Type) error { + switch { + case blobType.Has(FlagCommitOutputs): return b.encodeV0(w) default: - return ErrUnknownBlobVersion + return ErrUnknownBlobType } } // decode deserializes the JusticeKit according to the version, returning an // error if the version is unknown. -func (b *JusticeKit) decode(r io.Reader, ver uint16) error { - switch ver { - case 0: +func (b *JusticeKit) decode(r io.Reader, blobType Type) error { + switch { + case blobType.Has(FlagCommitOutputs): return b.decodeV0(r) default: - return ErrUnknownBlobVersion + return ErrUnknownBlobType } } diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go index ba36f134..cac4af8f 100644 --- a/watchtower/blob/justice_kit_test.go +++ b/watchtower/blob/justice_kit_test.go @@ -38,8 +38,8 @@ func makeAddr(size int) []byte { type descriptorTest struct { name string - encVersion uint16 - decVersion uint16 + encVersion blob.Type + decVersion blob.Type sweepAddr []byte revPubKey blob.PubKey delayPubKey blob.PubKey @@ -52,11 +52,15 @@ type descriptorTest struct { decErr error } +var rewardAndCommitType = blob.TypeFromFlags( + blob.FlagReward, blob.FlagCommitOutputs, +) + var descriptorTests = []descriptorTest{ { name: "to-local only", - encVersion: 0, - decVersion: 0, + encVersion: blob.TypeDefault, + decVersion: blob.TypeDefault, sweepAddr: makeAddr(22), revPubKey: makePubKey(0), delayPubKey: makePubKey(1), @@ -65,8 +69,8 @@ var descriptorTests = []descriptorTest{ }, { name: "to-local and p2wkh", - encVersion: 0, - decVersion: 0, + encVersion: rewardAndCommitType, + decVersion: rewardAndCommitType, sweepAddr: makeAddr(22), revPubKey: makePubKey(0), delayPubKey: makePubKey(1), @@ -78,30 +82,30 @@ var descriptorTests = []descriptorTest{ }, { name: "unknown encrypt version", - encVersion: 1, - decVersion: 0, + encVersion: 0, + decVersion: blob.TypeDefault, sweepAddr: makeAddr(34), revPubKey: makePubKey(0), delayPubKey: makePubKey(1), csvDelay: 144, commitToLocalSig: makeSig(1), - encErr: blob.ErrUnknownBlobVersion, + encErr: blob.ErrUnknownBlobType, }, { name: "unknown decrypt version", - encVersion: 0, - decVersion: 1, + encVersion: blob.TypeDefault, + decVersion: 0, sweepAddr: makeAddr(34), revPubKey: makePubKey(0), delayPubKey: makePubKey(1), csvDelay: 144, commitToLocalSig: makeSig(1), - decErr: blob.ErrUnknownBlobVersion, + decErr: blob.ErrUnknownBlobType, }, { name: "sweep addr length zero", - encVersion: 0, - decVersion: 0, + encVersion: blob.TypeDefault, + decVersion: blob.TypeDefault, sweepAddr: makeAddr(0), revPubKey: makePubKey(0), delayPubKey: makePubKey(1), @@ -110,8 +114,8 @@ var descriptorTests = []descriptorTest{ }, { name: "sweep addr max size", - encVersion: 0, - decVersion: 0, + encVersion: blob.TypeDefault, + decVersion: blob.TypeDefault, sweepAddr: makeAddr(blob.MaxSweepAddrSize), revPubKey: makePubKey(0), delayPubKey: makePubKey(1), @@ -120,8 +124,8 @@ var descriptorTests = []descriptorTest{ }, { name: "sweep addr too long", - encVersion: 0, - decVersion: 0, + encVersion: blob.TypeDefault, + decVersion: blob.TypeDefault, sweepAddr: makeAddr(blob.MaxSweepAddrSize + 1), revPubKey: makePubKey(0), delayPubKey: makePubKey(1), diff --git a/watchtower/lookout/justice_descriptor_test.go b/watchtower/lookout/justice_descriptor_test.go index d0c49e59..1fe215cc 100644 --- a/watchtower/lookout/justice_descriptor_test.go +++ b/watchtower/lookout/justice_descriptor_test.go @@ -19,6 +19,7 @@ import ( "github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/lookout" "github.com/lightningnetwork/lnd/watchtower/wtdb" + "github.com/lightningnetwork/lnd/watchtower/wtpolicy" ) const csvDelay uint32 = 144 @@ -170,8 +171,10 @@ func TestJusticeDescriptor(t *testing.T) { // parameters that should be used in constructing the justice // transaction. sessionInfo := &wtdb.SessionInfo{ - SweepFeeRate: 2000, - RewardRate: 900000, + Policy: wtpolicy.Policy{ + SweepFeeRate: 2000, + RewardRate: 900000, + }, RewardAddress: makeAddrSlice(22), } diff --git a/watchtower/lookout/lookout.go b/watchtower/lookout/lookout.go index 546e03f2..556db2e8 100644 --- a/watchtower/lookout/lookout.go +++ b/watchtower/lookout/lookout.go @@ -210,7 +210,7 @@ func (l *Lookout) processEpoch(epoch *chainntnfs.BlockEpoch, // sweep the breached commitment outputs. justiceKit, err := blob.Decrypt( commitTxID[:], match.EncryptedBlob, - match.SessionInfo.Version, + match.SessionInfo.Policy.BlobType, ) if err != nil { // If the decryption fails, this implies either that the diff --git a/watchtower/lookout/lookout_test.go b/watchtower/lookout/lookout_test.go index 60681325..4232791d 100644 --- a/watchtower/lookout/lookout_test.go +++ b/watchtower/lookout/lookout_test.go @@ -15,6 +15,7 @@ import ( "github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/lookout" "github.com/lightningnetwork/lnd/watchtower/wtdb" + "github.com/lightningnetwork/lnd/watchtower/wtpolicy" ) type mockPunisher struct { @@ -86,15 +87,25 @@ func TestLookoutBreachMatching(t *testing.T) { t.Fatalf("unable to start watcher: %v", err) } + rewardAndCommitType := blob.TypeFromFlags( + blob.FlagReward, blob.FlagCommitOutputs, + ) + // Create two sessions, representing two distinct clients. sessionInfo1 := &wtdb.SessionInfo{ - ID: makeArray33(1), - MaxUpdates: 10, + ID: makeArray33(1), + Policy: wtpolicy.Policy{ + BlobType: rewardAndCommitType, + MaxUpdates: 10, + }, RewardAddress: makeAddrSlice(22), } sessionInfo2 := &wtdb.SessionInfo{ - ID: makeArray33(2), - MaxUpdates: 10, + ID: makeArray33(2), + Policy: wtpolicy.Policy{ + BlobType: rewardAndCommitType, + MaxUpdates: 10, + }, RewardAddress: makeAddrSlice(22), } @@ -137,13 +148,13 @@ func TestLookoutBreachMatching(t *testing.T) { } // 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 { t.Fatalf("unable to encrypt sweep detail 1: %v", err) } // 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 { t.Fatalf("unable to encrypt sweep detail 2: %v", err) } diff --git a/watchtower/wtdb/session_info.go b/watchtower/wtdb/session_info.go index 23775fe6..5ff78172 100644 --- a/watchtower/wtdb/session_info.go +++ b/watchtower/wtdb/session_info.go @@ -4,7 +4,7 @@ import ( "errors" "github.com/btcsuite/btcutil" - "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/watchtower/wtpolicy" ) var ( @@ -49,12 +49,8 @@ type SessionInfo struct { // ID is the remote public key of the watchtower client. ID SessionID - // Version specifies the plaintext blob encoding of all state updates. - Version uint16 - - // MaxUpdates is the total number of updates the client can send for - // this session. - MaxUpdates uint16 + // Policy holds the negotiated session parameters. + Policy wtpolicy.Policy // LastApplied the sequence number of the last successful state update. LastApplied uint16 @@ -62,14 +58,6 @@ type SessionInfo struct { // ClientLastApplied the last last-applied the client has echoed back. 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 // to if a sweep transaction confirms. RewardAddress []byte @@ -96,7 +84,7 @@ func (s *SessionInfo) AcceptUpdateSequence(seqNum, lastApplied uint16) error { return ErrLastAppliedReversion // Client update exceeds capacity of session. - case seqNum > s.MaxUpdates: + case seqNum > s.Policy.MaxUpdates: return ErrSessionConsumed // 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, txVSize int64) (btcutil.Amount, btcutil.Amount, error) { - txFee := s.SweepFeeRate.FeeForWeight(txVSize) + txFee := s.Policy.SweepFeeRate.FeeForWeight(txVSize) if txFee > totalAmt { 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 // 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 // TODO(conner): check dustiness diff --git a/watchtower/wtserver/server.go b/watchtower/wtserver/server.go index 24483d3a..3e2d0fa1 100644 --- a/watchtower/wtserver/server.go +++ b/watchtower/wtserver/server.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/watchtower/wtdb" + "github.com/lightningnetwork/lnd/watchtower/wtpolicy" "github.com/lightningnetwork/lnd/watchtower/wtwire" ) @@ -246,14 +247,14 @@ func (s *Server) handleClient(peer Peer) { log.Infof("Received CreateSession from %s, "+ "version=%d nupdates=%d rewardrate=%d "+ - "sweepfeerate=%d", id, msg.BlobVersion, + "sweepfeerate=%d", id, msg.BlobType, msg.MaxUpdates, msg.RewardRate, msg.SweepFeeRate) // Attempt to open a new session for this client. err := s.handleCreateSession(peer, &id, msg) if err != nil { - log.Errorf("unable to handle CreateSession "+ + log.Errorf("Unable to handle CreateSession "+ "from %s: %v", id, err) } @@ -327,7 +328,7 @@ func (s *Server) handleInit(localInit, remoteInit *wtwire.Init) error { // 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. func (s *Server) handleCreateSession(peer Peer, id *wtdb.SessionID, - init *wtwire.CreateSession) error { + req *wtwire.CreateSession) error { // TODO(conner): validate accept against policy @@ -375,11 +376,13 @@ func (s *Server) handleCreateSession(peer Peer, id *wtdb.SessionID, // address, and session id. info := wtdb.SessionInfo{ ID: *id, - Version: init.BlobVersion, - MaxUpdates: init.MaxUpdates, - RewardRate: init.RewardRate, - SweepFeeRate: init.SweepFeeRate, 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 diff --git a/watchtower/wtserver/server_test.go b/watchtower/wtserver/server_test.go index 3ca056af..37ad57f5 100644 --- a/watchtower/wtserver/server_test.go +++ b/watchtower/wtserver/server_test.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/wtdb" "github.com/lightningnetwork/lnd/watchtower/wtserver" "github.com/lightningnetwork/lnd/watchtower/wtwire" @@ -155,7 +156,7 @@ var createSessionTests = []createSessionTestCase{ lnwire.NewRawFeatureVector(), ), createMsg: &wtwire.CreateSession{ - BlobVersion: 0, + BlobType: blob.TypeDefault, MaxUpdates: 1000, RewardRate: 0, SweepFeeRate: 1, @@ -258,7 +259,7 @@ var stateUpdateTests = []stateUpdateTestCase{ GlobalFeatures: lnwire.NewRawFeatureVector(), }}, createMsg: &wtwire.CreateSession{ - BlobVersion: 0, + BlobType: blob.TypeDefault, MaxUpdates: 3, RewardRate: 0, SweepFeeRate: 1, @@ -287,7 +288,7 @@ var stateUpdateTests = []stateUpdateTestCase{ GlobalFeatures: lnwire.NewRawFeatureVector(), }}, createMsg: &wtwire.CreateSession{ - BlobVersion: 0, + BlobType: blob.TypeDefault, MaxUpdates: 4, RewardRate: 0, SweepFeeRate: 1, @@ -310,7 +311,7 @@ var stateUpdateTests = []stateUpdateTestCase{ GlobalFeatures: lnwire.NewRawFeatureVector(), }}, createMsg: &wtwire.CreateSession{ - BlobVersion: 0, + BlobType: blob.TypeDefault, MaxUpdates: 4, RewardRate: 0, SweepFeeRate: 1, @@ -337,7 +338,7 @@ var stateUpdateTests = []stateUpdateTestCase{ GlobalFeatures: lnwire.NewRawFeatureVector(), }}, createMsg: &wtwire.CreateSession{ - BlobVersion: 0, + BlobType: blob.TypeDefault, MaxUpdates: 4, RewardRate: 0, SweepFeeRate: 1, @@ -364,7 +365,7 @@ var stateUpdateTests = []stateUpdateTestCase{ GlobalFeatures: lnwire.NewRawFeatureVector(), }}, createMsg: &wtwire.CreateSession{ - BlobVersion: 0, + BlobType: blob.TypeDefault, MaxUpdates: 4, RewardRate: 0, SweepFeeRate: 1, @@ -393,7 +394,7 @@ var stateUpdateTests = []stateUpdateTestCase{ GlobalFeatures: lnwire.NewRawFeatureVector(), }}, createMsg: &wtwire.CreateSession{ - BlobVersion: 0, + BlobType: blob.TypeDefault, MaxUpdates: 4, RewardRate: 0, SweepFeeRate: 1, @@ -421,7 +422,7 @@ var stateUpdateTests = []stateUpdateTestCase{ GlobalFeatures: lnwire.NewRawFeatureVector(), }}, createMsg: &wtwire.CreateSession{ - BlobVersion: 0, + BlobType: blob.TypeDefault, MaxUpdates: 3, RewardRate: 0, SweepFeeRate: 1, @@ -450,7 +451,7 @@ var stateUpdateTests = []stateUpdateTestCase{ GlobalFeatures: lnwire.NewRawFeatureVector(), }}, createMsg: &wtwire.CreateSession{ - BlobVersion: 0, + BlobType: blob.TypeDefault, MaxUpdates: 3, RewardRate: 0, SweepFeeRate: 1, diff --git a/watchtower/wtwire/create_session.go b/watchtower/wtwire/create_session.go index 8ee2c906..6067d25c 100644 --- a/watchtower/wtwire/create_session.go +++ b/watchtower/wtwire/create_session.go @@ -4,6 +4,7 @@ import ( "io" "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 @@ -11,9 +12,9 @@ import ( // An update is consumed by uploading an encrypted blob that contains // information required to sweep a revoked commitment transaction. type CreateSession struct { - // BlobVersion specifies the blob format that must be used by all - // updates sent under the session key used to negotiate this session. - BlobVersion uint16 + // 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. @@ -41,7 +42,7 @@ var _ Message = (*CreateSession)(nil) // This is part of the wtwire.Message interface. func (m *CreateSession) Decode(r io.Reader, pver uint32) error { return ReadElements(r, - &m.BlobVersion, + &m.BlobType, &m.MaxUpdates, &m.RewardRate, &m.SweepFeeRate, @@ -54,7 +55,7 @@ func (m *CreateSession) Decode(r io.Reader, pver uint32) error { // This is part of the wtwire.Message interface. func (m *CreateSession) Encode(w io.Writer, pver uint32) error { return WriteElements(w, - m.BlobVersion, + m.BlobType, m.MaxUpdates, m.RewardRate, m.SweepFeeRate, diff --git a/watchtower/wtwire/wtwire.go b/watchtower/wtwire/wtwire.go index ce3a9a63..2582c704 100644 --- a/watchtower/wtwire/wtwire.go +++ b/watchtower/wtwire/wtwire.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/watchtower/blob" ) // 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 } + case blob.Type: + var b [2]byte + binary.BigEndian.PutUint16(b[:], uint16(e)) + if _, err := w.Write(b[:]); err != nil { + return err + } + case uint32: var b [4]byte binary.BigEndian.PutUint32(b[:], e) @@ -127,6 +135,13 @@ func ReadElement(r io.Reader, element interface{}) error { } *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: var b [4]byte if _, err := io.ReadFull(r, b[:]); err != nil {