Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions btcjson/chainsvrresults.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ type GetBlockTemplateResult struct {
// Block proposal from BIP 0023.
Capabilities []string `json:"capabilities,omitempty"`
RejectReason string `json:"reject-reason,omitempty"`

Rules []string `json:"rules,omitempty"`
}

// GetMempoolEntryResult models the data returned from the getmempoolentry's
Expand Down
149 changes: 149 additions & 0 deletions integration/rpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"time"

"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg/v2"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/integration/rpctest"
Expand Down Expand Up @@ -289,6 +290,151 @@ func testGetNetworkHashPS3(r *rpctest.Harness, t *testing.T) {
}
}

func ensureSegwitActive(r *rpctest.Harness, t *testing.T) {
t.Helper()

for {
info, err := r.Client.GetBlockChainInfo()
if err != nil {
t.Fatalf("unable to get blockchain info: %v", err)
}

if info.Bip9SoftForks == nil ||
info.Bip9SoftForks["segwit"] == nil {
t.Fatalf("segwit softfork status not found in" +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line between info.Bip9SoftForks["segwit"] == nil { and t.Fatalf("segwit softfork status not found in" + would improve readability.

" blockchain info")
}

status := info.Bip9SoftForks["segwit"].Status
if status == "active" {
break
}

if _, err := r.Client.Generate(100); err != nil {
t.Fatalf("unable to generate blocks to activate "+
"segwit: %v", err)
}
}
}

func testGetBlockTemplateSegwitActiveNoRule(r *rpctest.Harness, t *testing.T) {
// Guarantee SegWit is fully active before testing
ensureSegwitActive(r, t)

// Call getblocktemplate with empty rules when segwit is active
req := &btcjson.TemplateRequest{
Rules: []string{},
}

_, err := r.Client.GetBlockTemplate(req)

if err == nil {
t.Fatalf("Expected getblocktemplate to fail without" +
" 'segwit' rule")
}

// Expect: ErrRPCInvalidParameter with correct message
rpcErr, ok := err.(*btcjson.RPCError)

if !ok {
t.Fatalf("Expected an RPCError, but got: %v", err)
}

if rpcErr.Code != btcjson.ErrRPCInvalidParameter {
t.Fatalf("Expected error code %d, but got: %d",
btcjson.ErrRPCInvalidParameter, rpcErr.Code)
}

expectedMessage := "Support for 'segwit' rule requires explicit " +
"client support"

if rpcErr.Message != expectedMessage {
t.Fatalf("Expected error message '%s', but got: '%s'",
expectedMessage, rpcErr.Message)
}
}

func testGetBlockTemplateSegwitActiveWithRule(
r *rpctest.Harness, t *testing.T) {
// Guarantee SegWit is fully active before testing
ensureSegwitActive(r, t)

// Call getblocktemplate with 'segwit' rule when segwit is active
req := &btcjson.TemplateRequest{
Rules: []string{"segwit"},
}

result, err := r.Client.GetBlockTemplate(req)

if err != nil {
t.Fatalf("Expected getblocktemplate to succeed, got "+
"error: %v", err)
}

if result == nil {
t.Fatal("Expected non-nil result")
}

hasSegwitRule := false
for _, rule := range result.Rules {
if rule == "segwit" || rule == "!segwit" {
hasSegwitRule = true
break
}
}

if !hasSegwitRule {
t.Fatalf("Expected 'segwit' rule to be present in the response")
}
}

func testGetBlockTemplateResponseRules(r *rpctest.Harness, t *testing.T) {
// Guarantee SegWit is fully active before testing
ensureSegwitActive(r, t)

// Call getblocktemplate with 'segwit' rule when segwit is active
req := &btcjson.TemplateRequest{
Rules: []string{"segwit"},
}

result, err := r.Client.GetBlockTemplate(req)

if err != nil {
t.Fatalf("Expected getblocktemplate to succeed, got "+
"error: %v", err)
}

if result == nil {
t.Fatal("Expected non-nil result")
}

// Verify blockTemplateResult includes "!segwit" in Rules when
// WitnessCommitment is non-nil and "segwit" when WitnessCommitment
// is nil.
hasNotSegwit := false
hasSegwit := false

for _, rule := range result.Rules {
if rule == "!segwit" {
hasNotSegwit = true
} else if rule == "segwit" {
hasSegwit = true
}
}

if result.DefaultWitnessCommitment != "" {
if !hasNotSegwit {
t.Fatalf("Expected Rules to contain '!segwit' because" +
" WitnessCommitment is present")
}
} else {
if !hasSegwit {
t.Fatalf("Expected Rules to contain 'segwit' because " +
"WitnessCommitment is absent")
}
}
}

var rpcTestCases = []rpctest.HarnessTestCase{
testGetBestBlock,
testGetBlockCount,
Expand All @@ -297,6 +443,9 @@ var rpcTestCases = []rpctest.HarnessTestCase{
testGetNetworkHashPS,
testGetNetworkHashPS2,
testGetNetworkHashPS3,
testGetBlockTemplateSegwitActiveNoRule,
testGetBlockTemplateSegwitActiveWithRule,
testGetBlockTemplateResponseRules,
}

var primaryHarness *rpctest.Harness
Expand Down
48 changes: 40 additions & 8 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"net"
"net/http"
"os"
"slices"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -1688,7 +1689,9 @@ func (state *gbtWorkState) updateBlockTemplate(s *rpcServer, useCoinbaseValue bo
// and returned to the caller.
//
// This function MUST be called with the state locked.
func (state *gbtWorkState) blockTemplateResult(useCoinbaseValue bool, submitOld *bool) (*btcjson.GetBlockTemplateResult, error) {
func (state *gbtWorkState) blockTemplateResult(
useCoinbaseValue, segwitActive bool,
submitOld *bool) (*btcjson.GetBlockTemplateResult, error) {
// 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
Expand Down Expand Up @@ -1789,6 +1792,10 @@ func (state *gbtWorkState) blockTemplateResult(useCoinbaseValue bool, submitOld
// data, then include the witness commitment in the GBT result.
if template.WitnessCommitment != nil {
reply.DefaultWitnessCommitment = hex.EncodeToString(template.WitnessCommitment)
reply.Rules = append(reply.Rules, "!segwit")

} else if segwitActive {
reply.Rules = append(reply.Rules, "segwit")
}

if useCoinbaseValue {
Expand Down Expand Up @@ -1839,7 +1846,9 @@ func (state *gbtWorkState) blockTemplateResult(useCoinbaseValue bool, submitOld
// has passed without finding a solution.
//
// See https://en.bitcoin.it/wiki/BIP_0022 for more details.
func handleGetBlockTemplateLongPoll(s *rpcServer, longPollID string, useCoinbaseValue bool, closeChan <-chan struct{}) (interface{}, error) {
func handleGetBlockTemplateLongPoll(
s *rpcServer, longPollID string, closeChan <-chan struct{},
useCoinbaseValue, segwitActive bool) (interface{}, error) {
state := s.gbtWorkState
state.Lock()
// The state unlock is intentionally not deferred here since it needs to
Expand All @@ -1855,7 +1864,9 @@ func handleGetBlockTemplateLongPoll(s *rpcServer, longPollID string, useCoinbase
// the caller is invalid.
prevHash, lastGenerated, err := decodeTemplateID(longPollID)
if err != nil {
result, err := state.blockTemplateResult(useCoinbaseValue, nil)
result, err := state.blockTemplateResult(
useCoinbaseValue, segwitActive, nil,
)
if err != nil {
state.Unlock()
return nil, err
Expand All @@ -1876,8 +1887,8 @@ func handleGetBlockTemplateLongPoll(s *rpcServer, longPollID string, useCoinbase
// old block template depending on whether or not a solution has
// already been found and added to the block chain.
submitOld := prevHash.IsEqual(prevTemplateHash)
result, err := state.blockTemplateResult(useCoinbaseValue,
&submitOld)
result, err := state.blockTemplateResult(
useCoinbaseValue, segwitActive, &submitOld)
if err != nil {
state.Unlock()
return nil, err
Expand Down Expand Up @@ -1917,7 +1928,9 @@ func handleGetBlockTemplateLongPoll(s *rpcServer, longPollID string, useCoinbase
// block template depending on whether or not a solution has already
// been found and added to the block chain.
submitOld := prevHash.IsEqual(&state.template.Block.Header.PrevBlock)
result, err := state.blockTemplateResult(useCoinbaseValue, &submitOld)
result, err := state.blockTemplateResult(
useCoinbaseValue, segwitActive, &submitOld,
)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1986,12 +1999,31 @@ func handleGetBlockTemplateRequest(s *rpcServer, request *btcjson.TemplateReques
}
}

segwitState, err := s.cfg.Chain.ThresholdState(
chaincfg.DeploymentSegwit,
)
if err != nil {
return nil, err
}

segwitActive := segwitState == blockchain.ThresholdActive
hasSegwitRule := request != nil && slices.Contains(
request.Rules, "segwit",
)
if segwitActive && !hasSegwitRule {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidParameter,
Message: "Support for 'segwit' rule requires explicit" +
" client support",
}
}

// 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,
useCoinbaseValue, closeChan)
closeChan, useCoinbaseValue, segwitActive)
}

// Protect concurrent access when updating block templates.
Expand All @@ -2008,7 +2040,7 @@ func handleGetBlockTemplateRequest(s *rpcServer, request *btcjson.TemplateReques
if err := state.updateBlockTemplate(s, useCoinbaseValue); err != nil {
return nil, err
}
return state.blockTemplateResult(useCoinbaseValue, nil)
return state.blockTemplateResult(useCoinbaseValue, segwitActive, nil)
}

// chainErrToGBTErrString converts an error returned from btcchain to a string
Expand Down
1 change: 1 addition & 0 deletions rpcserverhelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ var helpDescsEnUS = map[string]string{
"getblocktemplateresult-reject-reason": "Reason the proposal was invalid as-is (only applies to proposal responses)",
"getblocktemplateresult-default_witness_commitment": "The witness commitment itself. Will be populated if the block has witness data",
"getblocktemplateresult-weightlimit": "The current limit on the max allowed weight of a block",
"getblocktemplateresult-rules": "List of rules the server requires the client to understand and support",

// GetBlockTemplateCmd help.
"getblocktemplate--synopsis": "Returns a JSON object with information necessary to construct a block to mine or accepts a proposal to validate.\n" +
Expand Down