api.go raw

   1  // Package api is the vocabulary between the page and the wallet worker.
   2  //
   3  // It is the same discipline as the chain-data channel on the other side of the worker: binary
   4  // frames, an id on every request, a cancel, and a close that means the far end died. The
   5  // payloads are binary because an address is a string, a height is four bytes, and a phrase is
   6  // a secret that has no business being base64'd into a log.
   7  //
   8  // The page encodes and decodes these frames from JavaScript, so the format is fixed and
   9  // boring: an 18-byte header and a payload, little-endian, every string length-prefixed.
  10  package api
  11  
  12  import (
  13  	"encoding/binary"
  14  	"errors"
  15  	"fmt"
  16  )
  17  
  18  // The frame layout. The header is fixed so a decoder can reject anything it does not
  19  // understand before it allocates.
  20  const (
  21  	// Magic is "TWA1" read little-endian.
  22  	Magic uint32 = 0x31415754
  23  	// Version is the frame format.
  24  	Version uint8 = 1
  25  	// HeaderLen is magic, version, kind, method, status, id and length.
  26  	HeaderLen = 18
  27  	// MaxPayload bounds one frame. A phrase is 24 words; an address list is a few hundred
  28  	// bytes. Anything larger is a bug or an attack.
  29  	MaxPayload = 1 << 20
  30  )
  31  
  32  // ErrBadFrame is a frame this version cannot read.
  33  var ErrBadFrame = errors.New("api: malformed frame")
  34  
  35  // Kind is what a frame is.
  36  type Kind uint8
  37  
  38  // The kinds. A request expects one result; a cancel withdraws one; a close ends everything.
  39  const (
  40  	// KindRequest asks for one method to run.
  41  	KindRequest Kind = 1
  42  	// KindResult answers one request, with the request's id.
  43  	KindResult Kind = 2
  44  	// KindCancel withdraws a request.
  45  	KindCancel Kind = 3
  46  	// KindClose ends the conversation.
  47  	KindClose Kind = 4
  48  )
  49  
  50  // String renders a kind.
  51  func (k Kind) String() string {
  52  	switch k {
  53  	case KindRequest:
  54  		return "request"
  55  	case KindResult:
  56  		return "result"
  57  	case KindCancel:
  58  		return "cancel"
  59  	case KindClose:
  60  		return "close"
  61  	}
  62  
  63  	return "kind(" + itoa(uint32(k)) + ")"
  64  }
  65  
  66  // Method is what a request asks for.
  67  type Method uint16
  68  
  69  // The methods.
  70  const (
  71  	// MethodStatus reports what the wallet knows about a chain.
  72  	MethodStatus Method = 1
  73  	// MethodCreate makes a new wallet and returns the words once.
  74  	MethodCreate Method = 2
  75  	// MethodRestore makes a wallet from words.
  76  	MethodRestore Method = 3
  77  	// MethodUnlock opens the wallet with the passphrase.
  78  	MethodUnlock Method = 4
  79  	// MethodLock forgets the unlocked wallet.
  80  	MethodLock Method = 5
  81  	// MethodAddresses derives addresses.
  82  	MethodAddresses Method = 6
  83  	// MethodSync walks the chain and records matches.
  84  	MethodSync Method = 7
  85  	// MethodMatches lists the recorded heights.
  86  	MethodMatches Method = 8
  87  	// MethodChains lists the chains the wallet drives.
  88  	MethodChains Method = 9
  89  	// MethodUnspent lists the coins the wallet can spend.
  90  	MethodUnspent Method = 10
  91  )
  92  
  93  // String renders a method.
  94  func (m Method) String() string {
  95  	switch m {
  96  	case MethodStatus:
  97  		return "status"
  98  	case MethodCreate:
  99  		return "create"
 100  	case MethodRestore:
 101  		return "restore"
 102  	case MethodUnlock:
 103  		return "unlock"
 104  	case MethodLock:
 105  		return "lock"
 106  	case MethodAddresses:
 107  		return "addresses"
 108  	case MethodSync:
 109  		return "sync"
 110  	case MethodMatches:
 111  		return "matches"
 112  	case MethodChains:
 113  		return "chains"
 114  	case MethodUnspent:
 115  		return "unspent"
 116  	}
 117  
 118  	return "method(" + itoa(uint32(m)) + ")"
 119  }
 120  
 121  // Status is how an answer went.
 122  type Status uint16
 123  
 124  // The statuses.
 125  const (
 126  	// StatusOK is a completed request.
 127  	StatusOK Status = 0
 128  	// StatusFailed is a refusal with a message.
 129  	StatusFailed Status = 1
 130  	// StatusCanceled is a request the caller withdrew.
 131  	StatusCanceled Status = 2
 132  	// StatusUnknownMethod is a method this build does not have.
 133  	StatusUnknownMethod Status = 3
 134  	// StatusMalformed is a payload that did not decode, or a lock that is wrong.
 135  	StatusMalformed Status = 4
 136  	// StatusClosed is an answer to a request whose channel ended.
 137  	StatusClosed Status = 5
 138  )
 139  
 140  // String renders a status.
 141  func (s Status) String() string {
 142  	switch s {
 143  	case StatusOK:
 144  		return "ok"
 145  	case StatusFailed:
 146  		return "failed"
 147  	case StatusCanceled:
 148  		return "canceled"
 149  	case StatusUnknownMethod:
 150  		return "unknown method"
 151  	case StatusMalformed:
 152  		return "malformed"
 153  	case StatusClosed:
 154  		return "closed"
 155  	}
 156  
 157  	return "status(" + itoa(uint32(s)) + ")"
 158  }
 159  
 160  // Frame is one message.
 161  type Frame struct {
 162  	// Kind is what the frame is.
 163  	Kind Kind
 164  	// Method is what it asks for, on a request.
 165  	Method Method
 166  	// Status is how it went, on a result.
 167  	Status Status
 168  	// ID echoes the request on every answer.
 169  	ID uint32
 170  	// Payload is the method's bytes.
 171  	Payload []byte
 172  }
 173  
 174  // Encode renders a frame.
 175  func Encode(frame Frame) ([]byte, error) {
 176  	if len(frame.Payload) > MaxPayload {
 177  		return nil, fmt.Errorf("%w: a %d-byte payload", ErrBadFrame, len(frame.Payload))
 178  	}
 179  
 180  	out := make([]byte, HeaderLen, HeaderLen+len(frame.Payload))
 181  
 182  	binary.LittleEndian.PutUint32(out[0:4], Magic)
 183  	out[4] = Version
 184  	out[5] = byte(frame.Kind)
 185  	binary.LittleEndian.PutUint16(out[6:8], uint16(frame.Method))
 186  	binary.LittleEndian.PutUint16(out[8:10], uint16(frame.Status))
 187  	binary.LittleEndian.PutUint32(out[10:14], frame.ID)
 188  	binary.LittleEndian.PutUint32(out[14:18], uint32(len(frame.Payload)))
 189  	out = append(out, frame.Payload...)
 190  
 191  	return out, nil
 192  }
 193  
 194  // Decode reads a frame.
 195  func Decode(raw []byte) (Frame, error) {
 196  	if len(raw) < HeaderLen {
 197  		return Frame{}, fmt.Errorf("%w: %d bytes is shorter than a header", ErrBadFrame, len(raw))
 198  	}
 199  
 200  	if magic := binary.LittleEndian.Uint32(raw[0:4]); magic != Magic {
 201  		return Frame{}, fmt.Errorf("%w: magic %#x", ErrBadFrame, magic)
 202  	}
 203  
 204  	if raw[4] != Version {
 205  		return Frame{}, fmt.Errorf("%w: version %d", ErrBadFrame, raw[4])
 206  	}
 207  
 208  	length := binary.LittleEndian.Uint32(raw[14:18])
 209  
 210  	if length > MaxPayload {
 211  		return Frame{}, fmt.Errorf("%w: a %d-byte payload", ErrBadFrame, length)
 212  	}
 213  
 214  	if int(length) != len(raw)-HeaderLen {
 215  		return Frame{}, fmt.Errorf("%w: the header says %d bytes, the frame carries %d",
 216  			ErrBadFrame, length, len(raw)-HeaderLen)
 217  	}
 218  
 219  	return Frame{
 220  		Kind:    Kind(raw[5]),
 221  		Method:  Method(binary.LittleEndian.Uint16(raw[6:8])),
 222  		Status:  Status(binary.LittleEndian.Uint16(raw[8:10])),
 223  		ID:      binary.LittleEndian.Uint32(raw[10:14]),
 224  		Payload: raw[HeaderLen:],
 225  	}, nil
 226  }
 227  
 228  // Message is a result the worker can hand back without an error type: a status and a text.
 229  func Message(status Status, id uint32, text string) Frame {
 230  	return Frame{Kind: KindResult, Status: status, ID: id, Payload: PutText(nil, text)}
 231  }
 232  
 233  // Payload codecs. Each appends to a buffer so a caller can build one payload from several
 234  // fields without a wrapper type.
 235  func PutText(buffer []byte, text string) []byte {
 236  	buffer = binary.LittleEndian.AppendUint16(buffer, uint16(len(text)))
 237  
 238  	return append(buffer, text...)
 239  }
 240  
 241  // ReadText reads a text.
 242  func ReadText(payload []byte) (string, []byte, error) {
 243  	if len(payload) < 2 {
 244  		return "", nil, fmt.Errorf("%w: a text length", ErrBadFrame)
 245  	}
 246  
 247  	length := int(binary.LittleEndian.Uint16(payload[0:2]))
 248  
 249  	if len(payload) < 2+length {
 250  		return "", nil, fmt.Errorf("%w: a %d-byte text in %d bytes", ErrBadFrame, length, len(payload))
 251  	}
 252  
 253  	return string(payload[2 : 2+length]), payload[2+length:], nil
 254  }
 255  
 256  // PutUint32 appends a number.
 257  func PutUint32(buffer []byte, value uint32) []byte {
 258  	return binary.LittleEndian.AppendUint32(buffer, value)
 259  }
 260  
 261  // ReadUint32 reads a number.
 262  func ReadUint32(payload []byte) (uint32, []byte, error) {
 263  	if len(payload) < 4 {
 264  		return 0, nil, fmt.Errorf("%w: a number", ErrBadFrame)
 265  	}
 266  
 267  	return binary.LittleEndian.Uint32(payload[0:4]), payload[4:], nil
 268  }
 269  
 270  // PutBool appends a flag.
 271  func PutBool(buffer []byte, value bool) []byte {
 272  	if value {
 273  		return append(buffer, 1)
 274  	}
 275  
 276  	return append(buffer, 0)
 277  }
 278  
 279  // ReadBool reads a flag.
 280  func ReadBool(payload []byte) (bool, []byte, error) {
 281  	if len(payload) < 1 {
 282  		return false, nil, fmt.Errorf("%w: a flag", ErrBadFrame)
 283  	}
 284  
 285  	return payload[0] == 1, payload[1:], nil
 286  }
 287  
 288  // PutHeights appends a height list: a count, then each height.
 289  func PutHeights(buffer []byte, heights []uint32) []byte {
 290  	buffer = binary.LittleEndian.AppendUint16(buffer, uint16(len(heights)))
 291  
 292  	for _, height := range heights {
 293  		buffer = binary.LittleEndian.AppendUint32(buffer, height)
 294  	}
 295  
 296  	return buffer
 297  }
 298  
 299  // ReadHeights reads a height list.
 300  func ReadHeights(payload []byte) ([]uint32, []byte, error) {
 301  	if len(payload) < 2 {
 302  		return nil, nil, fmt.Errorf("%w: a height count", ErrBadFrame)
 303  	}
 304  
 305  	count := int(binary.LittleEndian.Uint16(payload[0:2]))
 306  	payload = payload[2:]
 307  
 308  	if len(payload) < count*4 {
 309  		return nil, nil, fmt.Errorf("%w: %d heights in %d bytes", ErrBadFrame, count, len(payload))
 310  	}
 311  
 312  	heights := make([]uint32, 0, count)
 313  
 314  	for index := 0; index < count; index++ {
 315  		heights = append(heights, binary.LittleEndian.Uint32(payload[index*4:]))
 316  	}
 317  
 318  	return heights, payload[count*4:], nil
 319  }
 320  
 321  // PutTexts appends a text list, which is how addresses travel.
 322  func PutTexts(buffer []byte, texts []string) []byte {
 323  	buffer = binary.LittleEndian.AppendUint16(buffer, uint16(len(texts)))
 324  
 325  	for _, text := range texts {
 326  		buffer = PutText(buffer, text)
 327  	}
 328  
 329  	return buffer
 330  }
 331  
 332  // ReadTexts reads a text list.
 333  func ReadTexts(payload []byte) ([]string, []byte, error) {
 334  	if len(payload) < 2 {
 335  		return nil, nil, fmt.Errorf("%w: a text count", ErrBadFrame)
 336  	}
 337  
 338  	count := int(binary.LittleEndian.Uint16(payload[0:2]))
 339  	payload = payload[2:]
 340  
 341  	texts := make([]string, 0, count)
 342  
 343  	for index := 0; index < count; index++ {
 344  		text, rest, err := ReadText(payload)
 345  
 346  		if err != nil {
 347  			return nil, nil, err
 348  		}
 349  
 350  		texts = append(texts, text)
 351  		payload = rest
 352  	}
 353  
 354  	return texts, payload, nil
 355  }
 356  
 357  // itoa renders a small number without pulling in formatting.
 358  func itoa(value uint32) string {
 359  	if value == 0 {
 360  		return "0"
 361  	}
 362  
 363  	var digits []byte
 364  
 365  	for value > 0 {
 366  		digits = append([]byte{byte('0' + value%10)}, digits...)
 367  		value /= 10
 368  	}
 369  
 370  	return string(digits)
 371  }
 372