From 48e403e8ea28b9e506d369c0a3cb0761719b7480 Mon Sep 17 00:00:00 2001 From: Techlateef Date: Mon, 6 Jul 2026 14:59:28 +0100 Subject: [PATCH] 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. --- btcjson/chainsvrresults.go | 2 + integration/rpcserver_test.go | 149 ++++++++++++++++++++++++++++++++++ rpcserver.go | 48 +++++++++-- rpcserverhelp.go | 1 + 4 files changed, 192 insertions(+), 8 deletions(-) diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 5cd1e907..bd2da2b4 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -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 diff --git a/integration/rpcserver_test.go b/integration/rpcserver_test.go index 06496446..94c98aa3 100644 --- a/integration/rpcserver_test.go +++ b/integration/rpcserver_test.go @@ -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" @@ -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" + + " 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, @@ -297,6 +443,9 @@ var rpcTestCases = []rpctest.HarnessTestCase{ testGetNetworkHashPS, testGetNetworkHashPS2, testGetNetworkHashPS3, + testGetBlockTemplateSegwitActiveNoRule, + testGetBlockTemplateSegwitActiveWithRule, + testGetBlockTemplateResponseRules, } var primaryHarness *rpctest.Harness diff --git a/rpcserver.go b/rpcserver.go index fb5f0665..fb5a89ae 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -20,6 +20,7 @@ import ( "net" "net/http" "os" + "slices" "strconv" "strings" "sync" @@ -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 @@ -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 { @@ -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 @@ -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 @@ -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 @@ -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 } @@ -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. @@ -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 diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 34a6c74a..c34e85f0 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -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" +