Skip to content

Commit 29f098f

Browse files
committed
rpc: enforce BIP 145 SegWit rules in getblocktemplate
This commit updates the getblocktemplate RPC to enforce the SegWit rules specified in BIP 145. Specifically, it ensures that: - Client requests are rejected with ErrRPCInvalidParameter if SegWit is active but the client does not explicitly support the 'segwit' rule. - The 'rules' array in the response conditionally includes '!segwit' if the generated block template contains transactions with witness data (indicating a witness commitment is required). - The 'rules' array includes 'segwit' (without the '!' prefix) when SegWit is active but the template does not contain any witness transactions. Additionally, integration tests have been added to verify that rule signaling and SegWit activation behave correctly during block template generation.
1 parent 6cfd717 commit 29f098f

4 files changed

Lines changed: 190 additions & 7 deletions

File tree

btcjson/chainsvrresults.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ type GetBlockTemplateResult struct {
308308
// Block proposal from BIP 0023.
309309
Capabilities []string `json:"capabilities,omitempty"`
310310
RejectReason string `json:"reject-reason,omitempty"`
311+
312+
Rules []string `json:"rules,omitempty"`
311313
}
312314

313315
// GetMempoolEntryResult models the data returned from the getmempoolentry's

integration/rpcserver_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"time"
1818

1919
"github.com/btcsuite/btcd/blockchain"
20+
"github.com/btcsuite/btcd/btcjson"
2021
"github.com/btcsuite/btcd/chaincfg/v2"
2122
"github.com/btcsuite/btcd/chainhash/v2"
2223
"github.com/btcsuite/btcd/integration/rpctest"
@@ -289,6 +290,150 @@ func testGetNetworkHashPS3(r *rpctest.Harness, t *testing.T) {
289290
}
290291
}
291292

