kaspad/rpc/handle_get_block_template.go
Ori Newman 8e170cf327
[NOD-1225] Rename wire to domainmessage and get rid of InvType (#853)
* [NOD-1225] Rename wire to domainmessage

* [NOD-1225] Get rid of references to package wire in the code, and get rid of InvType
2020-08-09 12:39:15 +03:00

773 lines
26 KiB
Go

package rpc
import (
"bytes"
"encoding/hex"
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/kaspanet/kaspad/util/mstime"
"github.com/kaspanet/kaspad/blockdag"
"github.com/kaspanet/kaspad/domainmessage"
"github.com/kaspanet/kaspad/mining"
"github.com/kaspanet/kaspad/rpc/model"
"github.com/kaspanet/kaspad/txscript"
"github.com/kaspanet/kaspad/util"
"github.com/kaspanet/kaspad/util/daghash"
"github.com/kaspanet/kaspad/util/random"
"github.com/pkg/errors"
)
const (
// gbtNonceRange is two 64-bit big-endian hexadecimal integers which
// represent the valid ranges of nonces returned by the getBlockTemplate
// RPC.
gbtNonceRange = "000000000000ffffffffffff"
// gbtRegenerateSeconds is the number of seconds that must pass before
// a new template is generated when the parent block hashes has not
// changed and there have been changes to the available transactions
// in the memory pool.
gbtRegenerateSeconds = 60
)
var (
// gbtMutableFields are the manipulations the server allows to be made
// to block templates generated by the getBlockTemplate RPC. It is
// declared here to avoid the overhead of creating the slice on every
// invocation for constant data.
gbtMutableFields = []string{
"time", "transactions/add", "parentblock", "coinbase/append",
}
// gbtCapabilities describes additional capabilities returned with a
// block template generated by the getBlockTemplate RPC. It is
// declared here to avoid the overhead of creating the slice on every
// invocation for constant data.
gbtCapabilities = []string{"proposal"}
)
// gbtWorkState houses state that is used in between multiple RPC invocations to
// getBlockTemplate.
type gbtWorkState struct {
sync.Mutex
lastTxUpdate mstime.Time
lastGenerated mstime.Time
tipHashes []*daghash.Hash
minTimestamp mstime.Time
template *mining.BlockTemplate
notifyMap map[string]map[int64]chan struct{}
payAddress util.Address
}
// newGbtWorkState returns a new instance of a gbtWorkState with all internal
// fields initialized and ready to use.
func newGbtWorkState() *gbtWorkState {
return &gbtWorkState{
notifyMap: make(map[string]map[int64]chan struct{}),
}
}
// builderScript is a convenience function which is used for hard-coded scripts
// built with the script builder. Any errors are converted to a panic since it
// is only, and must only, be used with hard-coded, and therefore, known good,
// scripts.
func builderScript(builder *txscript.ScriptBuilder) []byte {
script, err := builder.Script()
if err != nil {
panic(err)
}
return script
}
// handleGetBlockTemplate implements the getBlockTemplate command.
func handleGetBlockTemplate(s *Server, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) {
c := cmd.(*model.GetBlockTemplateCmd)
request := c.Request
// Set the default mode and override it if supplied.
mode := "template"
if request != nil && request.Mode != "" {
mode = request.Mode
}
switch mode {
case "template":
return handleGetBlockTemplateRequest(s, request, closeChan)
case "proposal":
return handleGetBlockTemplateProposal(s, request)
}
return nil, &model.RPCError{
Code: model.ErrRPCInvalidParameter,
Message: "Invalid mode",
}
}
// handleGetBlockTemplateRequest is a helper for handleGetBlockTemplate which
// deals with generating and returning block templates to the caller. It
// handles both long poll requests as specified by BIP 0022 as well as regular
// requests.
func handleGetBlockTemplateRequest(s *Server, request *model.TemplateRequest, closeChan <-chan struct{}) (interface{}, error) {
// Return an error if there are no peers connected since there is no
// way to relay a found block or receive transactions to work on.
// However, allow this state when running in the regression test or
// simulation test mode.
if !(s.cfg.RegressionTest || s.cfg.Simnet) &&
s.connectionManager.ConnectionCount() == 0 {
return nil, &model.RPCError{
Code: model.ErrRPCClientNotConnected,
Message: "Kaspa is not connected",
}
}
payAddr, err := util.DecodeAddress(request.PayAddress, s.dag.Params.Prefix)
if err != nil {
return nil, err
}
// When a long poll ID was provided, this is a long poll request by the
// client to be notified when block template referenced by the ID should
// be replaced with a new one.
if request != nil && request.LongPollID != "" {
return handleGetBlockTemplateLongPoll(s, request.LongPollID, payAddr, closeChan)
}
// Protect concurrent access when updating block templates.
state := s.gbtWorkState
state.Lock()
defer state.Unlock()
// Get and return a block template. A new block template will be
// generated when the current best block has changed or the transactions
// in the memory pool have been updated and it has been at least five
// seconds since the last template was generated. Otherwise, the
// timestamp for the existing block template is updated (and possibly
// the difficulty on testnet per the consesus rules).
if err := state.updateBlockTemplate(s, payAddr); err != nil {
return nil, err
}
return state.blockTemplateResult(s)
}
// handleGetBlockTemplateLongPoll is a helper for handleGetBlockTemplateRequest
// which deals with handling long polling for block templates. When a caller
// sends a request with a long poll ID that was previously returned, a response
// is not sent until the caller should stop working on the previous block
// template in favor of the new one. In particular, this is the case when the
// old block template is no longer valid due to a solution already being found
// and added to the block DAG, or new transactions have shown up and some time
// has passed without finding a solution.
func handleGetBlockTemplateLongPoll(s *Server, longPollID string, payAddr util.Address, closeChan <-chan struct{}) (interface{}, error) {
state := s.gbtWorkState
result, longPollChan, err := blockTemplateOrLongPollChan(s, longPollID, payAddr)
if err != nil {
return nil, err
}
if result != nil {
return result, nil
}
select {
// When the client closes before it's time to send a reply, just return
// now so the goroutine doesn't hang around.
case <-closeChan:
return nil, ErrClientQuit
// Wait until signal received to send the reply.
case <-longPollChan:
// Fallthrough
}
// Get the lastest block template
state.Lock()
defer state.Unlock()
if err := state.updateBlockTemplate(s, payAddr); err != nil {
return nil, err
}
// Include whether or not it is valid to submit work against the old
// block template depending on whether or not a solution has already
// been found and added to the block DAG.
result, err = state.blockTemplateResult(s)
if err != nil {
return nil, err
}
return result, nil
}
// blockTemplateOrLongPollChan returns a block template if the
// template identified by the provided long poll ID is stale or
// invalid. Otherwise, it returns a channel that will notify
// when there's a more current template.
func blockTemplateOrLongPollChan(s *Server, longPollID string, payAddr util.Address) (*model.GetBlockTemplateResult, chan struct{}, error) {
state := s.gbtWorkState
state.Lock()
defer state.Unlock()
// The state unlock is intentionally not deferred here since it needs to
// be manually unlocked before waiting for a notification about block
// template changes.
if err := state.updateBlockTemplate(s, payAddr); err != nil {
return nil, nil, err
}
// Just return the current block template if the long poll ID provided by
// the caller is invalid.
parentHashes, lastGenerated, err := decodeLongPollID(longPollID)
if err != nil {
result, err := state.blockTemplateResult(s)
if err != nil {
return nil, nil, err
}
return result, nil, nil
}
// Return the block template now if the specific block template
// identified by the long poll ID no longer matches the current block
// template as this means the provided template is stale.
areHashesEqual := daghash.AreEqual(state.template.Block.Header.ParentHashes, parentHashes)
if !areHashesEqual ||
lastGenerated != state.lastGenerated.UnixSeconds() {
// Include whether or not it is valid to submit work against the
// old block template depending on whether or not a solution has
// already been found and added to the block DAG.
result, err := state.blockTemplateResult(s)
if err != nil {
return nil, nil, err
}
return result, nil, nil
}
// Register the parent hashes and last generated time for notifications
// Get a channel that will be notified when the template associated with
// the provided ID is stale and a new block template should be returned to
// the caller.
longPollChan := state.templateUpdateChan(parentHashes, lastGenerated)
return nil, longPollChan, nil
}
// handleGetBlockTemplateProposal is a helper for handleGetBlockTemplate which
// deals with block proposals.
func handleGetBlockTemplateProposal(s *Server, request *model.TemplateRequest) (interface{}, error) {
hexData := request.Data
if hexData == "" {
return false, &model.RPCError{
Code: model.ErrRPCType,
Message: fmt.Sprintf("Data must contain the " +
"hex-encoded serialized block that is being " +
"proposed"),
}
}
// Ensure the provided data is sane and deserialize the proposed block.
if len(hexData)%2 != 0 {
hexData = "0" + hexData
}
dataBytes, err := hex.DecodeString(hexData)
if err != nil {
return false, &model.RPCError{
Code: model.ErrRPCDeserialization,
Message: fmt.Sprintf("Data must be "+
"hexadecimal string (not %q)", hexData),
}
}
var msgBlock domainmessage.MsgBlock
if err := msgBlock.Deserialize(bytes.NewReader(dataBytes)); err != nil {
return nil, &model.RPCError{
Code: model.ErrRPCDeserialization,
Message: "Block decode failed: " + err.Error(),
}
}
block := util.NewBlock(&msgBlock)
// Ensure the block is building from the expected parent blocks.
expectedParentHashes := s.dag.TipHashes()
parentHashes := block.MsgBlock().Header.ParentHashes
if !daghash.AreEqual(expectedParentHashes, parentHashes) {
return "bad-parentblk", nil
}
if err := s.dag.CheckConnectBlockTemplate(block); err != nil {
if !errors.As(err, &blockdag.RuleError{}) {
errStr := fmt.Sprintf("Failed to process block proposal: %s", err)
log.Error(errStr)
return nil, &model.RPCError{
Code: model.ErrRPCVerify,
Message: errStr,
}
}
log.Infof("Rejected block proposal: %s", err)
return dagErrToGBTErrString(err), nil
}
return nil, nil
}
// dagErrToGBTErrString converts an error returned from kaspa to a string
// which matches the reasons and format described in BIP0022 for rejection
// reasons.
func dagErrToGBTErrString(err error) string {
// When the passed error is not a RuleError, just return a generic
// rejected string with the error text.
var ruleErr blockdag.RuleError
if !errors.As(err, &ruleErr) {
return "rejected: " + err.Error()
}
switch ruleErr.ErrorCode {
case blockdag.ErrDuplicateBlock:
return "duplicate"
case blockdag.ErrBlockMassTooHigh:
return "bad-blk-mass"
case blockdag.ErrBlockVersionTooOld:
return "bad-version"
case blockdag.ErrTimeTooOld:
return "time-too-old"
case blockdag.ErrTimeTooNew:
return "time-too-new"
case blockdag.ErrDifficultyTooLow:
return "bad-diffbits"
case blockdag.ErrUnexpectedDifficulty:
return "bad-diffbits"
case blockdag.ErrHighHash:
return "high-hash"
case blockdag.ErrBadMerkleRoot:
return "bad-txnmrklroot"
case blockdag.ErrFinalityPointTimeTooOld:
return "finality-point-time-too-old"
case blockdag.ErrNoTransactions:
return "bad-txns-none"
case blockdag.ErrNoTxInputs:
return "bad-txns-noinputs"
case blockdag.ErrTxMassTooHigh:
return "bad-txns-mass"
case blockdag.ErrBadTxOutValue:
return "bad-txns-outputvalue"
case blockdag.ErrDuplicateTxInputs:
return "bad-txns-dupinputs"
case blockdag.ErrBadTxInput:
return "bad-txns-badinput"
case blockdag.ErrMissingTxOut:
return "bad-txns-missinginput"
case blockdag.ErrUnfinalizedTx:
return "bad-txns-unfinalizedtx"
case blockdag.ErrDuplicateTx:
return "bad-txns-duplicate"
case blockdag.ErrOverwriteTx:
return "bad-txns-overwrite"
case blockdag.ErrImmatureSpend:
return "bad-txns-maturity"
case blockdag.ErrSpendTooHigh:
return "bad-txns-highspend"
case blockdag.ErrBadFees:
return "bad-txns-fees"
case blockdag.ErrTooManySigOps:
return "high-sigops"
case blockdag.ErrFirstTxNotCoinbase:
return "bad-txns-nocoinbase"
case blockdag.ErrMultipleCoinbases:
return "bad-txns-multicoinbase"
case blockdag.ErrBadCoinbasePayloadLen:
return "bad-cb-length"
case blockdag.ErrScriptMalformed:
return "bad-script-malformed"
case blockdag.ErrScriptValidation:
return "bad-script-validate"
case blockdag.ErrParentBlockUnknown:
return "parent-blk-not-found"
case blockdag.ErrInvalidAncestorBlock:
return "bad-parentblk"
case blockdag.ErrParentBlockNotCurrentTips:
return "inconclusive-not-best-parentblk"
}
return "rejected: " + err.Error()
}
// notifyLongPollers notifies any channels that have been registered to be
// notified when block templates are stale.
//
// This function MUST be called with the state locked.
func (state *gbtWorkState) notifyLongPollers(tipHashes []*daghash.Hash, lastGenerated mstime.Time) {
// Notify anything that is waiting for a block template update from
// hashes which are not the current tip hashes.
tipHashesStr := daghash.JoinHashesStrings(tipHashes, "")
for hashesStr, channels := range state.notifyMap {
if hashesStr != tipHashesStr {
for _, c := range channels {
close(c)
}
delete(state.notifyMap, hashesStr)
}
}
// Return now if the provided last generated timestamp has not been
// initialized.
if lastGenerated.IsZero() {
return
}
// Return now if there is nothing registered for updates to the current
// best block hash.
channels, ok := state.notifyMap[tipHashesStr]
if !ok {
return
}
// Notify anything that is waiting for a block template update from a
// block template generated before the most recently generated block
// template.
lastGeneratedUnix := lastGenerated.UnixSeconds()
for lastGen, c := range channels {
if lastGen < lastGeneratedUnix {
close(c)
delete(channels, lastGen)
}
}
// Remove the entry altogether if there are no more registered
// channels.
if len(channels) == 0 {
delete(state.notifyMap, tipHashesStr)
}
}
// NotifyBlockAdded uses the newly-added block to notify any long poll
// clients with a new block template when their existing block template is
// stale due to the newly added block.
func (state *gbtWorkState) NotifyBlockAdded(tipHashes []*daghash.Hash) {
spawn("gbtWorkState.NotifyBlockAdded", func() {
state.Lock()
defer state.Unlock()
state.notifyLongPollers(tipHashes, state.lastTxUpdate)
})
}
// NotifyMempoolTx uses the new last updated time for the transaction memory
// pool to notify any long poll clients with a new block template when their
// existing block template is stale due to enough time passing and the contents
// of the memory pool changing.
func (state *gbtWorkState) NotifyMempoolTx(lastUpdated mstime.Time) {
spawn("NotifyMempoolTx", func() {
state.Lock()
defer state.Unlock()
// No need to notify anything if no block templates have been generated
// yet.
if state.tipHashes == nil || state.lastGenerated.IsZero() {
return
}
if mstime.Now().After(state.lastGenerated.Add(time.Second *
gbtRegenerateSeconds)) {
state.notifyLongPollers(state.tipHashes, lastUpdated)
}
})
}
// templateUpdateChan returns a channel that will be closed once the block
// template associated with the passed parent hashes and last generated time
// is stale. The function will return existing channels for duplicate
// parameters which allows multiple clients to wait for the same block template
// without requiring a different channel for each client.
//
// This function MUST be called with the state locked.
func (state *gbtWorkState) templateUpdateChan(tipHashes []*daghash.Hash, lastGenerated int64) chan struct{} {
tipHashesStr := daghash.JoinHashesStrings(tipHashes, "")
// Either get the current list of channels waiting for updates about
// changes to block template for the parent hashes or create a new one.
channels, ok := state.notifyMap[tipHashesStr]
if !ok {
m := make(map[int64]chan struct{})
state.notifyMap[tipHashesStr] = m
channels = m
}
// Get the current channel associated with the time the block template
// was last generated or create a new one.
c, ok := channels[lastGenerated]
if !ok {
c = make(chan struct{})
channels[lastGenerated] = c
}
return c
}
// updateBlockTemplate creates or updates a block template for the work state.
// A new block template will be generated when the current best block has
// changed or the transactions in the memory pool have been updated and it has
// been long enough since the last template was generated. Otherwise, the
// timestamp for the existing block template is updated (and possibly the
// difficulty on testnet per the consesus rules). Finally, if the
// useCoinbaseValue flag is false and the existing block template does not
// already contain a valid payment address, the block template will be updated
// with a randomly selected payment address from the list of configured
// addresses.
//
// This function MUST be called with the state locked.
func (state *gbtWorkState) updateBlockTemplate(s *Server, payAddr util.Address) error {
generator := s.blockTemplateGenerator
lastTxUpdate := generator.TxSource().LastUpdated()
if lastTxUpdate.IsZero() {
lastTxUpdate = mstime.Now()
}
// Generate a new block template when the current best block has
// changed or the transactions in the memory pool have been updated and
// it has been at least gbtRegenerateSecond since the last template was
// generated.
var msgBlock *domainmessage.MsgBlock
var targetDifficulty string
tipHashes := s.dag.TipHashes()
template := state.template
if template == nil || state.tipHashes == nil ||
!daghash.AreEqual(state.tipHashes, tipHashes) ||
state.payAddress.String() != payAddr.String() ||
(state.lastTxUpdate != lastTxUpdate &&
mstime.Now().After(state.lastGenerated.Add(time.Second*
gbtRegenerateSeconds))) {
// Reset the previous best hash the block template was generated
// against so any errors below cause the next invocation to try
// again.
state.tipHashes = nil
// Create a new block template that has a coinbase which anyone
// can redeem. This is only acceptable because the returned
// block template doesn't include the coinbase, so the caller
// will ultimately create their own coinbase which pays to the
// appropriate address(es).
extraNonce, err := random.Uint64()
if err != nil {
return internalRPCError(fmt.Sprintf("Failed to randomize "+
"extra nonce: %s", err.Error()), "")
}
blkTemplate, err := generator.NewBlockTemplate(payAddr, extraNonce)
if err != nil {
return internalRPCError(fmt.Sprintf("Failed to create new block "+
"template: %s", err.Error()), "")
}
template = blkTemplate
msgBlock = template.Block
targetDifficulty = fmt.Sprintf("%064x",
util.CompactToBig(msgBlock.Header.Bits))
// Get the minimum allowed timestamp for the block based on the
// median timestamp of the last several blocks per the DAG
// consensus rules.
minTimestamp := s.dag.NextBlockMinimumTime()
// Update work state to ensure another block template isn't
// generated until needed.
state.template = template
state.lastGenerated = mstime.Now()
state.lastTxUpdate = lastTxUpdate
state.tipHashes = tipHashes
state.minTimestamp = minTimestamp
state.payAddress = payAddr
log.Debugf("Generated block template (timestamp %s, "+
"target %s, merkle root %s)",
msgBlock.Header.Timestamp, targetDifficulty,
msgBlock.Header.HashMerkleRoot)
// Notify any clients that are long polling about the new
// template.
state.notifyLongPollers(tipHashes, lastTxUpdate)
} else {
// At this point, there is a saved block template and another
// request for a template was made, but either the available
// transactions haven't change or it hasn't been long enough to
// trigger a new block template to be generated. So, update the
// existing block template.
// Set locals for convenience.
msgBlock = template.Block
targetDifficulty = fmt.Sprintf("%064x",
util.CompactToBig(msgBlock.Header.Bits))
// Update the time of the block template to the current time
// while accounting for the median time of the past several
// blocks per the DAG consensus rules.
generator.UpdateBlockTime(msgBlock)
msgBlock.Header.Nonce = 0
log.Debugf("Updated block template (timestamp %s, "+
"target %s)", msgBlock.Header.Timestamp,
targetDifficulty)
}
return nil
}
// blockTemplateResult returns the current block template associated with the
// state as a model.GetBlockTemplateResult that is ready to be encoded to JSON
// and returned to the caller.
//
// This function MUST be called with the state locked.
func (state *gbtWorkState) blockTemplateResult(s *Server) (*model.GetBlockTemplateResult, error) {
dag := s.dag
// Ensure the timestamps are still in valid range for the template.
// This should really only ever happen if the local clock is changed
// after the template is generated, but it's important to avoid serving
// block templates that will be delayed on other nodes.
template := state.template
msgBlock := template.Block
header := &msgBlock.Header
adjustedTime := dag.Now()
maxTime := adjustedTime.Add(time.Millisecond * time.Duration(dag.TimestampDeviationTolerance))
if header.Timestamp.After(maxTime) {
return nil, &model.RPCError{
Code: model.ErrRPCOutOfRange,
Message: fmt.Sprintf("The template time is after the "+
"maximum allowed time for a block - template "+
"time %s, maximum time %s", adjustedTime,
maxTime),
}
}
// Convert each transaction in the block template to a template result
// transaction. The result does not include the coinbase, so notice
// the adjustments to the various lengths and indices.
numTx := len(msgBlock.Transactions)
transactions := make([]model.GetBlockTemplateResultTx, 0, numTx-1)
txIndex := make(map[daghash.TxID]int64, numTx)
for i, tx := range msgBlock.Transactions {
txID := tx.TxID()
txIndex[*txID] = int64(i)
// Create an array of 1-based indices to transactions that come
// before this one in the transactions list which this one
// depends on. This is necessary since the created block must
// ensure proper ordering of the dependencies. A map is used
// before creating the final array to prevent duplicate entries
// when multiple inputs reference the same transaction.
dependsMap := make(map[int64]struct{})
for _, txIn := range tx.TxIn {
if idx, ok := txIndex[txIn.PreviousOutpoint.TxID]; ok {
dependsMap[idx] = struct{}{}
}
}
depends := make([]int64, 0, len(dependsMap))
for idx := range dependsMap {
depends = append(depends, idx)
}
// Serialize the transaction for later conversion to hex.
txBuf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
if err := tx.Serialize(txBuf); err != nil {
context := "Failed to serialize transaction"
return nil, internalRPCError(err.Error(), context)
}
resultTx := model.GetBlockTemplateResultTx{
Data: hex.EncodeToString(txBuf.Bytes()),
ID: txID.String(),
Depends: depends,
Mass: template.TxMasses[i],
Fee: template.Fees[i],
}
transactions = append(transactions, resultTx)
}
// Generate the block template reply. Note that following mutations are
// implied by the included or omission of fields:
// Including MinTime -> time/decrement
// Omitting CoinbaseTxn -> coinbase, generation
targetDifficulty := fmt.Sprintf("%064x", util.CompactToBig(header.Bits))
longPollID := encodeLongPollID(state.tipHashes, state.payAddress, state.lastGenerated)
// Check whether this node is synced with the rest of of the
// network. There's almost never a good reason to mine on top
// of an unsynced DAG, and miners are generally expected not to
// mine when isSynced is false.
// This is not a straight-up error because the choice of whether
// to mine or not is the responsibility of the miner rather
// than the node's.
isSynced := s.blockTemplateGenerator.IsSynced()
reply := model.GetBlockTemplateResult{
Bits: strconv.FormatInt(int64(header.Bits), 16),
CurTime: header.Timestamp.UnixMilliseconds(),
Height: template.Height,
ParentHashes: daghash.Strings(header.ParentHashes),
MassLimit: domainmessage.MaxMassPerBlock,
Transactions: transactions,
HashMerkleRoot: header.HashMerkleRoot.String(),
AcceptedIDMerkleRoot: header.AcceptedIDMerkleRoot.String(),
UTXOCommitment: header.UTXOCommitment.String(),
Version: header.Version,
LongPollID: longPollID,
Target: targetDifficulty,
MinTime: state.minTimestamp.UnixMilliseconds(),
MaxTime: maxTime.UnixMilliseconds(),
Mutable: gbtMutableFields,
NonceRange: gbtNonceRange,
Capabilities: gbtCapabilities,
IsSynced: isSynced,
}
return &reply, nil
}
// encodeLongPollID encodes the passed details into an ID that can be used to
// uniquely identify a block template.
func encodeLongPollID(parentHashes []*daghash.Hash, miningAddress util.Address, lastGenerated mstime.Time) string {
return fmt.Sprintf("%s-%s-%d", daghash.JoinHashesStrings(parentHashes, ""), miningAddress, lastGenerated.UnixSeconds())
}
// decodeLongPollID decodes an ID that is used to uniquely identify a block
// template. This is mainly used as a mechanism to track when to update clients
// that are using long polling for block templates. The ID consists of the
// parent blocks hashes for the associated template and the time the associated
// template was generated.
func decodeLongPollID(longPollID string) ([]*daghash.Hash, int64, error) {
fields := strings.Split(longPollID, "-")
if len(fields) != 2 {
return nil, 0, errors.New("decodeLongPollID: invalid number of fields")
}
parentHashesStr := fields[0]
if len(parentHashesStr)%daghash.HashSize != 0 {
return nil, 0, errors.New("decodeLongPollID: invalid parent hashes format")
}
numberOfHashes := len(parentHashesStr) / daghash.HashSize
parentHashes := make([]*daghash.Hash, 0, numberOfHashes)
for i := 0; i < len(parentHashesStr); i += daghash.HashSize {
hash, err := daghash.NewHashFromStr(parentHashesStr[i : i+daghash.HashSize])
if err != nil {
return nil, 0, errors.Errorf("decodeLongPollID: NewHashFromStr: %s", err)
}
parentHashes = append(parentHashes, hash)
}
lastGenerated, err := strconv.ParseInt(fields[1], 10, 64)
if err != nil {
return nil, 0, errors.Errorf("decodeLongPollID: Cannot parse timestamp %s: %s", fields[1], err)
}
return parentHashes, lastGenerated, nil
}