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 dcc97fc9..6d4c1f3f 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -20,6 +20,7 @@ import ( "net" "net/http" "os" + "slices" "strconv" "strings" "sync" @@ -1704,7 +1705,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 @@ -1805,6 +1808,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 { @@ -1855,7 +1862,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 @@ -1871,7 +1880,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 @@ -1892,8 +1903,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 @@ -1933,7 +1944,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 } @@ -2002,12 +2015,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. @@ -2024,7 +2056,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" +