mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-03-30 15:08:33 +00:00

* [NOD-1225] Rename wire to domainmessage * [NOD-1225] Get rid of references to package wire in the code, and get rid of InvType
773 lines
26 KiB
Go
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
|
|
}
|