From f83a70e5190d5f76ed8239195ed4bf672208bc4e Mon Sep 17 00:00:00 2001 From: Rian Hughes Date: Tue, 15 Oct 2024 17:22:32 +0300 Subject: [PATCH 1/8] Fix traces (fees, state diff, events) (#2118) --- mocks/mock_vm.go | 4 ++-- node/throttled_vm.go | 8 ++++---- rpc/estimate_fee_test.go | 9 +++++---- rpc/simulation.go | 12 ++++++++---- rpc/simulation_test.go | 4 ++-- rpc/trace.go | 31 +++++-------------------------- rpc/trace_test.go | 4 ++-- vm/rust/src/juno_state_reader.rs | 12 +++++------- vm/rust/src/lib.rs | 12 +++++++----- vm/trace.go | 14 -------------- vm/vm.go | 24 +++++++++++++----------- 11 files changed, 53 insertions(+), 81 deletions(-) diff --git a/mocks/mock_vm.go b/mocks/mock_vm.go index 733ef7c20c..fb831acbc4 100644 --- a/mocks/mock_vm.go +++ b/mocks/mock_vm.go @@ -58,11 +58,11 @@ func (mr *MockVMMockRecorder) Call(arg0, arg1, arg2, arg3, arg4, arg5 any) *gomo } // Execute mocks base method. -func (m *MockVM) Execute(arg0 []core.Transaction, arg1 []core.Class, arg2 []*felt.Felt, arg3 *vm.BlockInfo, arg4 core.StateReader, arg5 *utils.Network, arg6, arg7, arg8, arg9 bool) ([]*felt.Felt, []*felt.Felt, []vm.TransactionTrace, uint64, error) { +func (m *MockVM) Execute(arg0 []core.Transaction, arg1 []core.Class, arg2 []*felt.Felt, arg3 *vm.BlockInfo, arg4 core.StateReader, arg5 *utils.Network, arg6, arg7, arg8, arg9 bool) ([]*felt.Felt, []core.GasConsumed, []vm.TransactionTrace, uint64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Execute", arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) ret0, _ := ret[0].([]*felt.Felt) - ret1, _ := ret[1].([]*felt.Felt) + ret1, _ := ret[1].([]core.GasConsumed) ret2, _ := ret[2].([]vm.TransactionTrace) ret3, _ := ret[3].(uint64) ret4, _ := ret[4].(error) diff --git a/node/throttled_vm.go b/node/throttled_vm.go index 6ae72af728..4612f3749e 100644 --- a/node/throttled_vm.go +++ b/node/throttled_vm.go @@ -33,14 +33,14 @@ func (tvm *ThrottledVM) Call(callInfo *vm.CallInfo, blockInfo *vm.BlockInfo, sta func (tvm *ThrottledVM) Execute(txns []core.Transaction, declaredClasses []core.Class, paidFeesOnL1 []*felt.Felt, blockInfo *vm.BlockInfo, state core.StateReader, network *utils.Network, skipChargeFee, skipValidate, errOnRevert, useBlobData bool, -) ([]*felt.Felt, []*felt.Felt, []vm.TransactionTrace, uint64, error) { +) ([]*felt.Felt, []core.GasConsumed, []vm.TransactionTrace, uint64, error) { var ret []*felt.Felt var traces []vm.TransactionTrace - var dataGasConsumed []*felt.Felt + var daGas []core.GasConsumed var numSteps uint64 - return ret, dataGasConsumed, traces, numSteps, tvm.Do(func(vm *vm.VM) error { + return ret, daGas, traces, numSteps, tvm.Do(func(vm *vm.VM) error { var err error - ret, dataGasConsumed, traces, numSteps, err = (*vm).Execute(txns, declaredClasses, paidFeesOnL1, blockInfo, state, network, + ret, daGas, traces, numSteps, err = (*vm).Execute(txns, declaredClasses, paidFeesOnL1, blockInfo, state, network, skipChargeFee, skipValidate, errOnRevert, useBlobData) return err }) diff --git a/rpc/estimate_fee_test.go b/rpc/estimate_fee_test.go index 660d3874d3..d075e5a0c7 100644 --- a/rpc/estimate_fee_test.go +++ b/rpc/estimate_fee_test.go @@ -59,7 +59,7 @@ func TestEstimateMessageFeeV0_6(t *testing.T) { }, gomock.Any(), &utils.Mainnet, gomock.Any(), false, true, false).DoAndReturn( func(txns []core.Transaction, declaredClasses []core.Class, paidFeesOnL1 []*felt.Felt, blockInfo *vm.BlockInfo, state core.StateReader, network *utils.Network, skipChargeFee, skipValidate, errOnRevert, useBlobData bool, - ) ([]*felt.Felt, []*felt.Felt, []vm.TransactionTrace, uint64, error) { + ) ([]*felt.Felt, []core.GasConsumed, []vm.TransactionTrace, uint64, error) { require.Len(t, txns, 1) assert.NotNil(t, txns[0].(*core.L1HandlerTransaction)) @@ -67,7 +67,8 @@ func TestEstimateMessageFeeV0_6(t *testing.T) { assert.Len(t, paidFeesOnL1, 1) actualFee := new(felt.Felt).Mul(expectedGasConsumed, blockInfo.Header.GasPrice) - return []*felt.Felt{actualFee}, []*felt.Felt{&felt.Zero}, []vm.TransactionTrace{{ + daGas := []core.GasConsumed{{L1Gas: 0, L1DataGas: 0}} + return []*felt.Felt{actualFee}, daGas, []vm.TransactionTrace{{ StateDiff: &vm.StateDiff{ StorageDiffs: []vm.StorageDiff{}, Nonces: []vm.Nonce{}, @@ -116,7 +117,7 @@ func TestEstimateFee(t *testing.T) { blockInfo := vm.BlockInfo{Header: &core.Header{}} t.Run("ok with zero values", func(t *testing.T) { mockVM.EXPECT().Execute([]core.Transaction{}, nil, []*felt.Felt{}, &blockInfo, mockState, n, true, false, true, true). - Return([]*felt.Felt{}, []*felt.Felt{}, []vm.TransactionTrace{}, uint64(123), nil) + Return([]*felt.Felt{}, []core.GasConsumed{}, []vm.TransactionTrace{}, uint64(123), nil) _, httpHeader, err := handler.EstimateFee([]rpc.BroadcastedTransaction{}, []rpc.SimulationFlag{}, rpc.BlockID{Latest: true}) require.Nil(t, err) @@ -125,7 +126,7 @@ func TestEstimateFee(t *testing.T) { t.Run("ok with zero values, skip validate", func(t *testing.T) { mockVM.EXPECT().Execute([]core.Transaction{}, nil, []*felt.Felt{}, &blockInfo, mockState, n, true, true, true, true). - Return([]*felt.Felt{}, []*felt.Felt{}, []vm.TransactionTrace{}, uint64(123), nil) + Return([]*felt.Felt{}, []core.GasConsumed{}, []vm.TransactionTrace{}, uint64(123), nil) _, httpHeader, err := handler.EstimateFee([]rpc.BroadcastedTransaction{}, []rpc.SimulationFlag{rpc.SkipValidateFlag}, rpc.BlockID{Latest: true}) require.Nil(t, err) diff --git a/rpc/simulation.go b/rpc/simulation.go index 63aee580ff..940fabdd90 100644 --- a/rpc/simulation.go +++ b/rpc/simulation.go @@ -114,7 +114,7 @@ func (h *Handler) simulateTransactions(id BlockID, transactions []BroadcastedTra BlockHashToBeRevealed: blockHashToBeRevealed, } useBlobData := !v0_6Response - overallFees, dataGasConsumed, traces, numSteps, err := h.vm.Execute(txns, classes, paidFeesOnL1, &blockInfo, + overallFees, daGas, traces, numSteps, err := h.vm.Execute(txns, classes, paidFeesOnL1, &blockInfo, state, h.bcReader.Network(), skipFeeCharge, skipValidate, errOnRevert, useBlobData) httpHeader.Set(ExecutionStepsHeader, strconv.FormatUint(numSteps, 10)) @@ -152,8 +152,9 @@ func (h *Handler) simulateTransactions(id BlockID, transactions []BroadcastedTra } var gasConsumed *felt.Felt + daGasL1DataGas := new(felt.Felt).SetUint64(daGas[i].L1DataGas) if !v0_6Response { - dataGasFee := new(felt.Felt).Mul(dataGasConsumed[i], dataGasPrice) + dataGasFee := new(felt.Felt).Mul(daGasL1DataGas, dataGasPrice) gasConsumed = new(felt.Felt).Sub(overallFee, dataGasFee) } else { gasConsumed = overallFee.Clone() @@ -163,7 +164,7 @@ func (h *Handler) simulateTransactions(id BlockID, transactions []BroadcastedTra estimate := FeeEstimate{ GasConsumed: gasConsumed, GasPrice: gasPrice, - DataGasConsumed: dataGasConsumed[i], + DataGasConsumed: daGasL1DataGas, DataGasPrice: dataGasPrice, OverallFee: overallFee, Unit: utils.Ptr(feeUnit), @@ -173,7 +174,10 @@ func (h *Handler) simulateTransactions(id BlockID, transactions []BroadcastedTra if !v0_6Response { trace := traces[i] executionResources := trace.TotalExecutionResources() - executionResources.DataAvailability = vm.NewDataAvailability(gasConsumed, dataGasConsumed[i], header.L1DAMode) + executionResources.DataAvailability = &vm.DataAvailability{ + L1Gas: daGas[i].L1Gas, + L1DataGas: daGas[i].L1DataGas, + } traces[i].ExecutionResources = executionResources } diff --git a/rpc/simulation_test.go b/rpc/simulation_test.go index c17929cfa7..d15f0908d1 100644 --- a/rpc/simulation_test.go +++ b/rpc/simulation_test.go @@ -39,7 +39,7 @@ func TestSimulateTransactionsV0_6(t *testing.T) { mockVM.EXPECT().Execute([]core.Transaction{}, nil, []*felt.Felt{}, &vm.BlockInfo{ Header: headsHeader, }, mockState, n, true, false, false, false). - Return([]*felt.Felt{}, []*felt.Felt{}, []vm.TransactionTrace{}, stepsUsed, nil) + Return([]*felt.Felt{}, []core.GasConsumed{}, []vm.TransactionTrace{}, stepsUsed, nil) _, httpHeader, err := handler.SimulateTransactionsV0_6(rpc.BlockID{Latest: true}, []rpc.BroadcastedTransaction{}, []rpc.SimulationFlag{rpc.SkipFeeChargeFlag}) require.Nil(t, err) @@ -51,7 +51,7 @@ func TestSimulateTransactionsV0_6(t *testing.T) { mockVM.EXPECT().Execute([]core.Transaction{}, nil, []*felt.Felt{}, &vm.BlockInfo{ Header: headsHeader, }, mockState, n, false, true, false, false). - Return([]*felt.Felt{}, []*felt.Felt{}, []vm.TransactionTrace{}, stepsUsed, nil) + Return([]*felt.Felt{}, []core.GasConsumed{}, []vm.TransactionTrace{}, stepsUsed, nil) _, httpHeader, err := handler.SimulateTransactionsV0_6(rpc.BlockID{Latest: true}, []rpc.BroadcastedTransaction{}, []rpc.SimulationFlag{rpc.SkipValidateFlag}) require.Nil(t, err) diff --git a/rpc/trace.go b/rpc/trace.go index 1bcb260d85..a61fa0451b 100644 --- a/rpc/trace.go +++ b/rpc/trace.go @@ -275,7 +275,7 @@ func (h *Handler) traceBlockTransactions(ctx context.Context, block *core.Block, } useBlobData := !v0_6Response - overallFees, dataGasConsumed, traces, numSteps, err := h.vm.Execute(block.Transactions, classes, paidFeesOnL1, + _, daGas, traces, numSteps, err := h.vm.Execute(block.Transactions, classes, paidFeesOnL1, &blockInfo, state, network, false, false, false, useBlobData) httpHeader.Set(ExecutionStepsHeader, strconv.FormatUint(numSteps, 10)) @@ -292,32 +292,11 @@ func (h *Handler) traceBlockTransactions(ctx context.Context, block *core.Block, result := make([]TracedBlockTransaction, 0, len(traces)) for index, trace := range traces { if !v0_6Response { - feeUnit := feeUnit(block.Transactions[index]) - - gasPrice := header.GasPrice - if feeUnit == FRI { - if gasPrice = header.GasPriceSTRK; gasPrice == nil { - gasPrice = &felt.Zero - } - } - - dataGasPrice := &felt.Zero - if header.L1DataGasPrice != nil { - switch feeUnit { - case FRI: - dataGasPrice = header.L1DataGasPrice.PriceInFri - case WEI: - dataGasPrice = header.L1DataGasPrice.PriceInWei - } - } - - dataGasFee := new(felt.Felt).Mul(dataGasConsumed[index], dataGasPrice) - gasConsumed := new(felt.Felt).Sub(overallFees[index], dataGasFee) - gasConsumed = gasConsumed.Div(gasConsumed, gasPrice) // division by zero felt is zero felt - executionResources := trace.TotalExecutionResources() - executionResources.DataAvailability = vm.NewDataAvailability(gasConsumed, dataGasConsumed[index], - header.L1DAMode) + executionResources.DataAvailability = &vm.DataAvailability{ + L1Gas: daGas[index].L1Gas, + L1DataGas: daGas[index].L1DataGas, + } traces[index].ExecutionResources = executionResources } result = append(result, TracedBlockTransaction{ diff --git a/rpc/trace_test.go b/rpc/trace_test.go index a9fc4a47d5..945985e60b 100644 --- a/rpc/trace_test.go +++ b/rpc/trace_test.go @@ -158,7 +158,7 @@ func TestTraceTransaction(t *testing.T) { }`, executionResources) vmTrace := new(vm.TransactionTrace) require.NoError(t, json.Unmarshal(json.RawMessage(vmTraceJSON), vmTrace)) - consumedGas := []*felt.Felt{new(felt.Felt).SetUint64(1)} + consumedGas := []core.GasConsumed{{L1Gas: 1, L1DataGas: 0}} overallFee := []*felt.Felt{new(felt.Felt).SetUint64(1)} stepsUsed := uint64(123) stepsUsedStr := "123" @@ -249,7 +249,7 @@ func TestTraceTransaction(t *testing.T) { }`, executionResources) vmTrace := new(vm.TransactionTrace) require.NoError(t, json.Unmarshal(json.RawMessage(vmTraceJSON), vmTrace)) - consumedGas := []*felt.Felt{new(felt.Felt).SetUint64(1)} + consumedGas := []core.GasConsumed{{L1Gas: 1, L1DataGas: 0}} overallFee := []*felt.Felt{new(felt.Felt).SetUint64(1)} stepsUsed := uint64(123) stepsUsedStr := "123" diff --git a/vm/rust/src/juno_state_reader.rs b/vm/rust/src/juno_state_reader.rs index 71f4e575fa..df540005d7 100644 --- a/vm/rust/src/juno_state_reader.rs +++ b/vm/rust/src/juno_state_reader.rs @@ -208,19 +208,17 @@ pub fn class_info_from_json_str(raw_json: &str) -> Result Date: Wed, 9 Oct 2024 13:00:52 +0100 Subject: [PATCH 2/8] feat: Check for rust version in Makefile - Enhanced MSRV check in GitHub workflow to include Makefile updates. - Added a new target in Makefile to ensure Rust version compliance. - Updated dependencies and formatting targets to include Rust version check. - Fixed minor formatting issues in Makefile comments. --- .github/workflows/find-smallest-rust.yml | 10 +++++++++- Makefile | 15 +++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/find-smallest-rust.yml b/.github/workflows/find-smallest-rust.yml index 7ab4b2a148..02a39b4e25 100644 --- a/.github/workflows/find-smallest-rust.yml +++ b/.github/workflows/find-smallest-rust.yml @@ -37,6 +37,14 @@ jobs: issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: 'Minimum supported Rust version is `' + msrv + '`, previous `' + previous_msrv + '`. Please update the README file and this workflow.' + body: 'Minimum supported Rust version is `' + msrv + '`, previous `' + previous_msrv + '`. Please update the README file, the Makefile, and this workflow.' }) } + + - name: "Check README and Makefile" + run: | + set -euxo pipefail + echo "Checking if version is set in README" + cat README.md | grep "\[Rust\]" | grep "${{ steps.rust-version.outputs.highest-msrv }}" + echo "Checking makefile" + cat Makefile | grep "MINIMUM_RUST_VERSION = ${{ steps.rust-version.outputs.highest-msrv }}" diff --git a/Makefile b/Makefile index ff962a7b12..951eb6d93c 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,8 @@ endif ifeq ($(shell uname -s),Darwin) export CGO_LDFLAGS=-framework Foundation -framework SystemConfiguration - # Set macOS deployment target in order to avoid linker warnings linke - # "ld: warning: object file XX was built for newer macOS version (14.4) than being linked (14.0)" + # Set macOS deployment target in order to avoid linker warnings linke + # "ld: warning: object file XX was built for newer macOS version (14.4) than being linked (14.0)" export MACOSX_DEPLOYMENT_TARGET=$(shell sw_vers --productVersion) # for test-race we need to pass -ldflags to fix linker warnings on macOS @@ -31,7 +31,7 @@ endif MAKEFLAGS += -j$(NPROCS) -rustdeps: vm core-rust compiler +rustdeps: check-rust vm core-rust compiler juno: rustdeps ## compile @mkdir -p build @@ -41,6 +41,13 @@ juno-cached: @mkdir -p build @go build $(GO_TAGS) -ldflags="-X main.Version=$(shell git describe --tags)" -o build/juno ./cmd/juno/ + +MINIMUM_RUST_VERSION = 1.80.1 +CURR_RUST_VERSION = $(shell rustc --version | grep -o '[0-9.]\+' | head -n1) +check-rust: ## Ensure rust version is greater than minimum + @echo "Checking if current rust version >= $(MINIMUM_RUST_VERSION)" + @bash -c '[[ $(CURR_RUST_VERSION) < $(MINIMUM_RUST_VERSION) ]] && (echo "Rust version must be >= $(MINIMUM_RUST_VERSION). Found version $(CURR_RUST_VERSION)" && exit 1) || echo "Current rust version is $(CURR_RUST_VERSION)"' + vm: $(MAKE) -C vm/rust $(VM_TARGET) @@ -74,7 +81,7 @@ test-cover: clean-testcache rustdeps ## tests with coverage go test $(GO_TAGS) -coverpkg=./... -coverprofile=coverage/coverage.out -covermode=atomic ./... go tool cover -html=coverage/coverage.out -o coverage/coverage.html -install-deps: | install-gofumpt install-mockgen install-golangci-lint## install some project dependencies +install-deps: | install-gofumpt install-mockgen install-golangci-lint check-rust ## install some project dependencies install-gofumpt: go install mvdan.cc/gofumpt@latest From a1be2ebdec563c0e08189f4abc30dbbd07420b05 Mon Sep 17 00:00:00 2001 From: Mario Apra Date: Fri, 11 Oct 2024 07:56:32 +0100 Subject: [PATCH 3/8] chore: Fix how to send comments to github Added necessary permissions to the GitHub Actions workflow to allow writing comments on issues and pull requests. This enables automated feedback and notifications, improving workflow efficiency and communication. --- .github/workflows/find-smallest-rust.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/find-smallest-rust.yml b/.github/workflows/find-smallest-rust.yml index 02a39b4e25..4942f8c7a3 100644 --- a/.github/workflows/find-smallest-rust.yml +++ b/.github/workflows/find-smallest-rust.yml @@ -11,6 +11,11 @@ on: - starknet/compiler/rust/* workflow_dispatch: + +permissions: + issues: write # Required for sending comments + pull-requests: write # Required for sending comments + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From 935b903f8dcd1fa435aa365d0735bc7a99502300 Mon Sep 17 00:00:00 2001 From: Kirill Date: Wed, 16 Oct 2024 13:40:55 +0400 Subject: [PATCH 4/8] Add db revert cmd (#2216) --- cmd/juno/dbcmd.go | 94 ++++++++++++++++++++++++++++++++++------ cmd/juno/dbcmd_test.go | 66 +++++++++++++++++++++++----- docs/docs/configuring.md | 1 + 3 files changed, 136 insertions(+), 25 deletions(-) diff --git a/cmd/juno/dbcmd.go b/cmd/juno/dbcmd.go index be3d51b64e..4fe5cd3a81 100644 --- a/cmd/juno/dbcmd.go +++ b/cmd/juno/dbcmd.go @@ -15,6 +15,10 @@ import ( "github.com/spf13/cobra" ) +const ( + dbRevertToBlockF = "to-block" +) + type DBInfo struct { Network string `json:"network"` ChainHeight uint64 `json:"chain_height"` @@ -33,7 +37,7 @@ func DBCmd(defaultDBPath string) *cobra.Command { } dbCmd.PersistentFlags().String(dbPathF, defaultDBPath, dbPathUsage) - dbCmd.AddCommand(DBInfoCmd(), DBSizeCmd()) + dbCmd.AddCommand(DBInfoCmd(), DBSizeCmd(), DBRevertCmd()) return dbCmd } @@ -55,21 +59,29 @@ func DBSizeCmd() *cobra.Command { } } +func DBRevertCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "revert", + Short: "Revert current head to given position", + Long: `This subcommand revert all data related to all blocks till given so it becomes new head.`, + RunE: dbRevert, + } + cmd.Flags().Uint64(dbRevertToBlockF, 0, "New head (this block won't be reverted)") + + return cmd +} + func dbInfo(cmd *cobra.Command, args []string) error { dbPath, err := cmd.Flags().GetString(dbPathF) if err != nil { return err } - if _, err = os.Stat(dbPath); os.IsNotExist(err) { - fmt.Fprintln(cmd.OutOrStdout(), "Database path does not exist") - return nil - } - - database, err := pebble.New(dbPath) + database, err := openDB(dbPath) if err != nil { - return fmt.Errorf("open DB: %w", err) + return err } + defer database.Close() chain := blockchain.New(database, nil) info := DBInfo{} @@ -110,6 +122,50 @@ func dbInfo(cmd *cobra.Command, args []string) error { return nil } +func dbRevert(cmd *cobra.Command, args []string) error { + dbPath, err := cmd.Flags().GetString(dbPathF) + if err != nil { + return err + } + + revertToBlock, err := cmd.Flags().GetUint64(dbRevertToBlockF) + if err != nil { + return err + } + + if revertToBlock == 0 { + return fmt.Errorf("--%v cannot be 0", dbRevertToBlockF) + } + + database, err := openDB(dbPath) + if err != nil { + return err + } + defer database.Close() + + for { + chain := blockchain.New(database, nil) + head, err := chain.Head() + if err != nil { + return fmt.Errorf("failed to get the latest block information: %v", err) + } + + if head.Number == revertToBlock { + fmt.Fprintf(cmd.OutOrStdout(), "Successfully reverted all blocks to %d\n", revertToBlock) + break + } + + err = chain.RevertHead() + if err != nil { + return fmt.Errorf("failed to revert head at block %d: %v", head.Number, err) + } + + fmt.Fprintf(cmd.OutOrStdout(), "Reverted head at block %d\n", head.Number) + } + + return nil +} + func dbSize(cmd *cobra.Command, args []string) error { dbPath, err := cmd.Flags().GetString(dbPathF) if err != nil { @@ -120,15 +176,11 @@ func dbSize(cmd *cobra.Command, args []string) error { return fmt.Errorf("--%v cannot be empty", dbPathF) } - if _, err = os.Stat(dbPath); os.IsNotExist(err) { - fmt.Fprintln(cmd.OutOrStdout(), "Database path does not exist") - return nil - } - - pebbleDB, err := pebble.New(dbPath) + pebbleDB, err := openDB(dbPath) if err != nil { return err } + defer pebbleDB.Close() var ( totalSize utils.DataSize @@ -201,3 +253,17 @@ func getNetwork(head *core.Block, stateDiff *core.StateDiff) string { return "unknown" } + +func openDB(path string) (db.DB, error) { + _, err := os.Stat(path) + if os.IsNotExist(err) { + return nil, fmt.Errorf("database path does not exist") + } + + database, err := pebble.New(path) + if err != nil { + return nil, fmt.Errorf("failed to open db: %w", err) + } + + return database, nil +} diff --git a/cmd/juno/dbcmd_test.go b/cmd/juno/dbcmd_test.go index 3491923962..454774e85f 100644 --- a/cmd/juno/dbcmd_test.go +++ b/cmd/juno/dbcmd_test.go @@ -2,6 +2,7 @@ package main_test import ( "context" + "strconv" "testing" "github.com/NethermindEth/juno/blockchain" @@ -12,6 +13,7 @@ import ( adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" "github.com/NethermindEth/juno/utils" "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -27,27 +29,69 @@ func TestDBCmd(t *testing.T) { cmd := juno.DBSizeCmd() executeCmdInDB(t, cmd) }) + + t.Run("revert db by 1 block", func(t *testing.T) { + network := utils.Mainnet + + const ( + syncToBlock = uint64(2) + revertToBlock = syncToBlock - 1 + ) + + cmd := juno.DBRevertCmd() + cmd.Flags().String("db-path", "", "") + + dbPath := prepareDB(t, &network, syncToBlock) + + require.NoError(t, cmd.Flags().Set("db-path", dbPath)) + require.NoError(t, cmd.Flags().Set("to-block", strconv.Itoa(int(revertToBlock)))) + require.NoError(t, cmd.Execute()) + + // unfortunately we cannot use blockchain from prepareDB because + // inside revert cmd another pebble instance is used which will panic if there are other instances + // that use the same db path + db, err := pebble.New(dbPath) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + chain := blockchain.New(db, &network) + block, err := chain.Head() + require.NoError(t, err) + assert.Equal(t, revertToBlock, block.Number) + }) } func executeCmdInDB(t *testing.T, cmd *cobra.Command) { cmd.Flags().String("db-path", "", "") - client := feeder.NewTestClient(t, &utils.Mainnet) - gw := adaptfeeder.New(client) - block0, err := gw.BlockByNumber(context.Background(), 0) - require.NoError(t, err) + dbPath := prepareDB(t, &utils.Mainnet, 0) - stateUpdate0, err := gw.StateUpdate(context.Background(), 0) - require.NoError(t, err) + require.NoError(t, cmd.Flags().Set("db-path", dbPath)) + require.NoError(t, cmd.Execute()) +} + +func prepareDB(t *testing.T, network *utils.Network, syncToBlock uint64) string { + client := feeder.NewTestClient(t, network) + gw := adaptfeeder.New(client) dbPath := t.TempDir() testDB, err := pebble.New(dbPath) require.NoError(t, err) - chain := blockchain.New(testDB, &utils.Mainnet) - require.NoError(t, chain.Store(block0, &emptyCommitments, stateUpdate0, nil)) - testDB.Close() + chain := blockchain.New(testDB, network) - require.NoError(t, cmd.Flags().Set("db-path", dbPath)) - require.NoError(t, cmd.Execute()) + for blockNumber := uint64(0); blockNumber <= syncToBlock; blockNumber++ { + block, err := gw.BlockByNumber(context.Background(), blockNumber) + require.NoError(t, err) + + stateUpdate, err := gw.StateUpdate(context.Background(), blockNumber) + require.NoError(t, err) + + require.NoError(t, chain.Store(block, &emptyCommitments, stateUpdate, nil)) + } + require.NoError(t, testDB.Close()) + + return dbPath } diff --git a/docs/docs/configuring.md b/docs/docs/configuring.md index ebd3dc9dcb..f05104da4f 100644 --- a/docs/docs/configuring.md +++ b/docs/docs/configuring.md @@ -109,6 +109,7 @@ Juno provides several subcommands to perform specific tasks or operations. Here - `db`: Perform database-related operations - `db info`: Retrieve information about the database. - `db size`: Calculate database size information for each data type. + - `db revert`: Reverts the database to a specific block number. To use a subcommand, append it when running Juno: From ca006de578cfbc5a99dc13c2f751ed0cd03adcb7 Mon Sep 17 00:00:00 2001 From: Kirill Date: Wed, 16 Oct 2024 16:14:16 +0400 Subject: [PATCH 5/8] Replace UnmarshalJSON() with UnmarshalText() for transaction statuses (#2220) * Replace UnmarshalJSON() with UnmarshalText() for transaction statuses UnmarshalText avoids issues with forgetting quotes in JSON, making it simpler for parsing plain text values. --- starknet/transaction.go | 54 +++++++++++++++--------------------- starknet/transaction_test.go | 38 +++++++++++++++++-------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/starknet/transaction.go b/starknet/transaction.go index 3898e95e5b..d73095d071 100644 --- a/starknet/transaction.go +++ b/starknet/transaction.go @@ -16,16 +16,16 @@ const ( Rejected ) -func (es *ExecutionStatus) UnmarshalJSON(data []byte) error { - switch string(data) { - case `"SUCCEEDED"`: +func (es *ExecutionStatus) UnmarshalText(data []byte) error { + switch str := string(data); str { + case "SUCCEEDED": *es = Succeeded - case `"REVERTED"`: + case "REVERTED": *es = Reverted - case `"REJECTED"`: + case "REJECTED": *es = Rejected default: - return errors.New("unknown ExecutionStatus") + return fmt.Errorf("unknown ExecutionStatus %q", str) } return nil } @@ -39,18 +39,18 @@ const ( Received ) -func (fs *FinalityStatus) UnmarshalJSON(data []byte) error { - switch string(data) { - case `"ACCEPTED_ON_L2"`: +func (fs *FinalityStatus) UnmarshalText(data []byte) error { + switch str := string(data); str { + case "ACCEPTED_ON_L2": *fs = AcceptedOnL2 - case `"ACCEPTED_ON_L1"`: + case "ACCEPTED_ON_L1": *fs = AcceptedOnL1 - case `"NOT_RECEIVED"`: + case "NOT_RECEIVED": *fs = NotReceived - case `"RECEIVED"`: + case "RECEIVED": *fs = Received default: - return errors.New("unknown FinalityStatus") + return fmt.Errorf("unknown FinalityStatus %q", str) } return nil } @@ -83,24 +83,24 @@ func (t TransactionType) String() string { } } -func (t TransactionType) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf("%q", t)), nil +func (t TransactionType) MarshalText() ([]byte, error) { + return []byte(t.String()), nil } -func (t *TransactionType) UnmarshalJSON(data []byte) error { - switch string(data) { - case `"DECLARE"`: +func (t *TransactionType) UnmarshalText(data []byte) error { + switch str := string(data); str { + case "DECLARE": *t = TxnDeclare - case `"DEPLOY"`: + case "DEPLOY": *t = TxnDeploy - case `"DEPLOY_ACCOUNT"`: + case "DEPLOY_ACCOUNT": *t = TxnDeployAccount - case `"INVOKE"`, `"INVOKE_FUNCTION"`: + case "INVOKE", "INVOKE_FUNCTION": *t = TxnInvoke - case `"L1_HANDLER"`: + case "L1_HANDLER": *t = TxnL1Handler default: - return errors.New("unknown TransactionType") + return fmt.Errorf("unknown TransactionType %q", str) } return nil } @@ -139,14 +139,6 @@ func (r Resource) MarshalText() ([]byte, error) { } } -func (r Resource) MarshalJSON() ([]byte, error) { - result, err := r.MarshalText() - if err != nil { - return nil, err - } - return []byte(`"` + string(result) + `"`), nil -} - type DataAvailabilityMode uint32 const ( diff --git a/starknet/transaction_test.go b/starknet/transaction_test.go index d5224106ea..e3e5f81096 100644 --- a/starknet/transaction_test.go +++ b/starknet/transaction_test.go @@ -1,6 +1,7 @@ package starknet_test import ( + "encoding/json" "testing" "github.com/NethermindEth/juno/starknet" @@ -10,22 +11,35 @@ import ( func TestUnmarshalExecutionStatus(t *testing.T) { es := new(starknet.ExecutionStatus) - require.NoError(t, es.UnmarshalJSON([]byte(`"SUCCEEDED"`))) - assert.Equal(t, starknet.Succeeded, *es) - require.NoError(t, es.UnmarshalJSON([]byte(`"REVERTED"`))) - assert.Equal(t, starknet.Reverted, *es) - - require.ErrorContains(t, es.UnmarshalJSON([]byte("ABC")), "unknown ExecutionStatus") + cases := map[string]starknet.ExecutionStatus{ + "SUCCEEDED": starknet.Succeeded, + "REVERTED": starknet.Reverted, + "REJECTED": starknet.Rejected, + } + for str, expected := range cases { + quotedStr := `"` + str + `"` + require.NoError(t, json.Unmarshal([]byte(quotedStr), es)) + assert.Equal(t, expected, *es) + } + + require.ErrorContains(t, json.Unmarshal([]byte(`"ABC"`), es), "unknown ExecutionStatus") } func TestUnmarshalFinalityStatus(t *testing.T) { fs := new(starknet.FinalityStatus) - require.NoError(t, fs.UnmarshalJSON([]byte(`"ACCEPTED_ON_L1"`))) - assert.Equal(t, starknet.AcceptedOnL1, *fs) - - require.NoError(t, fs.UnmarshalJSON([]byte(`"ACCEPTED_ON_L2"`))) - assert.Equal(t, starknet.AcceptedOnL2, *fs) - require.ErrorContains(t, fs.UnmarshalJSON([]byte("ABC")), "unknown FinalityStatus") + cases := map[string]starknet.FinalityStatus{ + "ACCEPTED_ON_L2": starknet.AcceptedOnL2, + "ACCEPTED_ON_L1": starknet.AcceptedOnL1, + "NOT_RECEIVED": starknet.NotReceived, + "RECEIVED": starknet.Received, + } + for str, expected := range cases { + quotedStr := `"` + str + `"` + require.NoError(t, json.Unmarshal([]byte(quotedStr), fs)) + assert.Equal(t, expected, *fs) + } + + require.ErrorContains(t, json.Unmarshal([]byte(`"ABC"`), fs), "unknown FinalityStatus") } From b43c46f48189926713c58a9760168ef2496c3bff Mon Sep 17 00:00:00 2001 From: Rian Hughes Date: Wed, 16 Oct 2024 15:42:07 +0300 Subject: [PATCH 6/8] Support plugins (#2051) Co-authored-by: rian Co-authored-by: LordGhostX --- README.md | 1 + blockchain/blockchain.go | 18 +++++- cmd/juno/juno.go | 4 ++ core/state.go | 64 ++++++++++++++------- docs/docs/plugins.md | 82 ++++++++++++++++++++++++++ docs/sidebars.js | 1 + go.mod | 1 + go.sum | 2 + mocks/mock_plugin.go | 98 ++++++++++++++++++++++++++++++++ node/node.go | 14 +++++ plugin/plugin.go | 73 ++++++++++++++++++++++++ plugin/plugin_test.go | 79 +++++++++++++++++++++++++ sync/sync.go | 65 +++++++++++++++++++-- vm/rust/src/juno_state_reader.rs | 15 +++-- 14 files changed, 487 insertions(+), 30 deletions(-) create mode 100644 docs/docs/plugins.md create mode 100644 mocks/mock_plugin.go create mode 100644 plugin/plugin.go create mode 100644 plugin/plugin_test.go diff --git a/README.md b/README.md index bed33114d8..3eaeab1df9 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,7 @@ After following these steps, Juno should be up and running on your machine, util - Starknet state construction and storage using a path-based Merkle Patricia trie. - Feeder gateway synchronisation of Blocks, Transactions, Receipts, State Updates and Classes. - Block and Transaction hash verification. +- Plugins ## 🛣 Roadmap diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go index ee2411c471..4aa6659b98 100644 --- a/blockchain/blockchain.go +++ b/blockchain/blockchain.go @@ -828,6 +828,23 @@ func (b *Blockchain) RevertHead() error { return b.database.Update(b.revertHead) } +func (b *Blockchain) GetReverseStateDiff() (*core.StateDiff, error) { + var reverseStateDiff *core.StateDiff + return reverseStateDiff, b.database.View(func(txn db.Transaction) error { + blockNumber, err := chainHeight(txn) + if err != nil { + return err + } + stateUpdate, err := stateUpdateByNumber(txn, blockNumber) + if err != nil { + return err + } + state := core.NewState(txn) + reverseStateDiff, err = state.GetReverseStateDiff(blockNumber, stateUpdate.StateDiff) + return err + }) +} + func (b *Blockchain) revertHead(txn db.Transaction) error { blockNumber, err := chainHeight(txn) if err != nil { @@ -874,7 +891,6 @@ func (b *Blockchain) revertHead(txn db.Transaction) error { } // Revert chain height and pending. - if genesisBlock { if err = txn.Delete(db.Pending.Key()); err != nil { return err diff --git a/cmd/juno/juno.go b/cmd/juno/juno.go index c61ee40529..6739a63357 100644 --- a/cmd/juno/juno.go +++ b/cmd/juno/juno.go @@ -82,6 +82,7 @@ const ( callMaxStepsF = "rpc-call-max-steps" corsEnableF = "rpc-cors-enable" versionedConstantsFileF = "versioned-constants-file" + pluginPathF = "plugin-path" defaultConfig = "" defaulHost = "localhost" @@ -119,6 +120,7 @@ const ( defaultGwTimeout = 5 * time.Second defaultCorsEnable = false defaultVersionedConstantsFile = "" + defaultPluginPath = "" configFlagUsage = "The YAML configuration file." logLevelFlagUsage = "Options: trace, debug, info, warn, error." @@ -170,6 +172,7 @@ const ( "The upper limit is 4 million steps, and any higher value will still be capped at 4 million." corsEnableUsage = "Enable CORS on RPC endpoints" versionedConstantsFileUsage = "Use custom versioned constants from provided file" + pluginPathUsage = "Path to the plugin .so file" ) var Version string @@ -355,6 +358,7 @@ func NewCmd(config *node.Config, run func(*cobra.Command, []string) error) *cobr junoCmd.Flags().Bool(corsEnableF, defaultCorsEnable, corsEnableUsage) junoCmd.Flags().String(versionedConstantsFileF, defaultVersionedConstantsFile, versionedConstantsFileUsage) junoCmd.MarkFlagsMutuallyExclusive(p2pFeederNodeF, p2pPeersF) + junoCmd.Flags().String(pluginPathF, defaultPluginPath, pluginPathUsage) junoCmd.AddCommand(GenP2PKeyPair(), DBCmd(defaultDBPath)) diff --git a/core/state.go b/core/state.go index 71198afbe6..effde8b518 100644 --- a/core/state.go +++ b/core/state.go @@ -540,10 +540,14 @@ func (s *State) Revert(blockNumber uint64, update *StateUpdate) error { return fmt.Errorf("remove declared classes: %v", err) } - // update contracts - reversedDiff, err := s.buildReverseDiff(blockNumber, update.StateDiff) + reversedDiff, err := s.GetReverseStateDiff(blockNumber, update.StateDiff) if err != nil { - return fmt.Errorf("build reverse diff: %v", err) + return fmt.Errorf("error getting reverse state diff: %v", err) + } + + err = s.performStateDeletions(blockNumber, update.StateDiff) + if err != nil { + return fmt.Errorf("error performing state deletions: %v", err) } stateTrie, storageCloser, err := s.storage() @@ -566,12 +570,17 @@ func (s *State) Revert(blockNumber uint64, update *StateUpdate) error { } } - // purge noClassContracts - // + if err = s.purgeNoClassContracts(); err != nil { + return err + } + + return s.verifyStateUpdateRoot(update.OldRoot) +} + +func (s *State) purgeNoClassContracts() error { // As noClassContracts are not in StateDiff.DeployedContracts we can only purge them if their storage no longer exists. // Updating contracts with reverse diff will eventually lead to the deletion of noClassContract's storage key from db. Thus, // we can use the lack of key's existence as reason for purging noClassContracts. - for addr := range noClassContracts { noClassC, err := NewContractUpdater(&addr, s.txn) if err != nil { @@ -592,8 +601,7 @@ func (s *State) Revert(blockNumber uint64, update *StateUpdate) error { } } } - - return s.verifyStateUpdateRoot(update.OldRoot) + return nil } func (s *State) removeDeclaredClasses(blockNumber uint64, v0Classes []*felt.Felt, v1Classes map[felt.Felt]*felt.Felt) error { @@ -657,7 +665,7 @@ func (s *State) purgeContract(addr *felt.Felt) error { return storageCloser() } -func (s *State) buildReverseDiff(blockNumber uint64, diff *StateDiff) (*StateDiff, error) { +func (s *State) GetReverseStateDiff(blockNumber uint64, diff *StateDiff) (*StateDiff, error) { reversed := *diff // storage diffs @@ -673,10 +681,6 @@ func (s *State) buildReverseDiff(blockNumber uint64, diff *StateDiff) (*StateDif } value = oldValue } - - if err := s.DeleteContractStorageLog(&addr, &key, blockNumber); err != nil { - return nil, err - } reversedDiffs[key] = value } reversed.StorageDiffs[addr] = reversedDiffs @@ -686,7 +690,6 @@ func (s *State) buildReverseDiff(blockNumber uint64, diff *StateDiff) (*StateDif reversed.Nonces = make(map[felt.Felt]*felt.Felt, len(diff.Nonces)) for addr := range diff.Nonces { oldNonce := &felt.Zero - if blockNumber > 0 { var err error oldNonce, err = s.ContractNonceAt(&addr, blockNumber-1) @@ -694,10 +697,6 @@ func (s *State) buildReverseDiff(blockNumber uint64, diff *StateDiff) (*StateDif return nil, err } } - - if err := s.DeleteContractNonceLog(&addr, blockNumber); err != nil { - return nil, err - } reversed.Nonces[addr] = oldNonce } @@ -712,12 +711,35 @@ func (s *State) buildReverseDiff(blockNumber uint64, diff *StateDiff) (*StateDif return nil, err } } + reversed.ReplacedClasses[addr] = classHash + } + return &reversed, nil +} + +func (s *State) performStateDeletions(blockNumber uint64, diff *StateDiff) error { + // storage diffs + for addr, storageDiffs := range diff.StorageDiffs { + for key := range storageDiffs { + if err := s.DeleteContractStorageLog(&addr, &key, blockNumber); err != nil { + return err + } + } + } + + // nonces + for addr := range diff.Nonces { + if err := s.DeleteContractNonceLog(&addr, blockNumber); err != nil { + return err + } + } + + // replaced classes + for addr := range diff.ReplacedClasses { if err := s.DeleteContractClassHashLog(&addr, blockNumber); err != nil { - return nil, err + return err } - reversed.ReplacedClasses[addr] = classHash } - return &reversed, nil + return nil } diff --git a/docs/docs/plugins.md b/docs/docs/plugins.md new file mode 100644 index 0000000000..816f028906 --- /dev/null +++ b/docs/docs/plugins.md @@ -0,0 +1,82 @@ +--- +title: Juno Plugins +--- + +Juno supports plugins that satisfy the `JunoPlugin` interface, enabling developers to extend and customize Juno's behaviour and functionality by dynamically loading external plugins during runtime. + +The `JunoPlugin` interface provides a structured way for plugins to interact with the blockchain by sending notifications when new blocks are added or reverted. This ensures state consistency, especially during blockchain reorganizations, while abstracting away the complexity of implementing block syncing and revert logic. + +## JunoPlugin Interface + +Your plugin must implement the `JunoPlugin` interface, which includes methods for initializing, shutting down, and handling new and reverted blocks. + +```go +type JunoPlugin interface { + Init() error + Shutdown() error + NewBlock(block *core.Block, stateUpdate *core.StateUpdate, newClasses map[felt.Felt]core.Class) error + RevertBlock(from, to *BlockAndStateUpdate, reverseStateDiff *core.StateDiff) error +} +``` + +**Init**: Called when the plugin is initialized. This can be used to set up database connections or any other necessary resources. + +**Shutdown**: Called when the Juno node is shut down. This can be used to clean up resources like database connections. + +**NewBlock**: Triggered when a new block is synced by the Juno client. Juno will send the block, the corresponding state update, and any new classes. Importantly, Juno waits for the plugin to finish processing this function call before continuing. This ensures that the plugin completes its task before Juno proceeds with the blockchain sync. + +**RevertBlock**: Called during a blockchain reorganization (reorg). Juno will invoke this method for each block that needs to be reverted. Similar to NewBlock, the client will wait for the plugin to finish handling the revert before moving on to the next block. + +## Example plugin + +Here is a basic example of a plugin that satisfies the `JunoPlugin` interface: + +```go +// go:generate go build -buildmode=plugin -o ../../build/plugin.so ./example.go +type examplePlugin string + +// Important: "JunoPluginInstance" needs to be exported for Juno to load the plugin correctly +var JunoPluginInstance examplePlugin + +var _ junoplugin.JunoPlugin = (*examplePlugin)(nil) + +func (p *examplePlugin) Init() error { + fmt.Println("ExamplePlugin initialized") + return nil +} + +func (p *examplePlugin) Shutdown() error { + fmt.Println("ExamplePlugin shutdown") + return nil +} + +func (p *examplePlugin) NewBlock(block *core.Block, stateUpdate *core.StateUpdate, newClasses map[felt.Felt]core.Class) error { + fmt.Println("ExamplePlugin NewBlock called") + return nil +} + +func (p *examplePlugin) RevertBlock(from, to *junoplugin.BlockAndStateUpdate, reverseStateDiff *core.StateDiff) error { + fmt.Println("ExamplePlugin RevertBlock called") + return nil +} +``` + +The `JunoPluginInstance` variable must be exported for Juno to correctly load the plugin: +`var JunoPluginInstance examplePlugin` + +We ensure the plugin implements the `JunoPlugin` interface, with the following line: +`var _ junoplugin.JunoPlugin = (*examplePlugin)(nil)` + +## Building and loading the plugin + +Once you have written your plugin, you can compile it into a shared object file (.so) using the following command: + +```shell +go build -buildmode=plugin -o ./plugin.so /path/to/your/plugin.go +``` + +This command compiles the plugin into a shared object file (`plugin.so`), which can then be loaded by the Juno client using the `--plugin-path` flag. + +## Running Juno with the plugin + +Once your plugin has been compiled into a `.so` file, you can run Juno with your plugin by providing the `--plugin-path` flag. This flag tells Juno where to find and load your plugin at runtime. diff --git a/docs/sidebars.js b/docs/sidebars.js index 10125a3020..b98fd7bacf 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -12,6 +12,7 @@ const sidebars = { "hardware-requirements", "running-juno", "configuring", + "plugins", "running-on-gcp", "updating", ], diff --git a/go.mod b/go.mod index df47107f31..56016a4cbf 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/ethereum/go-ethereum v1.14.11 github.com/fxamacker/cbor/v2 v2.7.0 github.com/go-playground/validator/v10 v10.22.1 + github.com/golang/protobuf v1.5.4 github.com/jinzhu/copier v0.4.0 github.com/libp2p/go-libp2p v0.36.2 github.com/libp2p/go-libp2p-kad-dht v0.27.0 diff --git a/go.sum b/go.sum index 32c31eb96b..0481e512ca 100644 --- a/go.sum +++ b/go.sum @@ -179,6 +179,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= diff --git a/mocks/mock_plugin.go b/mocks/mock_plugin.go new file mode 100644 index 0000000000..7c1d4a4391 --- /dev/null +++ b/mocks/mock_plugin.go @@ -0,0 +1,98 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/NethermindEth/juno/plugin (interfaces: JunoPlugin) +// +// Generated by this command: +// +// mockgen -destination=../mocks/mock_plugin.go -package=mocks github.com/NethermindEth/juno/plugin JunoPlugin +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + core "github.com/NethermindEth/juno/core" + felt "github.com/NethermindEth/juno/core/felt" + junoplugin "github.com/NethermindEth/juno/plugin" + gomock "go.uber.org/mock/gomock" +) + +// MockJunoPlugin is a mock of JunoPlugin interface. +type MockJunoPlugin struct { + ctrl *gomock.Controller + recorder *MockJunoPluginMockRecorder +} + +// MockJunoPluginMockRecorder is the mock recorder for MockJunoPlugin. +type MockJunoPluginMockRecorder struct { + mock *MockJunoPlugin +} + +// NewMockJunoPlugin creates a new mock instance. +func NewMockJunoPlugin(ctrl *gomock.Controller) *MockJunoPlugin { + mock := &MockJunoPlugin{ctrl: ctrl} + mock.recorder = &MockJunoPluginMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockJunoPlugin) EXPECT() *MockJunoPluginMockRecorder { + return m.recorder +} + +// Init mocks base method. +func (m *MockJunoPlugin) Init() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Init") + ret0, _ := ret[0].(error) + return ret0 +} + +// Init indicates an expected call of Init. +func (mr *MockJunoPluginMockRecorder) Init() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockJunoPlugin)(nil).Init)) +} + +// NewBlock mocks base method. +func (m *MockJunoPlugin) NewBlock(arg0 *core.Block, arg1 *core.StateUpdate, arg2 map[felt.Felt]core.Class) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewBlock", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// NewBlock indicates an expected call of NewBlock. +func (mr *MockJunoPluginMockRecorder) NewBlock(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBlock", reflect.TypeOf((*MockJunoPlugin)(nil).NewBlock), arg0, arg1, arg2) +} + +// RevertBlock mocks base method. +func (m *MockJunoPlugin) RevertBlock(arg0, arg1 *junoplugin.BlockAndStateUpdate, arg2 *core.StateDiff) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RevertBlock", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// RevertBlock indicates an expected call of RevertBlock. +func (mr *MockJunoPluginMockRecorder) RevertBlock(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertBlock", reflect.TypeOf((*MockJunoPlugin)(nil).RevertBlock), arg0, arg1, arg2) +} + +// Shutdown mocks base method. +func (m *MockJunoPlugin) Shutdown() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Shutdown") + ret0, _ := ret[0].(error) + return ret0 +} + +// Shutdown indicates an expected call of Shutdown. +func (mr *MockJunoPluginMockRecorder) Shutdown() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockJunoPlugin)(nil).Shutdown)) +} diff --git a/node/node.go b/node/node.go index 123a5b7085..96d94a182d 100644 --- a/node/node.go +++ b/node/node.go @@ -22,6 +22,7 @@ import ( "github.com/NethermindEth/juno/l1" "github.com/NethermindEth/juno/migration" "github.com/NethermindEth/juno/p2p" + junoplugin "github.com/NethermindEth/juno/plugin" "github.com/NethermindEth/juno/rpc" "github.com/NethermindEth/juno/service" adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" @@ -88,6 +89,8 @@ type Config struct { GatewayAPIKey string `mapstructure:"gw-api-key"` GatewayTimeout time.Duration `mapstructure:"gw-timeout"` + + PluginPath string `mapstructure:"plugin-path"` } type Node struct { @@ -156,6 +159,17 @@ func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen synchronizer := sync.New(chain, adaptfeeder.New(client), log, cfg.PendingPollInterval, dbIsRemote) gatewayClient := gateway.NewClient(cfg.Network.GatewayURL, log).WithUserAgent(ua).WithAPIKey(cfg.GatewayAPIKey) + pluginService := junoplugin.New(log) + if cfg.PluginPath != "" { + plugin, err := junoplugin.Load(cfg.PluginPath) + if err != nil { + return nil, err + } + synchronizer.WithPlugin(plugin) + pluginService.WithPlugin(plugin) + services = append(services, pluginService) + } + var p2pService *p2p.Service if cfg.P2P { if cfg.Network != utils.Sepolia { diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 0000000000..cb18f70343 --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,73 @@ +package junoplugin + +import ( + "context" + "fmt" + "plugin" + "sync" + + "github.com/NethermindEth/juno/core" + "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/juno/utils" +) + +type PluginService struct { + jPlugin JunoPlugin + wg sync.WaitGroup + log utils.SimpleLogger +} + +func New(log utils.SimpleLogger) *PluginService { + return &PluginService{wg: sync.WaitGroup{}, log: log} +} + +func (p *PluginService) WithPlugin(jPlugin JunoPlugin) { + p.jPlugin = jPlugin +} + +func (p *PluginService) Run(ctx context.Context) error { + p.wg.Add(1) + go func() { + defer p.wg.Done() + <-ctx.Done() + if err := p.jPlugin.Shutdown(); err != nil { + p.log.Errorw("Error while calling plugin Shutdown() function", "err", err) + } + }() + p.wg.Wait() + return nil +} + +//go:generate mockgen -destination=../mocks/mock_plugin.go -package=mocks github.com/NethermindEth/juno/plugin JunoPlugin +type JunoPlugin interface { + Init() error + Shutdown() error + NewBlock(block *core.Block, stateUpdate *core.StateUpdate, newClasses map[felt.Felt]core.Class) error + // The state is reverted by applying a write operation with the reverseStateDiff's StorageDiffs, Nonces, and ReplacedClasses, + // and a delete option with its DeclaredV0Classes, DeclaredV1Classes, and ReplacedClasses. + RevertBlock(from, to *BlockAndStateUpdate, reverseStateDiff *core.StateDiff) error +} + +type BlockAndStateUpdate struct { + Block *core.Block + StateUpdate *core.StateUpdate +} + +func Load(pluginPath string) (JunoPlugin, error) { + plug, err := plugin.Open(pluginPath) + if err != nil { + return nil, fmt.Errorf("error loading plugin .so file: %w", err) + } + + symPlugin, err := plug.Lookup("JunoPluginInstance") + if err != nil { + return nil, fmt.Errorf("error looking up PluginInstance: %w", err) + } + + pluginInstance, ok := symPlugin.(JunoPlugin) + if !ok { + return nil, fmt.Errorf("the plugin does not staisfy the required interface") + } + + return pluginInstance, pluginInstance.Init() +} diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go new file mode 100644 index 0000000000..54f9cffe17 --- /dev/null +++ b/plugin/plugin_test.go @@ -0,0 +1,79 @@ +package junoplugin_test + +import ( + "context" + "testing" + "time" + + "github.com/NethermindEth/juno/blockchain" + "github.com/NethermindEth/juno/clients/feeder" + "github.com/NethermindEth/juno/db/pebble" + "github.com/NethermindEth/juno/mocks" + junoplugin "github.com/NethermindEth/juno/plugin" + adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" + "github.com/NethermindEth/juno/sync" + "github.com/NethermindEth/juno/utils" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestPlugin(t *testing.T) { + timeout := time.Second + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + plugin := mocks.NewMockJunoPlugin(mockCtrl) + + mainClient := feeder.NewTestClient(t, &utils.Mainnet) + mainGw := adaptfeeder.New(mainClient) + + integClient := feeder.NewTestClient(t, &utils.Integration) + integGw := adaptfeeder.New(integClient) + + testDB := pebble.NewMemTest(t) + + // sync to integration for 2 blocks + for i := range 2 { + su, block, err := integGw.StateUpdateWithBlock(context.Background(), uint64(i)) + require.NoError(t, err) + plugin.EXPECT().NewBlock(block, su, gomock.Any()) + } + bc := blockchain.New(testDB, &utils.Integration) + synchronizer := sync.New(bc, integGw, utils.NewNopZapLogger(), 0, false).WithPlugin(plugin) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + require.NoError(t, synchronizer.Run(ctx)) + cancel() + + t.Run("resync to mainnet with the same db", func(t *testing.T) { + bc := blockchain.New(testDB, &utils.Mainnet) + + // Ensure current head is Integration head + head, err := bc.HeadsHeader() + require.NoError(t, err) + require.Equal(t, utils.HexToFelt(t, "0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86"), head.Hash) + + // Reorg 2 blocks, then sync 3 blocks + su1, block1, err := integGw.StateUpdateWithBlock(context.Background(), uint64(1)) + require.NoError(t, err) + su0, block0, err := integGw.StateUpdateWithBlock(context.Background(), uint64(0)) + require.NoError(t, err) + plugin.EXPECT().RevertBlock(&junoplugin.BlockAndStateUpdate{block1, su1}, &junoplugin.BlockAndStateUpdate{block0, su0}, gomock.Any()) + plugin.EXPECT().RevertBlock(&junoplugin.BlockAndStateUpdate{block0, su0}, nil, gomock.Any()) + for i := range 3 { + su, block, err := mainGw.StateUpdateWithBlock(context.Background(), uint64(i)) + require.NoError(t, err) + plugin.EXPECT().NewBlock(block, su, gomock.Any()) + } + + synchronizer = sync.New(bc, mainGw, utils.NewNopZapLogger(), 0, false).WithPlugin(plugin) + ctx, cancel = context.WithTimeout(context.Background(), timeout) + require.NoError(t, synchronizer.Run(ctx)) + cancel() + + // After syncing (and reorging) the current head should be at mainnet + head, err = bc.HeadsHeader() + require.NoError(t, err) + require.Equal(t, utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), head.Hash) + }) +} diff --git a/sync/sync.go b/sync/sync.go index a2e4ac0bf0..1270193d07 100644 --- a/sync/sync.go +++ b/sync/sync.go @@ -12,6 +12,7 @@ import ( "github.com/NethermindEth/juno/core/felt" "github.com/NethermindEth/juno/db" "github.com/NethermindEth/juno/feed" + junoplugin "github.com/NethermindEth/juno/plugin" "github.com/NethermindEth/juno/service" "github.com/NethermindEth/juno/starknetdata" "github.com/NethermindEth/juno/utils" @@ -72,6 +73,7 @@ type Synchronizer struct { pendingPollInterval time.Duration catchUpMode bool + plugin junoplugin.JunoPlugin } func New(bc *blockchain.Blockchain, starkNetData starknetdata.StarknetData, @@ -89,6 +91,12 @@ func New(bc *blockchain.Blockchain, starkNetData starknetdata.StarknetData, return s } +// WithPlugin registers an plugin +func (s *Synchronizer) WithPlugin(plugin junoplugin.JunoPlugin) *Synchronizer { + s.plugin = plugin + return s +} + // WithListener registers an EventListener func (s *Synchronizer) WithListener(listener EventListener) *Synchronizer { s.listener = listener @@ -180,6 +188,49 @@ func (s *Synchronizer) fetchUnknownClasses(ctx context.Context, stateUpdate *cor return newClasses, closer() } +func (s *Synchronizer) handlePluginRevertBlock() { + fromBlock, err := s.blockchain.Head() + if err != nil { + s.log.Warnw("Failed to retrieve the reverted blockchain head block for the plugin", "err", err) + return + } + fromSU, err := s.blockchain.StateUpdateByNumber(fromBlock.Number) + if err != nil { + s.log.Warnw("Failed to retrieve the reverted blockchain head state-update for the plugin", "err", err) + return + } + reverseStateDiff, err := s.blockchain.GetReverseStateDiff() + if err != nil { + s.log.Warnw("Failed to retrieve reverse state diff", "head", fromBlock.Number, "hash", fromBlock.Hash.ShortString(), "err", err) + return + } + + var toBlockAndStateUpdate *junoplugin.BlockAndStateUpdate + if fromBlock.Number != 0 { + toBlock, err := s.blockchain.BlockByHash(fromBlock.ParentHash) + if err != nil { + s.log.Warnw("Failed to retrieve the parent block for the plugin", "err", err) + return + } + toSU, err := s.blockchain.StateUpdateByNumber(toBlock.Number) + if err != nil { + s.log.Warnw("Failed to retrieve the parents state-update for the plugin", "err", err) + return + } + toBlockAndStateUpdate = &junoplugin.BlockAndStateUpdate{ + Block: toBlock, + StateUpdate: toSU, + } + } + err = s.plugin.RevertBlock( + &junoplugin.BlockAndStateUpdate{Block: fromBlock, StateUpdate: fromSU}, + toBlockAndStateUpdate, + reverseStateDiff) + if err != nil { + s.log.Errorw("Plugin RevertBlock failure:", "err", err) + } +} + func (s *Synchronizer) verifierTask(ctx context.Context, block *core.Block, stateUpdate *core.StateUpdate, newClasses map[felt.Felt]core.Class, resetStreams context.CancelFunc, ) stream.Callback { @@ -205,6 +256,9 @@ func (s *Synchronizer) verifierTask(ctx context.Context, block *core.Block, stat // revert the head and restart the sync process, hoping that the reorg is not deep // if the reorg is deeper, we will end up here again and again until we fully revert reorged // blocks + if s.plugin != nil { + s.handlePluginRevertBlock() + } s.revertHead(block) } else { s.log.Warnw("Failed storing Block", "number", block.Number, @@ -231,6 +285,12 @@ func (s *Synchronizer) verifierTask(ctx context.Context, block *core.Block, stat s.newHeads.Send(block.Header) s.log.Infow("Stored Block", "number", block.Number, "hash", block.Hash.ShortString(), "root", block.GlobalStateRoot.ShortString()) + if s.plugin != nil { + err := s.plugin.NewBlock(block, stateUpdate, newClasses) + if err != nil { + s.log.Errorw("Plugin NewBlock failure:", err) + } + } } } } @@ -315,11 +375,8 @@ func (s *Synchronizer) revertHead(forkBlock *core.Block) { if err == nil { localHead = head.Hash } - s.log.Infow("Reorg detected", "localHead", localHead, "forkHead", forkBlock.Hash) - - err = s.blockchain.RevertHead() - if err != nil { + if err := s.blockchain.RevertHead(); err != nil { s.log.Warnw("Failed reverting HEAD", "reverted", localHead, "err", err) } else { s.log.Infow("Reverted HEAD", "reverted", localHead) diff --git a/vm/rust/src/juno_state_reader.rs b/vm/rust/src/juno_state_reader.rs index df540005d7..ee4ad9734d 100644 --- a/vm/rust/src/juno_state_reader.rs +++ b/vm/rust/src/juno_state_reader.rs @@ -1,5 +1,5 @@ use std::{ - ffi::{c_char, c_uchar, c_void, c_int, CStr}, + ffi::{c_char, c_int, c_uchar, c_void, CStr}, slice, sync::Mutex, }; @@ -75,8 +75,14 @@ impl StateReader for JunoStateReader { let addr = felt_to_byte_array(contract_address.0.key()); let storage_key = felt_to_byte_array(key.0.key()); let mut buffer: [u8; 32] = [0; 32]; - let wrote = - unsafe { JunoStateGetStorageAt(self.handle, addr.as_ptr(), storage_key.as_ptr(), buffer.as_mut_ptr()) }; + let wrote = unsafe { + JunoStateGetStorageAt( + self.handle, + addr.as_ptr(), + storage_key.as_ptr(), + buffer.as_mut_ptr(), + ) + }; if wrote == 0 { Err(StateError::StateReadError(format!( "failed to read location {} at address {}", @@ -111,7 +117,8 @@ impl StateReader for JunoStateReader { fn get_class_hash_at(&self, contract_address: ContractAddress) -> StateResult { let addr = felt_to_byte_array(contract_address.0.key()); let mut buffer: [u8; 32] = [0; 32]; - let wrote = unsafe { JunoStateGetClassHashAt(self.handle, addr.as_ptr(), buffer.as_mut_ptr()) }; + let wrote = + unsafe { JunoStateGetClassHashAt(self.handle, addr.as_ptr(), buffer.as_mut_ptr()) }; if wrote == 0 { Err(StateError::StateReadError(format!( "failed to read class hash of address {}", From f5dc02caac7c59d36015845cf9d5b3355d312da4 Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 17 Oct 2024 11:15:15 +0400 Subject: [PATCH 7/8] Small restructure of plugin logic (#2221) --- node/node.go | 10 ++++------ plugin/plugin.go | 36 +++------------------------------ plugin/plugin_test.go | 2 +- plugin/service.go | 19 ++++++++++++++++++ plugin/service_test.go | 45 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 40 deletions(-) create mode 100644 plugin/service.go create mode 100644 plugin/service_test.go diff --git a/node/node.go b/node/node.go index 96d94a182d..ccb316c84d 100644 --- a/node/node.go +++ b/node/node.go @@ -22,7 +22,7 @@ import ( "github.com/NethermindEth/juno/l1" "github.com/NethermindEth/juno/migration" "github.com/NethermindEth/juno/p2p" - junoplugin "github.com/NethermindEth/juno/plugin" + "github.com/NethermindEth/juno/plugin" "github.com/NethermindEth/juno/rpc" "github.com/NethermindEth/juno/service" adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" @@ -159,15 +159,13 @@ func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen synchronizer := sync.New(chain, adaptfeeder.New(client), log, cfg.PendingPollInterval, dbIsRemote) gatewayClient := gateway.NewClient(cfg.Network.GatewayURL, log).WithUserAgent(ua).WithAPIKey(cfg.GatewayAPIKey) - pluginService := junoplugin.New(log) if cfg.PluginPath != "" { - plugin, err := junoplugin.Load(cfg.PluginPath) + p, err := plugin.Load(cfg.PluginPath) if err != nil { return nil, err } - synchronizer.WithPlugin(plugin) - pluginService.WithPlugin(plugin) - services = append(services, pluginService) + synchronizer.WithPlugin(p) + services = append(services, plugin.NewService(p)) } var p2pService *p2p.Service diff --git a/plugin/plugin.go b/plugin/plugin.go index cb18f70343..8dd9ee13d2 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -1,43 +1,13 @@ -package junoplugin +package plugin import ( - "context" "fmt" - "plugin" - "sync" + stdplugin "plugin" "github.com/NethermindEth/juno/core" "github.com/NethermindEth/juno/core/felt" - "github.com/NethermindEth/juno/utils" ) -type PluginService struct { - jPlugin JunoPlugin - wg sync.WaitGroup - log utils.SimpleLogger -} - -func New(log utils.SimpleLogger) *PluginService { - return &PluginService{wg: sync.WaitGroup{}, log: log} -} - -func (p *PluginService) WithPlugin(jPlugin JunoPlugin) { - p.jPlugin = jPlugin -} - -func (p *PluginService) Run(ctx context.Context) error { - p.wg.Add(1) - go func() { - defer p.wg.Done() - <-ctx.Done() - if err := p.jPlugin.Shutdown(); err != nil { - p.log.Errorw("Error while calling plugin Shutdown() function", "err", err) - } - }() - p.wg.Wait() - return nil -} - //go:generate mockgen -destination=../mocks/mock_plugin.go -package=mocks github.com/NethermindEth/juno/plugin JunoPlugin type JunoPlugin interface { Init() error @@ -54,7 +24,7 @@ type BlockAndStateUpdate struct { } func Load(pluginPath string) (JunoPlugin, error) { - plug, err := plugin.Open(pluginPath) + plug, err := stdplugin.Open(pluginPath) if err != nil { return nil, fmt.Errorf("error loading plugin .so file: %w", err) } diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go index 54f9cffe17..3933835ae7 100644 --- a/plugin/plugin_test.go +++ b/plugin/plugin_test.go @@ -1,4 +1,4 @@ -package junoplugin_test +package plugin_test import ( "context" diff --git a/plugin/service.go b/plugin/service.go new file mode 100644 index 0000000000..907feefa9e --- /dev/null +++ b/plugin/service.go @@ -0,0 +1,19 @@ +package plugin + +import "context" + +// Service provides an abstraction for signalling the plugin to shut down. +type Service struct { + plugin JunoPlugin +} + +func NewService(plugin JunoPlugin) *Service { + return &Service{ + plugin: plugin, + } +} + +func (p *Service) Run(ctx context.Context) error { + <-ctx.Done() + return p.plugin.Shutdown() +} diff --git a/plugin/service_test.go b/plugin/service_test.go new file mode 100644 index 0000000000..786cfc6a61 --- /dev/null +++ b/plugin/service_test.go @@ -0,0 +1,45 @@ +package plugin_test + +import ( + "context" + "errors" + "testing" + + "github.com/NethermindEth/juno/mocks" + "github.com/NethermindEth/juno/plugin" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestService(t *testing.T) { + t.Run("shutdown ok", func(t *testing.T) { + ctrl := gomock.NewController(t) + + p := mocks.NewMockJunoPlugin(ctrl) + p.EXPECT().Shutdown().Return(nil) + service := plugin.NewService(p) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + // after ^ this ctx already cancelled + + err := service.Run(ctx) + require.NoError(t, err) + }) + t.Run("shutdown with error", func(t *testing.T) { + ctrl := gomock.NewController(t) + + shutdownErr := errors.New("error during shutdown") + + p := mocks.NewMockJunoPlugin(ctrl) + p.EXPECT().Shutdown().Return(shutdownErr) + service := plugin.NewService(p) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + // after ^ this ctx already cancelled + + err := service.Run(ctx) + require.Equal(t, shutdownErr, err) + }) +} From f4d9d698792fd8ec711b9bf2cfc6405a098a7874 Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 17 Oct 2024 15:56:02 +0400 Subject: [PATCH 8/8] Add missing gomock.Controller.Finish() calls in plugin tests (#2225) --- plugin/service_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin/service_test.go b/plugin/service_test.go index 786cfc6a61..84e1e6ae9f 100644 --- a/plugin/service_test.go +++ b/plugin/service_test.go @@ -14,6 +14,7 @@ import ( func TestService(t *testing.T) { t.Run("shutdown ok", func(t *testing.T) { ctrl := gomock.NewController(t) + defer ctrl.Finish() p := mocks.NewMockJunoPlugin(ctrl) p.EXPECT().Shutdown().Return(nil) @@ -28,6 +29,7 @@ func TestService(t *testing.T) { }) t.Run("shutdown with error", func(t *testing.T) { ctrl := gomock.NewController(t) + defer ctrl.Finish() shutdownErr := errors.New("error during shutdown")