From 691aace07ee8f7e87147cfe5f6599b90e233f01e Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Mon, 21 Jul 2025 14:56:22 +0200 Subject: [PATCH 1/5] initial --- cli/nep_test/nep11_test.go | 2 +- cli/server/server.go | 183 ++++++++++++++++++++++++++++++++++ pkg/core/native/management.go | 21 ++++ 3 files changed, 205 insertions(+), 1 deletion(-) diff --git a/cli/nep_test/nep11_test.go b/cli/nep_test/nep11_test.go index bba2f705b9..c9e673b401 100644 --- a/cli/nep_test/nep11_test.go +++ b/cli/nep_test/nep11_test.go @@ -431,7 +431,7 @@ func TestNEP11_D_OwnerOf_BalanceOf_Transfer(t *testing.T) { require.Equal(t, 2, len(aer[0].Events)) nfsoMintEvent := aer[0].Events[1] require.Equal(t, "Transfer", nfsoMintEvent.Name) - tokenID, err := nfsoMintEvent.Item.Value().([]stackitem.Item)[3].TryBytes() + tokenID, err := z[3].TryBytes() require.NoError(t, err) require.NotNil(t, tokenID) return tokenID diff --git a/cli/server/server.go b/cli/server/server.go index 7469c35e49..b45ed25f43 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -17,16 +17,23 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/chaindump" + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/state" corestate "github.com/nspcc-dev/neo-go/pkg/core/stateroot" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/network" + "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/services/metrics" "github.com/nspcc-dev/neo-go/pkg/services/notary" "github.com/nspcc-dev/neo-go/pkg/services/oracle" "github.com/nspcc-dev/neo-go/pkg/services/rpcsrv" "github.com/nspcc-dev/neo-go/pkg/services/stateroot" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/urfave/cli/v2" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -82,6 +89,27 @@ func NewCommands() []*cli.Command { Usage: "Height of the state to reset DB to", Required: true, }) + var cfgDownloadFlags = slices.Clone(cfgFlags) + cfgDownloadFlags = append(cfgDownloadFlags, options.RPC...) + cfgDownloadFlags = append(cfgDownloadFlags, + &cli.UintFlag{ + Name: "height", + Usage: "Height at which to get the contract state for", + Required: false, + DefaultText: "latest", + }, + &cli.StringFlag{ + Name: "contract-hash", + Usage: "Script hash of the contract to download", + Required: true, + Aliases: []string{"c"}, + }, + &cli.BoolFlag{ + Name: "force", + Usage: "Overwrite local contract state", + Required: false, + }, + ) return []*cli.Command{ { Name: "node", @@ -94,6 +122,13 @@ func NewCommands() []*cli.Command { Name: "db", Usage: "Database manipulations", Subcommands: []*cli.Command{ + { + Name: "download-contract", + Usage: "Download a contract including storage from a remote chain into the local database", + UsageText: "neo-go db download-contract -c contract-hash -r endpoint [--height height] [--config-file] [--force]", + Action: downloadContract, + Flags: cfgDownloadFlags, + }, { Name: "dump", Usage: "Dump blocks (starting with the genesis or specified block) to the file", @@ -686,3 +721,151 @@ func Logo() string { /_/ |_/_____/\____/ \____/\____/ ` } + +func downloadContract(ctx *cli.Context) error { + if err := cmdargs.EnsureNone(ctx); err != nil { + return err + } + cfg, err := options.GetConfigFromContext(ctx) + if err != nil { + return cli.Exit(err, 1) + } + gctx, cancel := options.GetTimeoutContext(ctx) + defer cancel() + + c, err := options.GetRPCClient(gctx, ctx) + if err != nil { + return cli.Exit(err, 1) + } + + h := uint32(ctx.Uint("height")) + if h == 0 { + count, err := c.GetBlockCount() + if err != nil { + return cli.Exit(err, 1) + } + h = uint32(count) - 1 + } + + ch, err := util.Uint160DecodeStringLE(ctx.String("contract-hash")[2:]) + if err != nil { + return cli.Exit(fmt.Errorf("failed to decode contract hash: %v", err), 1) + } + + contractState, err := GetContractStateAtHeight(c, h, ch) + if err != nil { + return cli.Exit(err, 1) + } + + if contractState.ID < 0 { + return cli.Exit(fmt.Errorf("native contract download is not supported: %v", contractState.ID), 1) + } + + log, _, logCloser, err := options.HandleLoggingParams(ctx, cfg.ApplicationConfiguration) + if err != nil { + return cli.Exit(err, 1) + } + if logCloser != nil { + defer func() { _ = logCloser() }() + } + + chain, store, err := initBlockChain(cfg, log) + if err != nil { + return cli.Exit(fmt.Errorf("failed to create Blockchain instance: %w", err), 1) + } + + force := ctx.Bool("force") + existingState := chain.GetContractState(ch) + if existingState != nil && !force { + return cli.Exit(fmt.Errorf("contract already exists in the chain. Use --force to overwrite"), 1) + } + + d := dao.NewSimple(store, cfg.ProtocolConfiguration.StateRootInHeader) + + if !force { + // remote contract does not exist in chain by contract hash, but its ID can already be + // in use. + _, err := native.GetContractByID(d, contractState.ID) + if !errors.Is(err, storage.ErrKeyNotFound) { + // ID is in use, get a new one + newID, err := native.GetNextContractID(d) + if err != nil { + return err + } + contractState.ID = newID + } + } + + err = native.PutContractStateNoCache(d, contractState) + if err != nil { + return cli.Exit(fmt.Errorf("failed to put contract state: %w", err), 1) + } + + _, err = d.Persist() + if err != nil { + return cli.Exit(fmt.Errorf("failed to persist storage: %w", err), 1) + } + + nowstate := chain.GetContractState(ch) + if nowstate != nil { + fmt.Println("yeah") + } + + err = store.Close() + if err != nil { + return cli.Exit(fmt.Errorf("failed to close the DB: %w", err), 1) + } + + fmt.Printf("downloaded \"%s\" (0x%s)\n", contractState.Manifest.Name, contractState.Hash.StringLE()) + return nil +} + +// GetContractStateAtHeight gets the contract state for the given smart contract hash at the specific height. +func GetContractStateAtHeight( + client *rpcclient.Client, + height uint32, + hash util.Uint160, +) (*state.Contract, error) { + stateResponse, err := client.GetStateRootByHeight(height) + if err != nil { + return nil, fmt.Errorf("failed to get stateroot for height %d: %w", height, err) + } + + managementContract, err := util.Uint160DecodeBytesBE([]byte(management.Hash)) + if err != nil { + return nil, err + } + prefix := append([]byte{0x08}, hash.BytesBE()...) + states, err := client.FindStates( + stateResponse.Root, + managementContract, + prefix, + nil, + nil, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to fetch contract state for contract %s: %w", + hash.StringLE(), + err, + ) + } + if stateLen := len(states.Results); stateLen != 1 { + return nil, fmt.Errorf( + "unexpected response length for fetch contract contract. Expected 1 got %d", stateLen, + ) + } + + siArr, err := stackitem.DeserializeLimited(states.Results[0].Value, stackitem.MaxDeserialized*2) + if err != nil { + return nil, err + } + + var contractState state.Contract + err = contractState.FromStackItem(siArr) + if err != nil { + return nil, err + } + + return &contractState, nil +} diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index a0519ec173..4d3fea0ea5 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -802,6 +802,13 @@ func PutContractState(d *dao.Simple, cs *state.Contract) error { return putContractState(d, cs, true) } +// TODO: check with nspcc if we can update PutContractState with an extra param +// or else how to have the cache for ManagementContract initialized such that it doesn't panic +// trying to run markUpdated() +func PutContractStateNoCache(d *dao.Simple, cs *state.Contract) error { + return putContractState(d, cs, false) +} + // putContractState is an internal PutContractState representation. func putContractState(d *dao.Simple, cs *state.Contract, updateCache bool) error { key := MakeContractKey(cs.Hash) @@ -860,3 +867,17 @@ func checkScriptAndMethods(ic *interop.Context, script []byte, methods []manifes return nil } + +// TODO: decide how to best expose. There are too many options that for the time I picked a duplicate +// function to have something working and let nspcc decide their preferred exposing option +func GetNextContractID(d *dao.Simple) (int32, error) { + si := d.GetStorageItem(ManagementContractID, keyNextAvailableID) + if si == nil { + return 0, errors.New("nextAvailableID is not initialized") + } + id := bigint.FromBytes(si) + ret := int32(id.Int64()) + id.Add(id, intOne) + d.PutBigInt(ManagementContractID, keyNextAvailableID, id) + return ret, nil +} From e3888392bf7c1401b0987d69857df3493e446352 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Mon, 21 Jul 2025 15:26:53 +0200 Subject: [PATCH 2/5] use local contract ID if already exists by hash --- cli/server/server.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/server/server.go b/cli/server/server.go index b45ed25f43..0a8f27a419 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -776,8 +776,11 @@ func downloadContract(ctx *cli.Context) error { force := ctx.Bool("force") existingState := chain.GetContractState(ch) - if existingState != nil && !force { - return cli.Exit(fmt.Errorf("contract already exists in the chain. Use --force to overwrite"), 1) + if existingState != nil { + if !force { + return cli.Exit(fmt.Errorf("contract already exists in the chain. Use --force to overwrite"), 1) + } + contractState.ID = existingState.ID } d := dao.NewSimple(store, cfg.ProtocolConfiguration.StateRootInHeader) From c2764548bcdd9ec0e3625284dd4eaffbaf8b3b5b Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Mon, 21 Jul 2025 15:31:06 +0200 Subject: [PATCH 3/5] undo --- cli/nep_test/nep11_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/nep_test/nep11_test.go b/cli/nep_test/nep11_test.go index c9e673b401..bba2f705b9 100644 --- a/cli/nep_test/nep11_test.go +++ b/cli/nep_test/nep11_test.go @@ -431,7 +431,7 @@ func TestNEP11_D_OwnerOf_BalanceOf_Transfer(t *testing.T) { require.Equal(t, 2, len(aer[0].Events)) nfsoMintEvent := aer[0].Events[1] require.Equal(t, "Transfer", nfsoMintEvent.Name) - tokenID, err := z[3].TryBytes() + tokenID, err := nfsoMintEvent.Item.Value().([]stackitem.Item)[3].TryBytes() require.NoError(t, err) require.NotNil(t, tokenID) return tokenID From 75ae1141e654a542920d8240a79c1731675e229b Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Tue, 22 Jul 2025 11:48:31 +0200 Subject: [PATCH 4/5] also fetch & store contract storages --- cli/server/server.go | 96 +++++++++++++++++++++++++++-------- pkg/core/native/management.go | 4 +- 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/cli/server/server.go b/cli/server/server.go index 0a8f27a419..60e8ff1408 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -18,6 +18,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/chaindump" "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/mpt" "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/state" corestate "github.com/nspcc-dev/neo-go/pkg/core/stateroot" @@ -25,6 +26,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/network" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/services/metrics" @@ -749,16 +751,7 @@ func downloadContract(ctx *cli.Context) error { ch, err := util.Uint160DecodeStringLE(ctx.String("contract-hash")[2:]) if err != nil { - return cli.Exit(fmt.Errorf("failed to decode contract hash: %v", err), 1) - } - - contractState, err := GetContractStateAtHeight(c, h, ch) - if err != nil { - return cli.Exit(err, 1) - } - - if contractState.ID < 0 { - return cli.Exit(fmt.Errorf("native contract download is not supported: %v", contractState.ID), 1) + return cli.Exit(fmt.Errorf("failed to decode contract hash: %w", err), 1) } log, _, logCloser, err := options.HandleLoggingParams(ctx, cfg.ApplicationConfiguration) @@ -774,6 +767,11 @@ func downloadContract(ctx *cli.Context) error { return cli.Exit(fmt.Errorf("failed to create Blockchain instance: %w", err), 1) } + contractState, contractStorage, err := getContractStateAndStorageAtHeight(c, h, ch) + if err != nil { + return cli.Exit(err, 1) + } + force := ctx.Bool("force") existingState := chain.GetContractState(ch) if existingState != nil { @@ -799,6 +797,10 @@ func downloadContract(ctx *cli.Context) error { } } + for _, pair := range contractStorage { + d.PutStorageItem(contractState.ID, pair.Key, pair.Value) + } + err = native.PutContractStateNoCache(d, contractState) if err != nil { return cli.Exit(fmt.Errorf("failed to put contract state: %w", err), 1) @@ -809,38 +811,54 @@ func downloadContract(ctx *cli.Context) error { return cli.Exit(fmt.Errorf("failed to persist storage: %w", err), 1) } - nowstate := chain.GetContractState(ch) - if nowstate != nil { - fmt.Println("yeah") - } - err = store.Close() if err != nil { return cli.Exit(fmt.Errorf("failed to close the DB: %w", err), 1) } - fmt.Printf("downloaded \"%s\" (0x%s)\n", contractState.Manifest.Name, contractState.Hash.StringLE()) + fmt.Printf("downloaded \"%s\" contract (0x%s) and %d storage records\n", contractState.Manifest.Name, contractState.Hash.StringLE(), len(contractStorage)) return nil } -// GetContractStateAtHeight gets the contract state for the given smart contract hash at the specific height. -func GetContractStateAtHeight( +func getContractStateAndStorageAtHeight( client *rpcclient.Client, height uint32, hash util.Uint160, -) (*state.Contract, error) { +) (*state.Contract, []result.KeyValue, error) { stateResponse, err := client.GetStateRootByHeight(height) if err != nil { - return nil, fmt.Errorf("failed to get stateroot for height %d: %w", height, err) + return nil, nil, fmt.Errorf("failed to get stateroot for height %d: %w", height, err) + } + + cState, err := getContractStateHistoric(client, stateResponse.Root, hash) + if err != nil { + return nil, nil, fmt.Errorf("failed to contract state: %w", err) + } + + if cState.ID < 0 { + return nil, nil, fmt.Errorf("native contract download is not supported: %v", cState.ID) + } + + states, err := getContractStorageHistoric(client, stateResponse.Root, hash) + if err != nil { + return nil, nil, fmt.Errorf("failed to get storage: %w", err) } + return cState, states, nil +} + +func getContractStateHistoric( + client *rpcclient.Client, + stateRoot util.Uint256, + hash util.Uint160, +) (*state.Contract, error) { managementContract, err := util.Uint160DecodeBytesBE([]byte(management.Hash)) if err != nil { return nil, err } prefix := append([]byte{0x08}, hash.BytesBE()...) states, err := client.FindStates( - stateResponse.Root, + stateRoot, managementContract, prefix, nil, @@ -872,3 +890,39 @@ func GetContractStateAtHeight( return &contractState, nil } + +func getContractStorageHistoric( + client *rpcclient.Client, + stateRoot util.Uint256, + hash util.Uint160, +) ([]result.KeyValue, error) { + var start []byte + var states []result.KeyValue + for { + response, err := client.FindStates(stateRoot, hash, nil, start, nil) + if err != nil { + return nil, err + } + if len(response.Results) == 0 { + break + } + + if _, ok := mpt.VerifyProof(stateRoot, response.FirstProof.Key, response.FirstProof.Proof); !ok { + return nil, fmt.Errorf("failed to verify first proof") + } + + if len(response.Results) > 1 { + if _, ok := mpt.VerifyProof(stateRoot, response.LastProof.Key, response.LastProof.Proof); !ok { + return nil, fmt.Errorf("failed to verify last proof") + } + } + + states = append(states, response.Results...) + if !response.Truncated { + break + } + start = response.Results[len(response.Results)-1].Key + } + + return states, nil +} diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 4d3fea0ea5..8f56605fb3 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -804,7 +804,7 @@ func PutContractState(d *dao.Simple, cs *state.Contract) error { // TODO: check with nspcc if we can update PutContractState with an extra param // or else how to have the cache for ManagementContract initialized such that it doesn't panic -// trying to run markUpdated() +// trying to run markUpdated(). func PutContractStateNoCache(d *dao.Simple, cs *state.Contract) error { return putContractState(d, cs, false) } @@ -869,7 +869,7 @@ func checkScriptAndMethods(ic *interop.Context, script []byte, methods []manifes } // TODO: decide how to best expose. There are too many options that for the time I picked a duplicate -// function to have something working and let nspcc decide their preferred exposing option +// function to have something working and let nspcc decide their preferred exposing option. func GetNextContractID(d *dao.Simple) (int32, error) { si := d.GetStorageItem(ManagementContractID, keyNextAvailableID) if si == nil { From e05183634f21ad3a31ccadbc86636877ccb6b975 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Tue, 22 Jul 2025 12:50:24 +0200 Subject: [PATCH 5/5] process some feedback --- cli/server/server.go | 52 ++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/cli/server/server.go b/cli/server/server.go index 60e8ff1408..2ab0403c66 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -10,6 +10,7 @@ import ( "syscall" "github.com/nspcc-dev/neo-go/cli/cmdargs" + "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" @@ -20,11 +21,11 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/mpt" "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativehashes" "github.com/nspcc-dev/neo-go/pkg/core/state" corestate "github.com/nspcc-dev/neo-go/pkg/core/stateroot" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/network" @@ -100,9 +101,9 @@ func NewCommands() []*cli.Command { Required: false, DefaultText: "latest", }, - &cli.StringFlag{ - Name: "contract-hash", - Usage: "Script hash of the contract to download", + &flags.AddressFlag{ + Name: "contract", + Usage: "Contract hash or address to download", Required: true, Aliases: []string{"c"}, }, @@ -127,7 +128,7 @@ func NewCommands() []*cli.Command { { Name: "download-contract", Usage: "Download a contract including storage from a remote chain into the local database", - UsageText: "neo-go db download-contract -c contract-hash -r endpoint [--height height] [--config-file] [--force]", + UsageText: "neo-go db download-contract -c contract -r endpoint [--height height] [--config-file] [--force]", Action: downloadContract, Flags: cfgDownloadFlags, }, @@ -749,8 +750,11 @@ func downloadContract(ctx *cli.Context) error { h = uint32(count) - 1 } - ch, err := util.Uint160DecodeStringLE(ctx.String("contract-hash")[2:]) - if err != nil { + var ch util.Uint160 + addrFlag := ctx.Generic("contract").(*flags.Address) + if addrFlag.IsSet { + ch = addrFlag.Uint160() + } else { return cli.Exit(fmt.Errorf("failed to decode contract hash: %w", err), 1) } @@ -784,10 +788,12 @@ func downloadContract(ctx *cli.Context) error { d := dao.NewSimple(store, cfg.ProtocolConfiguration.StateRootInHeader) if !force { - // remote contract does not exist in chain by contract hash, but its ID can already be + // Remote contract does not exist in chain by contract hash, but its ID can already be // in use. - _, err := native.GetContractByID(d, contractState.ID) - if !errors.Is(err, storage.ErrKeyNotFound) { + _, err := chain.GetContractScriptHash(contractState.ID) + if err != nil && !errors.Is(err, storage.ErrKeyNotFound) { + return cli.Exit(err, 1) + } else { // ID is in use, get a new one newID, err := native.GetNextContractID(d) if err != nil { @@ -816,15 +822,11 @@ func downloadContract(ctx *cli.Context) error { return cli.Exit(fmt.Errorf("failed to close the DB: %w", err), 1) } - fmt.Printf("downloaded \"%s\" contract (0x%s) and %d storage records\n", contractState.Manifest.Name, contractState.Hash.StringLE(), len(contractStorage)) + fmt.Fprintf(ctx.App.Writer, `downloaded "%s" contract (0x%s) and %d storage records\n`, contractState.Manifest.Name, contractState.Hash.StringLE(), len(contractStorage)) return nil } -func getContractStateAndStorageAtHeight( - client *rpcclient.Client, - height uint32, - hash util.Uint160, -) (*state.Contract, []result.KeyValue, error) { +func getContractStateAndStorageAtHeight(client *rpcclient.Client, height uint32, hash util.Uint160) (*state.Contract, []result.KeyValue, error) { stateResponse, err := client.GetStateRootByHeight(height) if err != nil { return nil, nil, fmt.Errorf("failed to get stateroot for height %d: %w", height, err) @@ -847,29 +849,17 @@ func getContractStateAndStorageAtHeight( return cState, states, nil } -func getContractStateHistoric( - client *rpcclient.Client, - stateRoot util.Uint256, - hash util.Uint160, -) (*state.Contract, error) { - managementContract, err := util.Uint160DecodeBytesBE([]byte(management.Hash)) - if err != nil { - return nil, err - } +func getContractStateHistoric(client *rpcclient.Client, stateRoot util.Uint256, hash util.Uint160) (*state.Contract, error) { prefix := append([]byte{0x08}, hash.BytesBE()...) states, err := client.FindStates( stateRoot, - managementContract, + nativehashes.ContractManagement, prefix, nil, nil, ) if err != nil { - return nil, fmt.Errorf( - "failed to fetch contract state for contract %s: %w", - hash.StringLE(), - err, - ) + return nil, fmt.Errorf("failed to fetch contract state for contract %s: %w", hash.StringLE(), err) } if stateLen := len(states.Results); stateLen != 1 { return nil, fmt.Errorf(