From 4ac776b2b3dade7fd29fc0bac004dcba1446e307 Mon Sep 17 00:00:00 2001 From: oskarszoon <1449115+oskarszoon@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:57:59 +0200 Subject: [PATCH 1/7] feat: Chronicle-ready script validation and go-sdk upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use current block height for UTXO heights in BDK script validation instead of hardcoding genesisForkBlock. The BDK script engine uses UTXO heights to determine which consensus flags apply per input — hardcoding to genesisForkBlock meant Chronicle-era rules (OTDA sighash) were never activated, requiring X-SkipScriptValidation as a workaround. BDK already has correct Chronicle activation heights per network (mainnet: 882687, testnet: 1621670, regtest: 15000), so passing the current block height enables Chronicle rules for post-activation UTXOs. Also upgrades go-sdk from v1.2.12 to v1.2.20 for Chronicle compatibility. --- cmd/services/api.go | 7 +--- examples/custom/main.go | 6 +--- go.mod | 14 ++++---- go.sum | 32 +++++++++---------- internal/validator/beef/beef_validator.go | 16 ++++------ .../validator/beef/beef_validator_test.go | 4 +-- .../defaultvalidator/default_validator.go | 6 ++-- .../default_validator_test.go | 10 +++--- 8 files changed, 41 insertions(+), 54 deletions(-) diff --git a/cmd/services/api.go b/cmd/services/api.go index d6eef4151..4aa2756f0 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 @@ -205,19 +204,16 @@ func StartAPIServer(logger *slog.Logger, apiCfg *config.APIConfig, commonCfg *co if !bhsDefined { chainTracker = chaintracker.NewWhatsOnChain(chaintracker.TestNet, apiCfg.WocAPIKey) } - genesisBlock = apiHandler.GenesisForkBlockTest case "mainnet": network = "main" if !bhsDefined { chainTracker = chaintracker.NewWhatsOnChain(chaintracker.MainNet, apiCfg.WocAPIKey) } - genesisBlock = apiHandler.GenesisForkBlockMain case "regtest": network = "regtest" 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) @@ -227,11 +223,10 @@ func StartAPIServer(logger *slog.Logger, apiCfg *config.APIConfig, commonCfg *co 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..d675611ae 100644 --- a/examples/custom/main.go +++ b/examples/custom/main.go @@ -103,14 +103,11 @@ func main() { blockTxClient := blocktx.NewClient(blocktx_api.NewBlockTxAPIClient(btcConn)) network := arcConfig.Common.Network - genesisBlock := apiHandler.GenesisForkBlockRegtest switch arcConfig.Common.Network { case "testnet": network = "test" - genesisBlock = apiHandler.GenesisForkBlockTest case "mainnet": network = "main" - genesisBlock = apiHandler.GenesisForkBlockMain default: } @@ -136,14 +133,13 @@ func main() { arcConfig.API.DefaultPolicy, cachedFinder, se, - genesisBlock, ) // 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) 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/validator/beef/beef_validator.go b/internal/validator/beef/beef_validator.go index 8d601f8ee..fa6c246a1 100644 --- a/internal/validator/beef/beef_validator.go +++ b/internal/validator/beef/beef_validator.go @@ -40,7 +40,6 @@ type Validator struct { policy *bitcoin.Settings chainTracker ChainTracker scriptVerifier internalApi.ScriptVerifier - genesisForkBLock int32 tracingEnabled bool tracingAttributes []attribute.KeyValue } @@ -60,12 +59,11 @@ func WithTracer(attr ...attribute.KeyValue) func(s *Validator) { } } -func New(policy *bitcoin.Settings, chainTracker ChainTracker, sv internalApi.ScriptVerifier, genesisForkBLock int32, opts ...Option) *Validator { +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 +106,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) if vErr != nil { return tx, vErr } @@ -204,11 +202,11 @@ 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) *validator.Error { tx := beefTx.Transaction utxo := make([]int32, len(tx.Inputs)) for i := range tx.Inputs { - utxo[i] = genesisForkBLock + utxo[i] = blockHeight } b, err := tx.EF() diff --git a/internal/validator/beef/beef_validator_test.go b/internal/validator/beef/beef_validator_test.go index 3541e091f..2ab8a3df4 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) // 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(10000)) 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..aeeffb4a2 100644 --- a/internal/validator/defaultvalidator/default_validator.go +++ b/internal/validator/defaultvalidator/default_validator.go @@ -28,16 +28,14 @@ type DefaultValidator struct { policy *bitcoin.Settings txFinder validator.TxFinderI scriptVerifier internalApi.ScriptVerifier - genesisForkBLock 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, @@ -140,7 +138,7 @@ func (v *DefaultValidator) performStandardScriptValidation(scriptValidation vali if scriptValidation == validator.StandardScriptValidation { utxo := make([]int32, len(tx.Inputs)) for i := range tx.Inputs { - utxo[i] = v.genesisForkBLock + utxo[i] = blockHeight } b, err := tx.EF() diff --git a/internal/validator/defaultvalidator/default_validator_test.go b/internal/validator/defaultvalidator/default_validator_test.go index 29f1390ae..689e93fce 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)) // when actualError := sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 632099) @@ -151,7 +151,7 @@ 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) // when actualError := sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 10000) @@ -186,7 +186,7 @@ func TestValidator(t *testing.T) { policy := getPolicy(5) se := goscript.NewScriptEngine("regtest") - sut := New(policy, nil, se, int32(10000)) + sut := New(policy, nil, se) // when actualError := sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 10000) @@ -359,7 +359,7 @@ func BenchmarkValidator(b *testing.B) { tx, _ := sdkTx.NewTransactionFromHex("020000000000000000ef010f117b3f9ea4955d5c592c61838bea10096fc88ac1ad08561a9bcabd715a088200000000494830450221008fd0e0330470ac730b9f6b9baf1791b76859cbc327e2e241f3ebeb96561a719602201e73532eb1312a00833af276d636254b8aa3ecbb445324fb4c481f2a493821fb41feffffff00f2052a01000000232103b12bda06e5a3e439690bf3996f1d4b81289f4747068a5cbb12786df83ae14c18ac02a0860100000000001976a914b7b88045cc16f442a0c3dcb3dc31ecce8d156e7388ac605c042a010000001976a9147a904b8ae0c2f9d74448993029ad3c040ebdd69a88ac66000000") policy := getPolicy(500) se := goscript.NewScriptEngine("regtest") - sut := New(policy, nil, se, int32(10000)) + sut := New(policy, nil, se) for i := 0; i < b.N; i++ { _ = sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 10000) @@ -372,7 +372,7 @@ 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) // when err = sut.ValidateTransaction(context.TODO(), tx, validator.StandardFeeValidation, validator.StandardScriptValidation, 10000) From 6df286ebc8575b6c6ff78b8abfb9adbb2f53fa1d Mon Sep 17 00:00:00 2001 From: oskarszoon <1449115+oskarszoon@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:17:35 +0200 Subject: [PATCH 2/7] fix: guard against zero blockHeight in script validation Add blockHeight <= 0 guard to both validators to prevent incorrect script validation during startup before the first block height update (~5 seconds). Previously masked by the hardcoded genesisForkBlock. Also documents the rationale for using current block height as UTXO height: actual UTXO creation heights are not available in ARC's current architecture, and this is safe because Chronicle only adds capabilities without removing existing ones. --- internal/validator/beef/beef_validator.go | 6 ++++++ internal/validator/defaultvalidator/default_validator.go | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/internal/validator/beef/beef_validator.go b/internal/validator/beef/beef_validator.go index fa6c246a1..fb718b105 100644 --- a/internal/validator/beef/beef_validator.go +++ b/internal/validator/beef/beef_validator.go @@ -203,6 +203,12 @@ func cumulativeCheckFees(beefTx *sdkTx.Beef, feeModel *feemodel.SatoshisPerKilob } func validateScripts(beefTx *sdkTx.BeefTx, sv internalApi.ScriptVerifier, blockHeight int32) *validator.Error { + if blockHeight <= 0 { + return validator.NewError(fmt.Errorf("block height not yet available"), api.ErrStatusGeneric) + } + + // Use current block height as the UTXO height for all inputs. See comment in + // DefaultValidator.performStandardScriptValidation for rationale. tx := beefTx.Transaction utxo := make([]int32, len(tx.Inputs)) for i := range tx.Inputs { diff --git a/internal/validator/defaultvalidator/default_validator.go b/internal/validator/defaultvalidator/default_validator.go index aeeffb4a2..8bfc80377 100644 --- a/internal/validator/defaultvalidator/default_validator.go +++ b/internal/validator/defaultvalidator/default_validator.go @@ -136,6 +136,15 @@ 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(fmt.Errorf("block height not yet available"), api.ErrStatusGeneric) + } + + // Use current 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). This is safe because Chronicle only adds new + // capabilities (OTDA sighash) without changing how pre-Chronicle scripts validate, and + // ARC serves as a pre-check — the node performs authoritative validation. utxo := make([]int32, len(tx.Inputs)) for i := range tx.Inputs { utxo[i] = blockHeight From 2eed01a15f9761d45b87c5052b3c958065db2097 Mon Sep 17 00:00:00 2001 From: oskarszoon <1449115+oskarszoon@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:20:45 +0200 Subject: [PATCH 3/7] fix: use errors.New instead of fmt.Errorf for static strings Fixes revive unnecessary-format lint errors in CI. --- internal/validator/beef/beef_validator.go | 2 +- internal/validator/defaultvalidator/default_validator.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/validator/beef/beef_validator.go b/internal/validator/beef/beef_validator.go index fb718b105..76fcb71f9 100644 --- a/internal/validator/beef/beef_validator.go +++ b/internal/validator/beef/beef_validator.go @@ -204,7 +204,7 @@ func cumulativeCheckFees(beefTx *sdkTx.Beef, feeModel *feemodel.SatoshisPerKilob func validateScripts(beefTx *sdkTx.BeefTx, sv internalApi.ScriptVerifier, blockHeight int32) *validator.Error { if blockHeight <= 0 { - return validator.NewError(fmt.Errorf("block height not yet available"), api.ErrStatusGeneric) + return validator.NewError(errors.New("block height not yet available"), api.ErrStatusGeneric) } // Use current block height as the UTXO height for all inputs. See comment in diff --git a/internal/validator/defaultvalidator/default_validator.go b/internal/validator/defaultvalidator/default_validator.go index 8bfc80377..388e5c89c 100644 --- a/internal/validator/defaultvalidator/default_validator.go +++ b/internal/validator/defaultvalidator/default_validator.go @@ -137,7 +137,7 @@ 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(fmt.Errorf("block height not yet available"), api.ErrStatusGeneric) + return validator.NewError(errors.New("block height not yet available"), api.ErrStatusGeneric) } // Use current block height as the UTXO height for all inputs. Actual UTXO creation From cfac71dd6fb043279b501c16aedc9b7d3b88bd5c Mon Sep 17 00:00:00 2001 From: Deggen Date: Tue, 31 Mar 2026 09:55:15 -0500 Subject: [PATCH 4/7] feat: use Chronicle fork block height constants for UTXO validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace per-UTXO current block height with network-specific Chronicle fork block constants (mainnet: 882687, testnet: 1621670, regtest: 15000). All UTXOs are now treated uniformly under Chronicle rules — the most lenient — eliminating false negatives. ARC does not know UTXO creation heights, so this makes the constant behavior explicit rather than masking it behind a per-input loop. --- CHANGELOG.md | 4 +++ cmd/services/api.go | 8 +++++ examples/custom/main.go | 9 +++++- internal/api/handler/default.go | 4 +++ internal/validator/beef/beef_validator.go | 30 ++++++++++++------- .../validator/beef/beef_validator_test.go | 2 +- .../defaultvalidator/default_validator.go | 20 +++++++++---- .../default_validator_test.go | 18 +++++------ 8 files changed, 68 insertions(+), 27 deletions(-) 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 4aa2756f0..9e7cc66e0 100644 --- a/cmd/services/api.go +++ b/cmd/services/api.go @@ -198,19 +198,24 @@ 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) } case "mainnet": network = "main" + chronicleForkBlock = apiHandler.ChronicleForkBlockMain if !bhsDefined { chainTracker = chaintracker.NewWhatsOnChain(chaintracker.MainNet, apiCfg.WocAPIKey) } case "regtest": network = "regtest" + chronicleForkBlock = apiHandler.ChronicleForkBlockRegtest if !bhsDefined { chainTracker = merkle_verifier.New(global.MerkleRootsVerifier(blockTxClient), blockTxClient) } @@ -219,6 +224,9 @@ func StartAPIServer(logger *slog.Logger, apiCfg *config.APIConfig, commonCfg *co 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, diff --git a/examples/custom/main.go b/examples/custom/main.go index d675611ae..b0640c267 100644 --- a/examples/custom/main.go +++ b/examples/custom/main.go @@ -103,12 +103,18 @@ func main() { blockTxClient := blocktx.NewClient(blocktx_api.NewBlockTxAPIClient(btcConn)) network := arcConfig.Common.Network + var chronicleForkBlock int32 switch arcConfig.Common.Network { case "testnet": network = "test" + chronicleForkBlock = apiHandler.ChronicleForkBlockTest case "mainnet": network = "main" + chronicleForkBlock = apiHandler.ChronicleForkBlockMain + case "regtest": + chronicleForkBlock = apiHandler.ChronicleForkBlockRegtest default: + chronicleForkBlock = apiHandler.ChronicleForkBlockRegtest } se := goscript.NewScriptEngine(network) @@ -133,13 +139,14 @@ func main() { arcConfig.API.DefaultPolicy, cachedFinder, se, + 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) + 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/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 76fcb71f9..6b7ea3827 100644 --- a/internal/validator/beef/beef_validator.go +++ b/internal/validator/beef/beef_validator.go @@ -37,11 +37,12 @@ type ChainTracker interface { } type Validator struct { - policy *bitcoin.Settings - chainTracker ChainTracker - scriptVerifier internalApi.ScriptVerifier - 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) @@ -59,6 +60,12 @@ func WithTracer(attr ...attribute.KeyValue) func(s *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, @@ -106,7 +113,7 @@ func (v *Validator) ValidateTransaction(ctx context.Context, beefTx *sdkTx.Beef, } if scriptValidation == validator.StandardScriptValidation { - vErr = validateScripts(btx, v.scriptVerifier, blockHeight) + vErr = validateScripts(btx, v.scriptVerifier, blockHeight, v.chronicleForkBlock) if vErr != nil { return tx, vErr } @@ -202,17 +209,20 @@ func cumulativeCheckFees(beefTx *sdkTx.Beef, feeModel *feemodel.SatoshisPerKilob return nil } -func validateScripts(beefTx *sdkTx.BeefTx, sv internalApi.ScriptVerifier, blockHeight 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) } - // Use current block height as the UTXO height for all inputs. See comment in - // DefaultValidator.performStandardScriptValidation for rationale. + // 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] = blockHeight + 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 2ab8a3df4..2d17668cd 100644 --- a/internal/validator/beef/beef_validator_test.go +++ b/internal/validator/beef/beef_validator_test.go @@ -183,7 +183,7 @@ func TestValidateScripts(t *testing.T) { continue } - actualError := validateScripts(btx, se, 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 388e5c89c..c034ef6a4 100644 --- a/internal/validator/defaultvalidator/default_validator.go +++ b/internal/validator/defaultvalidator/default_validator.go @@ -28,6 +28,7 @@ type DefaultValidator struct { policy *bitcoin.Settings txFinder validator.TxFinderI scriptVerifier internalApi.ScriptVerifier + chronicleForkBlock int32 tracingEnabled bool tracingAttributes []attribute.KeyValue standardFormatSupported bool @@ -55,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 @@ -140,14 +147,15 @@ func (v *DefaultValidator) performStandardScriptValidation(scriptValidation vali return validator.NewError(errors.New("block height not yet available"), api.ErrStatusGeneric) } - // Use current 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). This is safe because Chronicle only adds new - // capabilities (OTDA sighash) without changing how pre-Chronicle scripts validate, and - // ARC serves as a pre-check — the node performs authoritative validation. + // 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] = blockHeight + 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 689e93fce..b74495942 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, 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) + 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) + 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") @@ -359,10 +359,10 @@ func BenchmarkValidator(b *testing.B) { tx, _ := sdkTx.NewTransactionFromHex("020000000000000000ef010f117b3f9ea4955d5c592c61838bea10096fc88ac1ad08561a9bcabd715a088200000000494830450221008fd0e0330470ac730b9f6b9baf1791b76859cbc327e2e241f3ebeb96561a719602201e73532eb1312a00833af276d636254b8aa3ecbb445324fb4c481f2a493821fb41feffffff00f2052a01000000232103b12bda06e5a3e439690bf3996f1d4b81289f4747068a5cbb12786df83ae14c18ac02a0860100000000001976a914b7b88045cc16f442a0c3dcb3dc31ecce8d156e7388ac605c042a010000001976a9147a904b8ae0c2f9d74448993029ad3c040ebdd69a88ac66000000") policy := getPolicy(500) se := goscript.NewScriptEngine("regtest") - sut := New(policy, nil, se) + 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 +372,10 @@ func TestFeeCalculation(t *testing.T) { require.NoError(t, err) policy := getPolicy(50) se := goscript.NewScriptEngine("regtest") - sut := New(policy, nil, se) + 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) From c157832d960587feed08b87d08f4368055869667 Mon Sep 17 00:00:00 2001 From: Deggen Date: Tue, 31 Mar 2026 09:58:20 -0500 Subject: [PATCH 5/7] test: cover blockHeight<=0 guard and BEEF validator options for SonarCloud coverage Add test for the zero block height error path in DefaultValidator script validation, and pass WithChronicleForkBlock option in BEEF validator tests to cover the options loop in New(). --- internal/validator/beef/beef_validator_test.go | 2 +- .../defaultvalidator/default_validator_test.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/validator/beef/beef_validator_test.go b/internal/validator/beef/beef_validator_test.go index 2d17668cd..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) + sut := New(getPolicy(1), ctMock, se, WithChronicleForkBlock(15000)) // when actualTx, err := sut.ValidateTransaction(context.TODO(), beefTx, validator.StandardFeeValidation, validator.StandardScriptValidation, 632099) diff --git a/internal/validator/defaultvalidator/default_validator_test.go b/internal/validator/defaultvalidator/default_validator_test.go index b74495942..8da7495e9 100644 --- a/internal/validator/defaultvalidator/default_validator_test.go +++ b/internal/validator/defaultvalidator/default_validator_test.go @@ -354,6 +354,22 @@ 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") From baeaa582f08ee34e5235f64ff311a1653ba1421e Mon Sep 17 00:00:00 2001 From: Deggen Date: Tue, 31 Mar 2026 11:16:49 -0500 Subject: [PATCH 6/7] fix: merge identical switch branches in examples/custom to satisfy revive --- examples/custom/main.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/custom/main.go b/examples/custom/main.go index b0640c267..dc1f4a39a 100644 --- a/examples/custom/main.go +++ b/examples/custom/main.go @@ -111,9 +111,7 @@ func main() { case "mainnet": network = "main" chronicleForkBlock = apiHandler.ChronicleForkBlockMain - case "regtest": - chronicleForkBlock = apiHandler.ChronicleForkBlockRegtest - default: + default: // includes regtest chronicleForkBlock = apiHandler.ChronicleForkBlockRegtest } From cddf582d1db8ea781442a77936ba90484147216b Mon Sep 17 00:00:00 2001 From: oskarszoon <1449115+oskarszoon@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:46:06 +0200 Subject: [PATCH 7/7] fix: guard against unconfigured chronicleForkBlock and tighten example network switch Add chronicleForkBlock <= 0 guard in both validators to catch misconfiguration where WithChronicleForkBlock option is not provided. Make examples/custom/main.go panic on unsupported network instead of silently falling through to regtest defaults. --- examples/custom/main.go | 4 +++- internal/validator/beef/beef_validator.go | 3 +++ internal/validator/defaultvalidator/default_validator.go | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/custom/main.go b/examples/custom/main.go index dc1f4a39a..a5a780077 100644 --- a/examples/custom/main.go +++ b/examples/custom/main.go @@ -111,8 +111,10 @@ func main() { case "mainnet": network = "main" chronicleForkBlock = apiHandler.ChronicleForkBlockMain - default: // includes regtest + case "regtest": chronicleForkBlock = apiHandler.ChronicleForkBlockRegtest + default: + panic(fmt.Sprintf("unsupported network: %s", arcConfig.Common.Network)) } se := goscript.NewScriptEngine(network) diff --git a/internal/validator/beef/beef_validator.go b/internal/validator/beef/beef_validator.go index 6b7ea3827..b5e7042fb 100644 --- a/internal/validator/beef/beef_validator.go +++ b/internal/validator/beef/beef_validator.go @@ -213,6 +213,9 @@ func validateScripts(beefTx *sdkTx.BeefTx, sv internalApi.ScriptVerifier, blockH 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 diff --git a/internal/validator/defaultvalidator/default_validator.go b/internal/validator/defaultvalidator/default_validator.go index c034ef6a4..a5309eabb 100644 --- a/internal/validator/defaultvalidator/default_validator.go +++ b/internal/validator/defaultvalidator/default_validator.go @@ -146,6 +146,9 @@ func (v *DefaultValidator) performStandardScriptValidation(scriptValidation vali 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