293+
func ensureSegwitActive(r *rpctest.Harness, t *testing.T) {
294+
t.Helper()
295+
296+
for {
297+
info, err := r.Client.GetBlockChainInfo()
298+
if err != nil {
299+
t.Fatalf("unable to get blockchain info: %v", err)
300+
}
301+
302+
if info.Bip9SoftForks == nil || info.Bip9SoftForks["segwit"]
303+
== nil {
304+
t.Fatalf("segwit softfork status not found in" +
305+
" blockchain info")
306+
}
307+
308+
status := info.Bip9SoftForks["segwit"].Status
309+
if status == "active" {
310+
break
311+
}
312+
313+
if _, err := r.Client.Generate(100); err != nil {
314+
t.Fatalf("unable to generate blocks to activate "+
315+
"segwit: %v", err)
316+
}
317+
}
318+
}
319+
320+
func testGetBlockTemplateSegwitActiveNoRule(r *rpctest.Harness, t *testing.T) {
321+
// Guarantee SegWit is fully active before testing
322+
ensureSegwitActive(r, t)
323+
324+
// Call getblocktemplate with empty rules when segwit is active
325+
req := &btcjson.TemplateRequest{
326+
Rules: []string{},
327+
}
328+
329+
_, err := r.Client.GetBlockTemplate(req)
330+
331+
if err == nil {
332+
t.Fatalf("Expected getblocktemplate to fail without" +
333+
" 'segwit' rule")
334+
}
335+
336+
// Expect: ErrRPCInvalidParameter with correct message
337+
rpcErr, ok := err.(*btcjson.RPCError)
338+
339+
if !ok {
340+
t.Fatalf("Expected an RPCError, but got: %v", err)
341+
}
342+
343+
if rpcErr.Code != btcjson.ErrRPCInvalidParameter {
344+
t.Fatalf("Expected error code %d, but got: %d",
345+
btcjson.ErrRPCInvalidParameter, rpcErr.Code)
346+
}
347+
348+
expectedMessage := "Support for 'segwit' rule requires explicit " +
349+
"client support"
350+
351+
if rpcErr.Message != expectedMessage {
352+
t.Fatalf("Expected error message '%s', but got: '%s'",
353+
expectedMessage, rpcErr.Message)
354+
}
355+
}
356+
357+
func testGetBlockTemplateSegwitActiveWithRule(r *rpctest.Harness, t *testing.T) {
358+
// Guarantee SegWit is fully active before testing
359+
ensureSegwitActive(r, t)
360+
361+
// Call getblocktemplate with 'segwit' rule when segwit is active
362+
req := &btcjson.TemplateRequest{
363+
Rules: []string{"segwit"},
364+
}
365+
366+
result, err := r.Client.GetBlockTemplate(req)
367+
368+
if err != nil {
369+
t.Fatalf("Expected getblocktemplate to succeed, got "+
370+
"error: %v", err)
371+
}
372+
373+
if result == nil {
374+
t.Fatal("Expected non-nil result")
375+
}
376+
377+
hasSegwitRule := false
378+
for _, rule := range result.Rules {
379+
if rule == "segwit" || rule == "!segwit" {
380+
hasSegwitRule = true
381+
break
382+
}
383+
}
384+
385+
if !hasSegwitRule {
386+
t.Fatalf("Expected 'segwit' rule to be present in the response")
387+
}
388+
}
389+
390+
func testGetBlockTemplateResponseRules(r *rpctest.Harness, t *testing.T) {
391+
// Guarantee SegWit is fully active before testing
392+
ensureSegwitActive(r, t)
393+
394+
// Call getblocktemplate with 'segwit' rule when segwit is active
395+
req := &btcjson.TemplateRequest{
396+
Rules: []string{"segwit"},
397+
}
398+
399+
result, err := r.Client.GetBlockTemplate(req)
400+
401+
if err != nil {
402+
t.Fatalf("Expected getblocktemplate to succeed, got "+
403+
"error: %v", err)
404+
}
405+
406+
if result == nil {
407+
t.Fatal("Expected non-nil result")
408+
}
409+
410+
// Verify blockTemplateResult includes "!segwit" in Rules when
411+
// WitnessCommitment is non-nil and "segwit" when WitnessCommitment
412+
// is nil.
413+
hasNotSegwit := false
414+
hasSegwit := false
415+
416+
for _, rule := range result.Rules {
417+
if rule == "!segwit" {
418+
hasNotSegwit = true
419+
} else if rule == "segwit" {
420+
hasSegwit = true
421+
}
422+
}
423+
424+
if result.DefaultWitnessCommitment != "" {
425+
if !hasNotSegwit {
426+
t.Fatalf("Expected Rules to contain '!segwit' because" +
427+
" WitnessCommitment is present")
428+
}
429+
} else {
430+
if !hasSegwit {
431+
t.Fatalf("Expected Rules to contain 'segwit' because " +
432+
"WitnessCommitment is absent")
433+
}
434+
}
435+
}
436+
292437
var rpcTestCases = []rpctest.HarnessTestCase{
293438
testGetBestBlock,
294439
testGetBlockCount,
@@ -297,6 +442,9 @@ var rpcTestCases = []rpctest.HarnessTestCase{
297442
testGetNetworkHashPS,
298443
testGetNetworkHashPS2,
299444
testGetNetworkHashPS3,
445+
testGetBlockTemplateSegwitActiveNoRule,
446+
testGetBlockTemplateSegwitActiveWithRule,
447+
testGetBlockTemplateResponseRules,
300448
}
301449

302450
var primaryHarness *rpctest.Harness

rpcserver.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"net"
2121
"net/http"
2222
"os"
23+
"slices"
2324
"strconv"
2425
"strings"
2526
"sync"
@@ -1688,7 +1689,9 @@ func (state *gbtWorkState) updateBlockTemplate(s *rpcServer, useCoinbaseValue bo
16881689
// and returned to the caller.
16891690
//
16901691
// This function MUST be called with the state locked.
1691-
func (state *gbtWorkState) blockTemplateResult(useCoinbaseValue bool, submitOld *bool) (*btcjson.GetBlockTemplateResult, error) {
1692+
func (state *gbtWorkState) blockTemplateResult(
1693+
useCoinbaseValue bool, submitOld *bool,
1694+
segwitActive bool) (*btcjson.GetBlockTemplateResult, error) {
16921695
// Ensure the timestamps are still in valid range for the template.
16931696
// This should really only ever happen if the local clock is changed
16941697
// after the template is generated, but it's important to avoid serving
@@ -1789,6 +1792,10 @@ func (state *gbtWorkState) blockTemplateResult(useCoinbaseValue bool, submitOld
17891792
// data, then include the witness commitment in the GBT result.
17901793
if template.WitnessCommitment != nil {
17911794
reply.DefaultWitnessCommitment = hex.EncodeToString(template.WitnessCommitment)
1795+
reply.Rules = append(reply.Rules, "!segwit")
1796+
1797+
} else if segwitActive {
1798+
reply.Rules = append(reply.Rules, "segwit")
17921799
}
17931800

17941801
if useCoinbaseValue {
@@ -1839,7 +1846,9 @@ func (state *gbtWorkState) blockTemplateResult(useCoinbaseValue bool, submitOld
18391846
// has passed without finding a solution.
18401847
//
18411848
// See https://en.bitcoin.it/wiki/BIP_0022 for more details.
1842-
func handleGetBlockTemplateLongPoll(s *rpcServer, longPollID string, useCoinbaseValue bool, closeChan <-chan struct{}) (interface{}, error) {
1849+
func handleGetBlockTemplateLongPoll(
1850+
s *rpcServer, longPollID string, useCoinbaseValue bool,
1851+
closeChan <-chan struct{}, segwitActive bool) (interface{}, error) {
18431852
state := s.gbtWorkState
18441853
state.Lock()
18451854
// The state unlock is intentionally not deferred here since it needs to
@@ -1855,7 +1864,9 @@ func handleGetBlockTemplateLongPoll(s *rpcServer, longPollID string, useCoinbase
18551864
// the caller is invalid.
18561865
prevHash, lastGenerated, err := decodeTemplateID(longPollID)
18571866
if err != nil {
1858-
result, err := state.blockTemplateResult(useCoinbaseValue, nil)
1867+
result, err := state.blockTemplateResult(
1868+
useCoinbaseValue, nil, segwitActive,
1869+
)
18591870
if err != nil {
18601871
state.Unlock()
18611872
return nil, err
@@ -1877,7 +1888,7 @@ func handleGetBlockTemplateLongPoll(s *rpcServer, longPollID string, useCoinbase
18771888
// already been found and added to the block chain.
18781889
submitOld := prevHash.IsEqual(prevTemplateHash)
18791890
result, err := state.blockTemplateResult(useCoinbaseValue,
1880-
&submitOld)
1891+
&submitOld, segwitActive)
18811892
if err != nil {
18821893
state.Unlock()
18831894
return nil, err
@@ -1917,7 +1928,9 @@ func handleGetBlockTemplateLongPoll(s *rpcServer, longPollID string, useCoinbase
19171928
// block template depending on whether or not a solution has already
19181929
// been found and added to the block chain.
19191930
submitOld := prevHash.IsEqual(&state.template.Block.Header.PrevBlock)
1920-
result, err := state.blockTemplateResult(useCoinbaseValue, &submitOld)
1931+
result, err := state.blockTemplateResult(
1932+
useCoinbaseValue, &submitOld, segwitActive,
1933+
)
19211934
if err != nil {
19221935
return nil, err
19231936
}
@@ -1986,12 +1999,31 @@ func handleGetBlockTemplateRequest(s *rpcServer, request *btcjson.TemplateReques
19861999
}
19872000
}
19882001

2002+
segwitState, err := s.cfg.Chain.ThresholdState(
2003+
chaincfg.DeploymentSegwit,
2004+
)
2005+
if err != nil {
2006+
return nil, err
2007+
}
2008+
2009+
segwitActive := segwitState == blockchain.ThresholdActive
2010+
hasSegwitRule := request != nil && slices.Contains(
2011+
request.Rules, "segwit",
2012+
)
2013+
if segwitActive && !hasSegwitRule {
2014+
return nil, &btcjson.RPCError{
2015+
Code: btcjson.ErrRPCInvalidParameter,
2016+
Message: "Support for 'segwit' rule requires explicit" +
2017+
" client support",
2018+
}
2019+
}
2020+
19892021
// When a long poll ID was provided, this is a long poll request by the
19902022
// client to be notified when block template referenced by the ID should
19912023
// be replaced with a new one.
19922024
if request != nil && request.LongPollID != "" {
19932025
return handleGetBlockTemplateLongPoll(s, request.LongPollID,
1994-
useCoinbaseValue, closeChan)
2026+
useCoinbaseValue, closeChan, segwitActive)
19952027
}
19962028

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

20142046
// chainErrToGBTErrString converts an error returned from btcchain to a string

rpcserverhelp.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ var helpDescsEnUS = map[string]string{
340340
"getblocktemplateresult-reject-reason": "Reason the proposal was invalid as-is (only applies to proposal responses)",
341341
"getblocktemplateresult-default_witness_commitment": "The witness commitment itself. Will be populated if the block has witness data",
342342
"getblocktemplateresult-weightlimit": "The current limit on the max allowed weight of a block",
343+
"getblocktemplateresult-rules": "List of rules the server requires the client to understand and support",
343344

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

0 commit comments

Comments
 (0)