package blocks
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
. "github.com/tendermint/tendermint/binary"
|
|
. "github.com/tendermint/tendermint/common"
|
|
db_ "github.com/tendermint/tendermint/db"
|
|
"github.com/tendermint/tendermint/p2p"
|
|
)
|
|
|
|
var dbKeyState = []byte("state")
|
|
|
|
const (
|
|
blocksInfoCh = byte(0x10) // For requests & cancellations
|
|
blocksDataCh = byte(0x11) // For data
|
|
|
|
msgTypeUnknown = Byte(0x00)
|
|
msgTypeState = Byte(0x01)
|
|
msgTypeRequest = Byte(0x02)
|
|
msgTypeData = Byte(0x03)
|
|
|
|
maxRequestsPerPeer = 2 // Maximum number of outstanding requests from peer.
|
|
maxRequestsPerData = 2 // Maximum number of outstanding requests of some data.
|
|
maxRequestAheadBlock = 5 // Maximum number of blocks to request ahead of current verified. Must be >= 1
|
|
|
|
defaultRequestTimeoutS =
|
|
timeoutRepeatTimerMS = 1000 // Handle timed out requests periodically
|
|
)
|
|
|
|
/*
|
|
TODO: keep a heap of dataRequests * their corresponding timeouts.
|
|
timeout dataRequests and update the peerState,
|
|
TODO: need to keep track of progress, blocks are too large. or we need to chop into chunks.
|
|
TODO: need to validate blocks. :/
|
|
TODO: actually save the block.
|
|
*/
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
const (
|
|
dataTypeBlock = byte(0x00)
|
|
// TODO: allow for more types, such as specific transactions
|
|
)
|
|
|
|
type dataKey struct {
|
|
dataType byte
|
|
height uint64
|
|
}
|
|
|
|
func newDataKey(dataType byte, height uint64) dataKey {
|
|
return dataKey{dataType, height}
|
|
}
|
|
|
|
func readDataKey(r io.Reader) dataKey {
|
|
return dataKey{
|
|
dataType: ReadByte(r),
|
|
height: ReadUInt64(r),
|
|
}
|
|
}
|
|
|
|
func (dk dataKey) WriteTo(w io.Writer) (n int64, err error) {
|
|
n, err = WriteTo(dk.dataType, w, n, err)
|
|
n, err = WriteTo(dk.height, w, n, err)
|
|
return
|
|
}
|
|
|
|
func (dk dataKey) String() string {
|
|
switch dataType {
|
|
case dataTypeBlock:
|
|
return dataKeyfmt.Sprintf("B%v", height)
|
|
default:
|
|
Panicf("Unknown datatype %X", dataType)
|
|
return "" // should not happen
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
type BlockManager struct {
|
|
db *db_.LevelDB
|
|
sw *p2p.Switch
|
|
swEvents chan interface{}
|
|
state *blockManagerState
|
|
timeoutTimer *RepeatTimer
|
|
quit chan struct{}
|
|
started uint32
|
|
stopped uint32
|
|
}
|
|
|
|
func NewBlockManager(sw *p2p.Switch, db *db_.LevelDB) *BlockManager {
|
|
swEvents := make(chan interface{})
|
|
sw.AddEventListener("BlockManager.swEvents", swEvents)
|
|
bm := &BlockManager{
|
|
db: db,
|
|
sw: sw,
|
|
swEvents: swEvents,
|
|
state: newBlockManagerState(),
|
|
timeoutTimer: NewRepeatTimer(timeoutRepeatTimerMS * time.Second),
|
|
quit: make(chan struct{}),
|
|
}
|
|
bm.loadState()
|
|
return bm
|
|
}
|
|
|
|
func (bm *BlockManager) Start() {
|
|
if atomic.CompareAndSwapUint32(&bm.started, 0, 1) {
|
|
log.Info("Starting BlockManager")
|
|
go bm.switchEventsHandler()
|
|
go bm.blocksInfoHandler()
|
|
go bm.blocksDataHandler()
|
|
go bm.requestTimeoutHandler()
|
|
}
|
|
}
|
|
|
|
func (bm *BlockManager) Stop() {
|
|
if atomic.CompareAndSwapUint32(&bm.stopped, 0, 1) {
|
|
log.Info("Stopping BlockManager")
|
|
close(bm.quit)
|
|
close(bm.swEvents)
|
|
}
|
|
}
|
|
|
|
// NOTE: assumes that data is already validated.
|
|
// "request" is optional, it's the request response that supplied
|
|
// the data.
|
|
func (bm *BlockManager) StoreBlock(block *Block, origin *dataRequest) {
|
|
dataKey := newDataKey(dataTypeBlock, uint64(block.Header.Height))
|
|
|
|
// XXX actually save the block.
|
|
|
|
canceled, newHeight := bm.state.didGetDataFromPeer(dataKey, origin.peer)
|
|
|
|
// Notify peers that the request has been canceled.
|
|
for _, request := range canceled {
|
|
msg := &requestMessage{
|
|
key: dataKey,
|
|
type_: requestTypeCanceled,
|
|
}
|
|
tm := p2p.TypedMessage{msgTypeRequest, msg}
|
|
request.peer.TrySend(blocksInfoCh, tm.Bytes())
|
|
}
|
|
|
|
// If we have new data that extends our contiguous range, then announce it.
|
|
if newHeight {
|
|
bm.sw.Broadcast(blocksInfoCh, bm.state.makeStateMessage())
|
|
}
|
|
}
|
|
|
|
func (bm *BlockManager) LoadBlock(height uint64) *Block {
|
|
panic("not yet implemented")
|
|
}
|
|
|
|
// Handle peer new/done events
|
|
func (bm *BlockManager) switchEventsHandler() {
|
|
for {
|
|
swEvent, ok := <-bm.swEvents
|
|
if !ok {
|
|
break
|
|
}
|
|
switch swEvent.(type) {
|
|
case p2p.SwitchEventNewPeer:
|
|
event := swEvent.(p2p.SwitchEventNewPeer)
|
|
// Create peerState for event.Peer
|
|
bm.state.createEntryForPeer(event.Peer)
|
|
// Share our state with event.Peer
|
|
msg := &stateMessage{
|
|
lastBlockHeight: UInt64(bm.state.lastBlockHeight),
|
|
}
|
|
tm := p2p.TypedMessage{msgTypeRequest, msg}
|
|
event.Peer.TrySend(blocksInfoCh, tm.Bytes())
|
|
case p2p.SwitchEventDonePeer:
|
|
event := swEvent.(p2p.SwitchEventDonePeer)
|
|
// Delete peerState for event.Peer
|
|
bm.state.deleteEntryForPeer(event.Peer)
|
|
default:
|
|
log.Warning("Unhandled switch event type")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle requests/cancellations from the blocksInfo channel
|
|
func (bm *BlockManager) blocksInfoHandler() {
|
|
for {
|
|
inMsg, ok := bm.sw.Receive(blocksInfoCh)
|
|
if !ok {
|
|
break // Client has stopped
|
|
}
|
|
|
|
msg := decodeMessage(inMsg.Bytes)
|
|
log.Info("blocksInfoHandler received %v", msg)
|
|
|
|
switch msg.(type) {
|
|
case *stateMessage:
|
|
m := msg.(*stateMessage)
|
|
peerState := bm.getPeerState(inMsg.MConn.Peer)
|
|
if peerState == nil {
|
|
continue // peer has since been disconnected.
|
|
}
|
|
newDataTypes := peerState.applyStateMessage(m)
|
|
// Consider requesting data.
|
|
// Does the peer claim to have something we want?
|
|
FOR_LOOP:
|
|
for _, newDataType := range newDataTypes {
|
|
// Are we already requesting too much data from peer?
|
|
if !peerState.canRequestMore() {
|
|
break FOR_LOOP
|
|
}
|
|
for _, wantedKey := range bm.state.nextWantedKeysForType(newDataType) {
|
|
if !peerState.hasData(wantedKey) {
|
|
break FOR_LOOP
|
|
}
|
|
// Request wantedKey from peer.
|
|
msg := &requestMessage{
|
|
key: dataKey,
|
|
type_: requestTypeFetch,
|
|
}
|
|
tm := p2p.TypedMessage{msgTypeRequest, msg}
|
|
sent := inMsg.MConn.Peer.TrySend(blocksInfoCh, tm.Bytes())
|
|
if sent {
|
|
// Log the request
|
|
request := &dataRequest{
|
|
peer: inMsg.MConn.Peer,
|
|
key: wantedKey,
|
|
time: time.Now(),
|
|
timeout: time.Now().Add(defaultRequestTimeout
|
|
}
|
|
bm.state.addDataRequest(request)
|
|
}
|
|
}
|
|
}
|
|
case *requestMessage:
|
|
m := msg.(*requestMessage)
|
|
switch m.type_ {
|
|
case requestTypeFetch:
|
|
// TODO: prevent abuse.
|
|
if !inMsg.MConn.Peer.CanSend(blocksDataCh) {
|
|
msg := &requestMessage{
|
|
key: dataKey,
|
|
type_: requestTypeTryAgain,
|
|
}
|
|
tm := p2p.TypedMessage{msgTypeRequest, msg}
|
|
sent := inMsg.MConn.Peer.TrySend(blocksInfoCh, tm.Bytes())
|
|
} else {
|
|
// If we don't have it, log and ignore.
|
|
block := bm.LoadBlock(m.key.height)
|
|
if block == nil {
|
|
log.Warning("Peer %v asked for nonexistant block %v", inMsg.MConn.Peer, m.key)
|
|
}
|
|
// Send the data.
|
|
msg := &dataMessage{
|
|
key: dataKey,
|
|
bytes: BinaryBytes(block),
|
|
}
|
|
tm := p2p.TypedMessage{msgTypeData, msg}
|
|
inMsg.MConn.Peer.TrySend(blocksDataCh, tm.Bytes())
|
|
}
|
|
case requestTypeCanceled:
|
|
// TODO: handle
|
|
// This requires modifying mconnection to keep track of item keys.
|
|
case requestTypeTryAgain:
|
|
// TODO: handle
|
|
default:
|
|
log.Warning("Invalid request: %v", m)
|
|
// Ignore.
|
|
}
|
|
default:
|
|
// should not happen
|
|
Panicf("Unknown message %v", msg)
|
|
// bm.sw.StopPeerForError(inMsg.MConn.Peer, errInvalidMessage)
|
|
}
|
|
}
|
|
|
|
// Cleanup
|
|
}
|
|
|
|
// Handle receiving data from the blocksData channel
|
|
func (bm *BlockManager) blocksDataHandler() {
|
|
for {
|
|
inMsg, ok := bm.sw.Receive(blocksDataCh)
|
|
if !ok {
|
|
break // Client has stopped
|
|
}
|
|
|
|
msg := decodeMessage(inMsg.Bytes)
|
|
log.Info("blocksDataHandler received %v", msg)
|
|
|
|
switch msg.(type) {
|
|
case *dataMessage:
|
|
// See if we want the data.
|
|
// Validate data.
|
|
// Add to db.
|
|
// Update state & broadcast as necessary.
|
|
default:
|
|
// Ignore unknown message
|
|
// bm.sw.StopPeerForError(inMsg.MConn.Peer, errInvalidMessage)
|
|
}
|
|
}
|
|
|
|
// Cleanup
|
|
}
|
|
|
|
// Handle timed out requests by requesting from others.
|
|
func (bm *BlockManager) requestTimeoutHandler() {
|
|
for {
|
|
_, ok := <-bm.timeoutTimer
|
|
if !ok {
|
|
break
|
|
}
|
|
// Iterate over requests by time and handle timed out requests.
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// blockManagerState keeps track of which block parts are stored locally.
|
|
// It's also persisted via JSON in the db.
|
|
type blockManagerState struct {
|
|
mtx sync.Mutex
|
|
lastBlockHeight uint64 // Last contiguous header height
|
|
otherBlockHeights map[uint64]struct{}
|
|
requestsByKey map[dataKey][]*dataRequest
|
|
requestsByTimeout *Heap // Could be a linkedlist, but more flexible.
|
|
peerStates map[string]*peerState
|
|
}
|
|
|
|
func newBlockManagerState() *blockManagerState {
|
|
return &blockManagerState{
|
|
requestsByKey: make(map[dataKey][]*dataRequest),
|
|
requestsByTimeout: NewHeap(),
|
|
peerStates: make(map[string]*peerState),
|
|
}
|
|
}
|
|
|
|
type blockManagerStateJSON struct {
|
|
LastBlockHeight uint64 // Last contiguous header height
|
|
OtherBlockHeights map[uint64]struct{}
|
|
}
|
|
|
|
func (bms *BlockManagerState) loadState(db _db.LevelDB) {
|
|
bms.mtx.Lock()
|
|
defer bms.mtx.Unlock()
|
|
stateBytes := db.Get(dbKeyState)
|
|
if stateBytes == nil {
|
|
log.Info("New BlockManager with no state")
|
|
} else {
|
|
bmsJSON := &blockManagerStateJSON{}
|
|
err := json.Unmarshal(stateBytes, bmsJSON)
|
|
if err != nil {
|
|
Panicf("Could not unmarshal state bytes: %X", stateBytes)
|
|
}
|
|
bms.lastBlockHeight = bmsJSON.LastBlockHeight
|
|
bms.otherBlockHeights = bmsJSON.OtherBlockHeights
|
|
}
|
|
}
|
|
|
|
func (bms *BlockManagerState) saveState(db _db.LevelDB) {
|
|
bms.mtx.Lock()
|
|
defer bms.mtx.Unlock()
|
|
bmsJSON := &blockManagerStateJSON{
|
|
LastBlockHeight: bms.lastBlockHeight,
|
|
OtherBlockHeights: bms.otherBlockHeights,
|
|
}
|
|
stateBytes, err := json.Marshal(bmsJSON)
|
|
if err != nil {
|
|
panic("Could not marshal state bytes")
|
|
}
|
|
db.Set(dbKeyState, stateBytes)
|
|
}
|
|
|
|
func (bms *blockManagerState) makeStateMessage() *stateMessage {
|
|
bms.mtx.Lock()
|
|
defer bms.mtx.Unlock()
|
|
return &stateMessage{
|
|
lastBlockHeight: UInt64(bms.lastBlockHeight),
|
|
}
|
|
}
|
|
|
|
func (bms *blockManagerState) createEntryForPeer(peer *peer) {
|
|
bms.mtx.Lock()
|
|
defer bms.mtx.Unlock()
|
|
bms.peerStates[peer.Key] = &peerState{peer: peer}
|
|
}
|
|
|
|
func (bms *blockManagerState) deleteEntryForPeer(peer *peer) {
|
|
bms.mtx.Lock()
|
|
defer bms.mtx.Unlock()
|
|
delete(bms.peerStates, peer.Key)
|
|
}
|
|
|
|
func (bms *blockManagerState) getPeerState(peer *Peer) {
|
|
bms.mtx.Lock()
|
|
defer bms.mtx.Unlock()
|
|
return bms.peerStates[peer.Key]
|
|
}
|
|
|
|
func (bms *blockManagerState) addDataRequest(newRequest *dataRequest) {
|
|
ps.mtx.Lock()
|
|
bms.requestsByKey[newRequest.key] = append(bms.requestsByKey[newRequest.key], newRequest)
|
|
bms.requestsByTimeout.Push(newRequest) // XXX
|
|
peerState, ok := bms.peerStates[newRequest.peer.Key]
|
|
ps.mtx.Unlock()
|
|
if ok {
|
|
peerState.addDataRequest(newRequest)
|
|
}
|
|
}
|
|
|
|
func (bms *blockManagerState) didGetDataFromPeer(key dataKey, peer *p2p.Peer) (canceled []*dataRequest, newHeight bool) {
|
|
bms.mtx.Lock()
|
|
defer bms.mtx.Unlock()
|
|
if key.dataType != dataTypeBlock {
|
|
Panicf("Unknown datatype %X", key.dataType)
|
|
}
|
|
// Adjust lastBlockHeight/otherBlockHeights.
|
|
height := key.height
|
|
if bms.lastBlockHeight == height-1 {
|
|
bms.lastBlockHeight = height
|
|
height++
|
|
for _, ok := bms.otherBlockHeights[height]; ok; {
|
|
delete(bms.otherBlockHeights, height)
|
|
bms.lastBlockHeight = height
|
|
height++
|
|
}
|
|
newHeight = true
|
|
}
|
|
// Remove dataRequests
|
|
requests := bms.requestsByKey[key]
|
|
for _, request := range requests {
|
|
peerState, ok := bms.peerStates[peer.Key]
|
|
if ok {
|
|
peerState.removeDataRequest(request)
|
|
}
|
|
if request.peer == peer {
|
|
continue
|
|
}
|
|
canceled = append(canceled, request)
|
|
}
|
|
delete(bms.requestsByKey, key)
|
|
|
|
return canceled, newHeight
|
|
}
|
|
|
|
// Returns at most maxRequestAheadBlock dataKeys that we don't yet have &
|
|
// aren't already requesting from maxRequestsPerData peers.
|
|
func (bms *blockManagerState) nextWantedKeysForType(dataType byte) []dataKey {
|
|
bms.mtx.Lock()
|
|
defer bms.mtx.Unlock()
|
|
var keys []dataKey
|
|
switch dataType {
|
|
case dataTypeBlock:
|
|
for h := bms.lastBlockHeight + 1; h <= bms.lastBlockHeight+maxRequestAheadBlock; h++ {
|
|
if _, ok := bms.otherBlockHeights[h]; !ok {
|
|
key := newDataKey(dataTypeBlock, h)
|
|
if len(bms.requestsByKey[key]) < maxRequestsPerData {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
}
|
|
return keys
|
|
default:
|
|
Panicf("Unknown datatype %X", dataType)
|
|
return
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// dataRequest keeps track of each request for a given peice of data & peer.
|
|
type dataRequest struct {
|
|
peer *p2p.Peer
|
|
key dataKey
|
|
time time.Time
|
|
timeout time.Time
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
type peerState struct {
|
|
mtx sync.Mutex
|
|
peer *Peer
|
|
lastBlockHeight uint64 // Last contiguous header height
|
|
requests []*dataRequest // Active requests
|
|
// XXX we need to
|
|
}
|
|
|
|
// Returns which dataTypes are new as declared by stateMessage.
|
|
func (ps *peerState) applyStateMessage(msg *stateMessage) []byte {
|
|
ps.mtx.Lock()
|
|
defer ps.mtx.Unlock()
|
|
var newTypes []byte
|
|
if uint64(msg.lastBlockHeight) > ps.lastBlockHeight {
|
|
newTypes = append(newTypes, dataTypeBlock)
|
|
ps.lastBlockHeight = uint64(msg.lastBlockHeight)
|
|
} else {
|
|
log.Info("Strange, peer declares a regression of %X", dataTypeBlock)
|
|
}
|
|
return newTypes
|
|
}
|
|
|
|
func (ps *peerState) hasData(key dataKey) bool {
|
|
ps.mtx.Lock()
|
|
defer ps.mtx.Unlock()
|
|
switch key.dataType {
|
|
case dataTypeBlock:
|
|
return key.height <= ps.lastBlockHeight
|
|
default:
|
|
Panicf("Unknown datatype %X", dataType)
|
|
return false // should not happen
|
|
}
|
|
}
|
|
|
|
func (ps *peerState) addDataRequest(newRequest *dataRequest) {
|
|
ps.mtx.Lock()
|
|
defer ps.mtx.Unlock()
|
|
for _, request := range ps.requests {
|
|
if request.key == newRequest.key {
|
|
return
|
|
}
|
|
}
|
|
ps.requests = append(ps.requests, newRequest)
|
|
return newRequest
|
|
}
|
|
|
|
func (ps *peerState) remoteDataRequest(key dataKey) bool {
|
|
ps.mtx.Lock()
|
|
defer ps.mtx.Unlock()
|
|
filtered := []*dataRequest{}
|
|
removed := false
|
|
for _, request := range ps.requests {
|
|
if request.key == key {
|
|
removed = true
|
|
} else {
|
|
filtered = append(filtered, request)
|
|
}
|
|
}
|
|
ps.requests = filtered
|
|
return removed
|
|
}
|
|
|
|
func (ps *peerState) canRequestMore() bool {
|
|
ps.mtx.Lock()
|
|
defer ps.mtx.Unlock()
|
|
return len(ps.requests) < maxRequestsPerPeer
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/* Messages */
|
|
|
|
// TODO: check for unnecessary extra bytes at the end.
|
|
func decodeMessage(bz ByteSlice) (msg interface{}) {
|
|
// log.Debug("decoding msg bytes: %X", bz)
|
|
switch Byte(bz[0]) {
|
|
case msgTypeState:
|
|
return &stateMessage{}
|
|
case msgTypeRequest:
|
|
return readRequestMessage(bytes.NewReader(bz[1:]))
|
|
case msgTypeData:
|
|
return readDataMessage(bytes.NewReader(bz[1:]))
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/*
|
|
A stateMessage declares what (contiguous) blocks & headers are known.
|
|
*/
|
|
type stateMessage struct {
|
|
lastBlockHeight UInt64 // Last contiguous block height
|
|
}
|
|
|
|
func readStateMessage(r io.Reader) *stateMessage {
|
|
return &stateMessage{
|
|
lastBlockHeight: ReadUInt64(r),
|
|
}
|
|
}
|
|
|
|
func (m *stateMessage) WriteTo(w io.Writer) (n int64, err error) {
|
|
n, err = WriteTo(msgTypeState, w, n, err)
|
|
n, err = WriteTo(m.lastBlockHeight, w, n, err)
|
|
return
|
|
}
|
|
|
|
func (m *stateMessage) String() string {
|
|
return fmt.Sprintf("[State B:%v]", m.lastBlockHeight)
|
|
}
|
|
|
|
/*
|
|
A requestMessage requests a block and/or header at a given height.
|
|
*/
|
|
type requestMessage struct {
|
|
key dataKey
|
|
type_ Byte
|
|
}
|
|
|
|
const (
|
|
requestTypeFetch = Byte(0x01)
|
|
requestTypeCanceled = Byte(0x02)
|
|
requestTypeTryAgain = Byte(0x03)
|
|
)
|
|
|
|
func readRequestMessage(r io.Reader) *requestMessage {
|
|
return &requestMessage{
|
|
key: ReadDataKey(r),
|
|
type_: ReadByte(r),
|
|
}
|
|
}
|
|
|
|
func (m *requestMessage) WriteTo(w io.Writer) (n int64, err error) {
|
|
n, err = WriteTo(msgTypeRequest, w, n, err)
|
|
n, err = WriteTo(m.key, w, n, err)
|
|
n, err = WriteTo(m.type_, w, n, err)
|
|
return
|
|
}
|
|
|
|
func (m *requestMessage) String() string {
|
|
switch m.type_ {
|
|
case requestTypeByte:
|
|
return fmt.Sprintf("[Request(fetch) %v]", m.key)
|
|
case requestTypeCanceled:
|
|
return fmt.Sprintf("[Request(canceled) %v]", m.key)
|
|
case requestTypeTryAgain:
|
|
return fmt.Sprintf("[Request(tryagain) %v]", m.key)
|
|
default:
|
|
return fmt.Sprintf("[Request(invalid) %v]", m.key)
|
|
}
|
|
}
|
|
|
|
/*
|
|
A dataMessage contains block data, maybe requested.
|
|
The data can be a Validation, Txs, or whole Block object.
|
|
*/
|
|
type dataMessage struct {
|
|
key dataKey
|
|
bytes ByteSlice
|
|
}
|
|
|
|
func readDataMessage(r io.Reader) *dataMessage {
|
|
return &dataMessage{
|
|
key: readDataKey(r),
|
|
bytes: readByteSlice(r),
|
|
}
|
|
}
|
|
|
|
func (m *dataMessage) WriteTo(w io.Writer) (n int64, err error) {
|
|
n, err = WriteTo(msgTypeData, w, n, err)
|
|
n, err = WriteTo(m.key, w, n, err)
|
|
n, err = WriteTo(m.bytes, w, n, err)
|
|
return
|
|
}
|
|
|
|
func (m *dataMessage) String() string {
|
|
return fmt.Sprintf("[Data %v]", m.key)
|
|
}
|