diff --git a/CHANGELOG.md b/CHANGELOG.md index 4167b123a..0fe4991db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,10 @@ All notable changes to this project will be documented in this file. The format ## [Unreleased] +### Changed + +- Use Chronicle fork block height constants for UTXO heights in script validation instead of the current block height. The Chronicle fork activation heights (mainnet: 882687, testnet: 1621670, regtest: 15000) are now defined as `ChronicleForkBlock*` constants and used uniformly for all UTXO inputs passed to BDK's `VerifyScript`. This makes the validation behavior explicit and consistent — all UTXOs are treated under Chronicle rules (the most lenient), eliminating false negatives while accepting some expected false positives. ARC serves as a pre-check; the node performs authoritative validation. + ## [1.5.3] - 2025-10-22 ### Changed diff --git a/cmd/services/api.go b/cmd/services/api.go index d6eef4151..9e7cc66e0 100644 --- a/cmd/services/api.go +++ b/cmd/services/api.go @@ -180,7 +180,6 @@ func StartAPIServer(logger *slog.Logger, apiCfg *config.APIConfig, commonCfg *co cachedFinder := txfinder.NewCached(finder, cachedFinderOpts...) var network string - var genesisBlock int32 var chainTracker beefValidator.ChainTracker bhsDefined := len(apiCfg.MerkleRootVerification.BlockHeaderServices) != 0 @@ -199,39 +198,43 @@ func StartAPIServer(logger *slog.Logger, apiCfg *config.APIConfig, commonCfg *co stoppable = append(stoppable, merkleVerifierClient) } + var chronicleForkBlock int32 + switch commonCfg.Network { case "testnet": network = "test" + chronicleForkBlock = apiHandler.ChronicleForkBlockTest if !bhsDefined { chainTracker = chaintracker.NewWhatsOnChain(chaintracker.TestNet, apiCfg.WocAPIKey) } - genesisBlock = apiHandler.GenesisForkBlockTest case "mainnet": network = "main" + chronicleForkBlock = apiHandler.ChronicleForkBlockMain if !bhsDefined { chainTracker = chaintracker.NewWhatsOnChain(chaintracker.MainNet, apiCfg.WocAPIKey) } - genesisBlock = apiHandler.GenesisForkBlockMain case "regtest": network = "regtest" + chronicleForkBlock = apiHandler.ChronicleForkBlockRegtest if !bhsDefined { chainTracker = merkle_verifier.New(global.MerkleRootsVerifier(blockTxClient), blockTxClient) } - genesisBlock = apiHandler.GenesisForkBlockRegtest default: stopFn() return nil, fmt.Errorf("invalid network type: %s", commonCfg.Network) } + defaultValidatorOpts = append(defaultValidatorOpts, defaultValidator.WithChronicleForkBlock(chronicleForkBlock)) + beefValidatorOpts = append(beefValidatorOpts, beefValidator.WithChronicleForkBlock(chronicleForkBlock)) + dv := defaultValidator.New( policy, cachedFinder, goscript.NewScriptEngine(network), - genesisBlock, defaultValidatorOpts..., ) - bv := beefValidator.New(policy, chainTracker, goscript.NewScriptEngine(network), genesisBlock, beefValidatorOpts...) + bv := beefValidator.New(policy, chainTracker, goscript.NewScriptEngine(network), beefValidatorOpts...) defaultAPIHandler, err := apiHandler.NewDefault(logger, mtmClient, blockTxClient, policy, dv, bv, apiOpts...) if err != nil { diff --git a/examples/custom/main.go b/examples/custom/main.go index db971ef12..a5a780077 100644 --- a/examples/custom/main.go +++ b/examples/custom/main.go @@ -103,15 +103,18 @@ func main() { blockTxClient := blocktx.NewClient(blocktx_api.NewBlockTxAPIClient(btcConn)) network := arcConfig.Common.Network - genesisBlock := apiHandler.GenesisForkBlockRegtest + var chronicleForkBlock int32 switch arcConfig.Common.Network { case "testnet": network = "test" - genesisBlock = apiHandler.GenesisForkBlockTest + chronicleForkBlock = apiHandler.ChronicleForkBlockTest case "mainnet": network = "main" - genesisBlock = apiHandler.GenesisForkBlockMain + chronicleForkBlock = apiHandler.ChronicleForkBlockMain + case "regtest": + chronicleForkBlock = apiHandler.ChronicleForkBlockRegtest default: + panic(fmt.Sprintf("unsupported network: %s", arcConfig.Common.Network)) } se := goscript.NewScriptEngine(network) @@ -136,14 +139,14 @@ func main() { arcConfig.API.DefaultPolicy, cachedFinder, se, - genesisBlock, + defaultValidator.WithChronicleForkBlock(chronicleForkBlock), ) // initialise the arc default api handler, with our txHandler and any handler options var handler api.ServerInterface chainTrackerMock := &apimocks.ChainTrackerMock{} - bv := beefValidator.New(arcConfig.API.DefaultPolicy, chainTrackerMock, se, genesisBlock) + bv := beefValidator.New(arcConfig.API.DefaultPolicy, chainTrackerMock, se, beefValidator.WithChronicleForkBlock(chronicleForkBlock)) defaultHandler, err := apiHandler.NewDefault(logger, metamorphClient, blockTxClient, arcConfig.API.DefaultPolicy, dv, bv) if err != nil { panic(err) diff --git a/go.mod b/go.mod index 074bc0bff..1bcf2e42e 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/bitcoin-sv/block-headers-service v1.1.1 github.com/bitcoinsv/bsvutil v0.0.0-20181216182056-1d77cf353ea9 github.com/bsv-blockchain/go-bt/v2 v2.5.1 - github.com/bsv-blockchain/go-sdk v1.2.12 + github.com/bsv-blockchain/go-sdk v1.2.20 github.com/cbeuw/connutil v1.0.1 github.com/ccoveille/go-safecast/v2 v2.0.0 github.com/cenkalti/backoff/v4 v4.3.0 @@ -53,8 +53,8 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 go.opentelemetry.io/otel/sdk/metric v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 - golang.org/x/net v0.47.0 - golang.org/x/sync v0.18.0 + golang.org/x/net v0.51.0 + golang.org/x/sync v0.20.0 google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 @@ -170,11 +170,11 @@ require ( go.opentelemetry.io/proto/otlp v1.8.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/oauth2 v0.31.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect diff --git a/go.sum b/go.sum index 75af3a960..550be7e27 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/bitcoinsv/bsvutil v0.0.0-20181216182056-1d77cf353ea9/go.mod h1:p44KuN github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bsv-blockchain/go-bt/v2 v2.5.1 h1:FM5JfBwzmBSlgrlR2vJPMZqbZ21uzQE3mT0irCZiOds= github.com/bsv-blockchain/go-bt/v2 v2.5.1/go.mod h1:PRmOtffMuHE1LZyRrb5rWQQ6dEEct2x68cvnsS7ka/4= -github.com/bsv-blockchain/go-sdk v1.2.12 h1:t/50ONqCTgumJH82YbQ8iqdo30ezIACyuFgvyHbkX9A= -github.com/bsv-blockchain/go-sdk v1.2.12/go.mod h1:1FWCWH+x6xc1kH9r6tuyRQqUomfrLBOQfdPesJZK/1k= +github.com/bsv-blockchain/go-sdk v1.2.20 h1:eifeQDIEFCGBpeFUo10sh3eGQVwXo4oE7yRdDwzjpn0= +github.com/bsv-blockchain/go-sdk v1.2.20/go.mod h1:5mmw1QLusuAkjWmQgUOurQYCXdIsQEsWXbAZ9zwme3g= github.com/cbeuw/connutil v1.0.1 h1:LWuNYjwm7JEDYG/ISAO1TfU4G+q2dA5NhR97eq2roCA= github.com/cbeuw/connutil v1.0.1/go.mod h1:lKofNtrW7Atmosgp1eNnTt2j2NjA2IkifapgLVI1QtA= github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0= @@ -388,45 +388,45 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/api/handler/default.go b/internal/api/handler/default.go index b8307966c..8ad3cc219 100644 --- a/internal/api/handler/default.go +++ b/internal/api/handler/default.go @@ -34,6 +34,10 @@ const ( GenesisForkBlockMain = int32(620539) GenesisForkBlockTest = int32(1344302) GenesisForkBlockRegtest = int32(10000) + + ChronicleForkBlockMain = int32(882687) + ChronicleForkBlockTest = int32(1621670) + ChronicleForkBlockRegtest = int32(15000) ) var ( diff --git a/internal/validator/beef/beef_validator.go b/internal/validator/beef/beef_validator.go index 8d601f8ee..b5e7042fb 100644 --- a/internal/validator/beef/beef_validator.go +++ b/internal/validator/beef/beef_validator.go @@ -37,12 +37,12 @@ type ChainTracker interface { } type Validator struct { - policy *bitcoin.Settings - chainTracker ChainTracker - scriptVerifier internalApi.ScriptVerifier - genesisForkBLock int32 - tracingEnabled bool - tracingAttributes []attribute.KeyValue + policy *bitcoin.Settings + chainTracker ChainTracker + scriptVerifier internalApi.ScriptVerifier + chronicleForkBlock int32 + tracingEnabled bool + tracingAttributes []attribute.KeyValue } type Option func(d *Validator) @@ -60,12 +60,17 @@ func WithTracer(attr ...attribute.KeyValue) func(s *Validator) { } } -func New(policy *bitcoin.Settings, chainTracker ChainTracker, sv internalApi.ScriptVerifier, genesisForkBLock int32, opts ...Option) *Validator { +func WithChronicleForkBlock(height int32) func(*Validator) { + return func(v *Validator) { + v.chronicleForkBlock = height + } +} + +func New(policy *bitcoin.Settings, chainTracker ChainTracker, sv internalApi.ScriptVerifier, opts ...Option) *Validator { v := &Validator{ - policy: policy, - chainTracker: chainTracker, - scriptVerifier: sv, - genesisForkBLock: genesisForkBLock, + policy: policy, + chainTracker: chainTracker, + scriptVerifier: sv, } // apply options for _, opt := range opts { @@ -108,7 +113,7 @@ func (v *Validator) ValidateTransaction(ctx context.Context, beefTx *sdkTx.Beef, } if scriptValidation == validator.StandardScriptValidation { - vErr = validateScripts(btx, v.scriptVerifier, blockHeight, v.genesisForkBLock) + vErr = validateScripts(btx, v.scriptVerifier, blockHeight, v.chronicleForkBlock) if vErr != nil { return tx, vErr } @@ -204,11 +209,23 @@ func cumulativeCheckFees(beefTx *sdkTx.Beef, feeModel *feemodel.SatoshisPerKilob return nil } -func validateScripts(beefTx *sdkTx.BeefTx, sv internalApi.ScriptVerifier, blockHeight int32, genesisForkBLock int32) *validator.Error { +func validateScripts(beefTx *sdkTx.BeefTx, sv internalApi.ScriptVerifier, blockHeight int32, chronicleForkBlock int32) *validator.Error { + if blockHeight <= 0 { + return validator.NewError(errors.New("block height not yet available"), api.ErrStatusGeneric) + } + if chronicleForkBlock <= 0 { + return validator.NewError(errors.New("chronicle fork block height not configured"), api.ErrStatusGeneric) + } + + // Use the Chronicle fork block height as the UTXO height for all inputs. Actual UTXO + // creation heights are not available in ARC's current architecture. The Chronicle fork + // height is used because all UTXOs are treated uniformly — Chronicle rules are the most + // lenient, so there are no false negatives. ARC serves as a pre-check; the node performs + // authoritative validation. tx := beefTx.Transaction utxo := make([]int32, len(tx.Inputs)) for i := range tx.Inputs { - utxo[i] = genesisForkBLock + utxo[i] = chronicleForkBlock } b, err := tx.EF() diff --git a/internal/validator/beef/beef_validator_test.go b/internal/validator/beef/beef_validator_test.go index 3541e091f..fd33a3894 100644 --- a/internal/validator/beef/beef_validator_test.go +++ b/internal/validator/beef/beef_validator_test.go @@ -121,7 +121,7 @@ func TestBeefValidator(t *testing.T) { } se := goscript.NewScriptEngine("regtest") - sut := New(getPolicy(1), ctMock, se, int32(10000)) + sut := New(getPolicy(1), ctMock, se, WithChronicleForkBlock(15000)) // when actualTx, err := sut.ValidateTransaction(context.TODO(), beefTx, validator.StandardFeeValidation, validator.StandardScriptValidation, 632099) @@ -183,7 +183,7 @@ func TestValidateScripts(t *testing.T) { continue } - actualError := validateScripts(btx, se, int32(10000), int32(10000)) + actualError := validateScripts(btx, se, int32(15000), int32(15000)) if tc.expectedError != nil { require.Equal(t, tc.expectedError, actualError) return diff --git a/internal/validator/defaultvalidator/default_validator.go b/internal/validator/defaultvalidator/default_validator.go index b012bbf4b..a5309eabb 100644 --- a/internal/validator/defaultvalidator/default_validator.go +++ b/internal/validator/defaultvalidator/default_validator.go @@ -28,16 +28,15 @@ type DefaultValidator struct { policy *bitcoin.Settings txFinder validator.TxFinderI scriptVerifier internalApi.ScriptVerifier - genesisForkBLock int32 + chronicleForkBlock int32 tracingEnabled bool tracingAttributes []attribute.KeyValue standardFormatSupported bool } -func New(policy *bitcoin.Settings, finder validator.TxFinderI, sv internalApi.ScriptVerifier, genesisForkBLock int32, opts ...Option) *DefaultValidator { +func New(policy *bitcoin.Settings, finder validator.TxFinderI, sv internalApi.ScriptVerifier, opts ...Option) *DefaultValidator { d := &DefaultValidator{ scriptVerifier: sv, - genesisForkBLock: genesisForkBLock, policy: policy, txFinder: finder, standardFormatSupported: true, @@ -57,6 +56,12 @@ func WithStandardFormatSupported(standardFormatSupported bool) func(*DefaultVali } } +func WithChronicleForkBlock(height int32) func(*DefaultValidator) { + return func(d *DefaultValidator) { + d.chronicleForkBlock = height + } +} + func WithTracer(attr ...attribute.KeyValue) func(s *DefaultValidator) { return func(a *DefaultValidator) { a.tracingEnabled = true @@ -138,9 +143,22 @@ func (v *DefaultValidator) ValidateTransaction(ctx context.Context, tx *sdkTx.Tr func (v *DefaultValidator) performStandardScriptValidation(scriptValidation validator.ScriptValidation, tx *sdkTx.Transaction, blockHeight int32) *validator.Error { //nolint: revive //false error thrown if scriptValidation == validator.StandardScriptValidation { + if blockHeight <= 0 { + return validator.NewError(errors.New("block height not yet available"), api.ErrStatusGeneric) + } + if v.chronicleForkBlock <= 0 { + return validator.NewError(errors.New("chronicle fork block height not configured"), api.ErrStatusGeneric) + } + + // Use the Chronicle fork block height as the UTXO height for all inputs. Actual UTXO + // creation heights are not available in ARC's current architecture (extendTx only fetches + // parent outputs, not their confirmation height). The Chronicle fork height is used because + // all UTXOs are treated uniformly — Chronicle rules are the most lenient, so there are no + // false negatives. ARC serves as a pre-check; the node performs authoritative validation. + utxoHeight := v.chronicleForkBlock utxo := make([]int32, len(tx.Inputs)) for i := range tx.Inputs { - utxo[i] = v.genesisForkBLock + utxo[i] = utxoHeight } b, err := tx.EF() diff --git a/internal/validator/defaultvalidator/default_validator_test.go b/internal/validator/defaultvalidator/default_validator_test.go index 29f1390ae..8da7495e9 100644 --- a/internal/validator/defaultvalidator/default_validator_test.go +++ b/internal/validator/defaultvalidator/default_validator_test.go @@ -123,7 +123,7 @@ func TestValidator(t *testing.T) { tx, _ := sdkTx.NewTransactionFromHex(tc.txHex) policy := getPolicy(tc.satPerKb) se := goscript.NewScriptEngine("main") - sut := New(policy, tc.finder, se, int32(632099), WithStandardFormatSupported(tc.stdFormatSupported)) + sut := New(policy, tc.finder, se, WithStandardFormatSupported(tc.stdFormatSupported), WithChronicleForkBlock(882687)) // when actualError := sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 632099) @@ -151,10 +151,10 @@ func TestValidator(t *testing.T) { require.NoError(t, err, "Could not parse tx hex") policy := getPolicy(5) se := goscript.NewScriptEngine("regtest") - sut := New(policy, nil, se, int32(10000)) + sut := New(policy, nil, se, WithChronicleForkBlock(15000)) // when - actualError := sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 10000) + actualError := sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 15000) // then require.NoError(t, actualError, "Failed to validate tx %d", txIndex) @@ -186,10 +186,10 @@ func TestValidator(t *testing.T) { policy := getPolicy(5) se := goscript.NewScriptEngine("regtest") - sut := New(policy, nil, se, int32(10000)) + sut := New(policy, nil, se, WithChronicleForkBlock(15000)) // when - actualError := sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 10000) + actualError := sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 15000) // then require.NoError(t, actualError, "Failed to validate tx") @@ -354,15 +354,31 @@ func TestCheckScripts(t *testing.T) { }) } +func TestPerformStandardScriptValidation_BlockHeightZero(t *testing.T) { + // given + tx, err := sdkTx.NewTransactionFromHex("020000000000000000ef010f117b3f9ea4955d5c592c61838bea10096fc88ac1ad08561a9bcabd715a088200000000494830450221008fd0e0330470ac730b9f6b9baf1791b76859cbc327e2e241f3ebeb96561a719602201e73532eb1312a00833af276d636254b8aa3ecbb445324fb4c481f2a493821fb41feffffff00f2052a01000000232103b12bda06e5a3e439690bf3996f1d4b81289f4747068a5cbb12786df83ae14c18ac02a0860100000000001976a914b7b88045cc16f442a0c3dcb3dc31ecce8d156e7388ac605c042a010000001976a9147a904b8ae0c2f9d74448993029ad3c040ebdd69a88ac66000000") + require.NoError(t, err) + policy := getPolicy(500) + se := goscript.NewScriptEngine("regtest") + sut := New(policy, nil, se, WithChronicleForkBlock(15000)) + + // when — block height 0 should be rejected + actualError := sut.ValidateTransaction(context.TODO(), tx, validator.NoneFeeValidation, validator.StandardScriptValidation, 0) + + // then + require.Error(t, actualError) + require.ErrorContains(t, actualError, "block height not yet available") +} + func BenchmarkValidator(b *testing.B) { // extended tx tx, _ := sdkTx.NewTransactionFromHex("020000000000000000ef010f117b3f9ea4955d5c592c61838bea10096fc88ac1ad08561a9bcabd715a088200000000494830450221008fd0e0330470ac730b9f6b9baf1791b76859cbc327e2e241f3ebeb96561a719602201e73532eb1312a00833af276d636254b8aa3ecbb445324fb4c481f2a493821fb41feffffff00f2052a01000000232103b12bda06e5a3e439690bf3996f1d4b81289f4747068a5cbb12786df83ae14c18ac02a0860100000000001976a914b7b88045cc16f442a0c3dcb3dc31ecce8d156e7388ac605c042a010000001976a9147a904b8ae0c2f9d74448993029ad3c040ebdd69a88ac66000000") policy := getPolicy(500) se := goscript.NewScriptEngine("regtest") - sut := New(policy, nil, se, int32(10000)) + sut := New(policy, nil, se, WithChronicleForkBlock(15000)) for i := 0; i < b.N; i++ { - _ = sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 10000) + _ = sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 15000) } } @@ -372,10 +388,10 @@ func TestFeeCalculation(t *testing.T) { require.NoError(t, err) policy := getPolicy(50) se := goscript.NewScriptEngine("regtest") - sut := New(policy, nil, se, int32(10000)) + sut := New(policy, nil, se, WithChronicleForkBlock(15000)) // when - err = sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 10000) + err = sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 15000) // then t.Log(err)