github.com/ethersphere/bee/v2@v2.2.0/pkg/node/devnode.go (about)

     1  // Copyright 2021 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package node
     6  
     7  import (
     8  	"context"
     9  	"crypto/rand"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	stdlog "log"
    14  	"math/big"
    15  	"net"
    16  	"net/http"
    17  	"time"
    18  
    19  	"github.com/ethereum/go-ethereum/common"
    20  	"github.com/ethersphere/bee/v2/pkg/accesscontrol"
    21  	mockAccounting "github.com/ethersphere/bee/v2/pkg/accounting/mock"
    22  	"github.com/ethersphere/bee/v2/pkg/api"
    23  	"github.com/ethersphere/bee/v2/pkg/bzz"
    24  	"github.com/ethersphere/bee/v2/pkg/crypto"
    25  	"github.com/ethersphere/bee/v2/pkg/feeds/factory"
    26  	"github.com/ethersphere/bee/v2/pkg/log"
    27  	mockP2P "github.com/ethersphere/bee/v2/pkg/p2p/mock"
    28  	mockPingPong "github.com/ethersphere/bee/v2/pkg/pingpong/mock"
    29  	"github.com/ethersphere/bee/v2/pkg/postage"
    30  	"github.com/ethersphere/bee/v2/pkg/postage/batchstore"
    31  	mockPost "github.com/ethersphere/bee/v2/pkg/postage/mock"
    32  	"github.com/ethersphere/bee/v2/pkg/postage/postagecontract"
    33  	mockPostContract "github.com/ethersphere/bee/v2/pkg/postage/postagecontract/mock"
    34  	postagetesting "github.com/ethersphere/bee/v2/pkg/postage/testing"
    35  	"github.com/ethersphere/bee/v2/pkg/pss"
    36  	"github.com/ethersphere/bee/v2/pkg/pushsync"
    37  	mockPushsync "github.com/ethersphere/bee/v2/pkg/pushsync/mock"
    38  	resolverMock "github.com/ethersphere/bee/v2/pkg/resolver/mock"
    39  	"github.com/ethersphere/bee/v2/pkg/settlement/pseudosettle"
    40  	"github.com/ethersphere/bee/v2/pkg/settlement/swap/chequebook"
    41  	mockchequebook "github.com/ethersphere/bee/v2/pkg/settlement/swap/chequebook/mock"
    42  	erc20mock "github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20/mock"
    43  	swapmock "github.com/ethersphere/bee/v2/pkg/settlement/swap/mock"
    44  	"github.com/ethersphere/bee/v2/pkg/statestore/leveldb"
    45  	mockSteward "github.com/ethersphere/bee/v2/pkg/steward/mock"
    46  	"github.com/ethersphere/bee/v2/pkg/storage/inmemstore"
    47  	"github.com/ethersphere/bee/v2/pkg/storageincentives/staking"
    48  	stakingContractMock "github.com/ethersphere/bee/v2/pkg/storageincentives/staking/mock"
    49  	"github.com/ethersphere/bee/v2/pkg/storer"
    50  	"github.com/ethersphere/bee/v2/pkg/swarm"
    51  	"github.com/ethersphere/bee/v2/pkg/topology/lightnode"
    52  	mockTopology "github.com/ethersphere/bee/v2/pkg/topology/mock"
    53  	"github.com/ethersphere/bee/v2/pkg/tracing"
    54  	"github.com/ethersphere/bee/v2/pkg/transaction"
    55  	"github.com/ethersphere/bee/v2/pkg/transaction/backendmock"
    56  	transactionmock "github.com/ethersphere/bee/v2/pkg/transaction/mock"
    57  	"github.com/ethersphere/bee/v2/pkg/util/ioutil"
    58  	"github.com/hashicorp/go-multierror"
    59  	"github.com/multiformats/go-multiaddr"
    60  	"golang.org/x/sync/errgroup"
    61  )
    62  
    63  type DevBee struct {
    64  	tracerCloser        io.Closer
    65  	stateStoreCloser    io.Closer
    66  	localstoreCloser    io.Closer
    67  	apiCloser           io.Closer
    68  	pssCloser           io.Closer
    69  	accesscontrolCloser io.Closer
    70  	errorLogWriter      io.Writer
    71  	apiServer           *http.Server
    72  }
    73  
    74  type DevOptions struct {
    75  	Logger                   log.Logger
    76  	APIAddr                  string
    77  	CORSAllowedOrigins       []string
    78  	DBOpenFilesLimit         uint64
    79  	ReserveCapacity          uint64
    80  	DBWriteBufferSize        uint64
    81  	DBBlockCacheCapacity     uint64
    82  	DBDisableSeeksCompaction bool
    83  }
    84  
    85  // NewDevBee starts the bee instance in 'development' mode
    86  // this implies starting an API and a Debug endpoints while mocking all their services.
    87  func NewDevBee(logger log.Logger, o *DevOptions) (b *DevBee, err error) {
    88  	tracer, tracerCloser, err := tracing.NewTracer(&tracing.Options{
    89  		Enabled: false,
    90  	})
    91  	if err != nil {
    92  		return nil, fmt.Errorf("tracer: %w", err)
    93  	}
    94  
    95  	sink := ioutil.WriterFunc(func(p []byte) (int, error) {
    96  		logger.Error(nil, string(p))
    97  		return len(p), nil
    98  	})
    99  
   100  	b = &DevBee{
   101  		errorLogWriter: sink,
   102  		tracerCloser:   tracerCloser,
   103  	}
   104  
   105  	stateStore, err := leveldb.NewInMemoryStateStore(logger)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	b.stateStoreCloser = stateStore
   110  
   111  	swarmAddress, err := randomAddress()
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	batchStore, err := batchstore.New(stateStore, func(b []byte) error { return nil }, 1000000, logger)
   117  	if err != nil {
   118  		return nil, fmt.Errorf("batchstore: %w", err)
   119  	}
   120  
   121  	err = batchStore.PutChainState(&postage.ChainState{
   122  		CurrentPrice: big.NewInt(1),
   123  		TotalAmount:  big.NewInt(1),
   124  	})
   125  	if err != nil {
   126  		return nil, fmt.Errorf("batchstore: %w", err)
   127  	}
   128  
   129  	mockKey, err := crypto.GenerateSecp256k1Key()
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	signer := crypto.NewDefaultSigner(mockKey)
   134  
   135  	overlayEthAddress, err := signer.EthereumAddress()
   136  	if err != nil {
   137  		return nil, fmt.Errorf("blockchain address: %w", err)
   138  	}
   139  
   140  	var mockTransaction = transactionmock.New(transactionmock.WithPendingTransactionsFunc(func() ([]common.Hash, error) {
   141  		return []common.Hash{common.HexToHash("abcd")}, nil
   142  	}), transactionmock.WithResendTransactionFunc(func(ctx context.Context, txHash common.Hash) error {
   143  		return nil
   144  	}), transactionmock.WithStoredTransactionFunc(func(txHash common.Hash) (*transaction.StoredTransaction, error) {
   145  		recipient := common.HexToAddress("dfff")
   146  		return &transaction.StoredTransaction{
   147  			To:          &recipient,
   148  			Created:     1,
   149  			Data:        []byte{1, 2, 3, 4},
   150  			GasPrice:    big.NewInt(12),
   151  			GasTipBoost: 10,
   152  			GasFeeCap:   big.NewInt(12),
   153  			GasTipCap:   new(big.Int).Div(new(big.Int).Mul(big.NewInt(int64(10)+100), big.NewInt(12)), big.NewInt(100)),
   154  			GasLimit:    5345,
   155  			Value:       big.NewInt(4),
   156  			Nonce:       3,
   157  			Description: "test",
   158  		}, nil
   159  	}), transactionmock.WithCancelTransactionFunc(func(ctx context.Context, originalTxHash common.Hash) (common.Hash, error) {
   160  		return common.Hash{}, nil
   161  	}),
   162  	)
   163  
   164  	chainBackend := backendmock.New(
   165  		backendmock.WithBlockNumberFunc(func(ctx context.Context) (uint64, error) {
   166  			return 1, nil
   167  		}),
   168  		backendmock.WithBalanceAt(func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) {
   169  			return big.NewInt(0), nil
   170  		}),
   171  	)
   172  
   173  	// Create api.Probe in healthy state and switch to ready state after all components have been constructed
   174  	probe := api.NewProbe()
   175  	probe.SetHealthy(api.ProbeStatusOK)
   176  	defer func(probe *api.Probe) {
   177  		if err != nil {
   178  			probe.SetHealthy(api.ProbeStatusNOK)
   179  		} else {
   180  			probe.SetReady(api.ProbeStatusOK)
   181  		}
   182  	}(probe)
   183  
   184  	localStore, err := storer.New(context.Background(), "", &storer.Options{
   185  		Logger:        logger,
   186  		CacheCapacity: 1_000_000,
   187  	})
   188  	if err != nil {
   189  		return nil, fmt.Errorf("localstore: %w", err)
   190  	}
   191  	b.localstoreCloser = localStore
   192  
   193  	session := accesscontrol.NewDefaultSession(mockKey)
   194  	actLogic := accesscontrol.NewLogic(session)
   195  	accesscontrol := accesscontrol.NewController(actLogic)
   196  	b.accesscontrolCloser = accesscontrol
   197  
   198  	pssService := pss.New(mockKey, logger)
   199  	b.pssCloser = pssService
   200  
   201  	pssService.SetPushSyncer(mockPushsync.New(func(ctx context.Context, chunk swarm.Chunk) (*pushsync.Receipt, error) {
   202  		pssService.TryUnwrap(chunk)
   203  		return &pushsync.Receipt{}, nil
   204  	}))
   205  
   206  	post := mockPost.New()
   207  	postageContract := mockPostContract.New(
   208  		mockPostContract.WithCreateBatchFunc(
   209  			func(ctx context.Context, amount *big.Int, depth uint8, immutable bool, label string) (common.Hash, []byte, error) {
   210  				id := postagetesting.MustNewID()
   211  				batch := &postage.Batch{
   212  					ID:        id,
   213  					Owner:     overlayEthAddress.Bytes(),
   214  					Value:     big.NewInt(0).Mul(amount, big.NewInt(int64(1<<depth))),
   215  					Depth:     depth,
   216  					Immutable: immutable,
   217  				}
   218  
   219  				err := batchStore.Save(batch)
   220  				if err != nil {
   221  					return common.Hash{}, nil, err
   222  				}
   223  
   224  				stampIssuer := postage.NewStampIssuer(label, string(overlayEthAddress.Bytes()), id, amount, batch.Depth, 0, 0, immutable)
   225  				_ = post.Add(stampIssuer)
   226  
   227  				return common.Hash{}, id, nil
   228  			},
   229  		),
   230  		mockPostContract.WithTopUpBatchFunc(
   231  			func(ctx context.Context, batchID []byte, topupAmount *big.Int) (common.Hash, error) {
   232  				return common.Hash{}, postagecontract.ErrNotImplemented
   233  			},
   234  		),
   235  		mockPostContract.WithDiluteBatchFunc(
   236  			func(ctx context.Context, batchID []byte, newDepth uint8) (common.Hash, error) {
   237  				return common.Hash{}, postagecontract.ErrNotImplemented
   238  			},
   239  		),
   240  	)
   241  
   242  	var (
   243  		lightNodes = lightnode.NewContainer(swarm.NewAddress(nil))
   244  		pingPong   = mockPingPong.New(pong)
   245  		p2ps       = mockP2P.New(
   246  			mockP2P.WithConnectFunc(func(ctx context.Context, addr multiaddr.Multiaddr) (address *bzz.Address, err error) {
   247  				return &bzz.Address{}, nil
   248  			}), mockP2P.WithDisconnectFunc(
   249  				func(swarm.Address, string) error {
   250  					return nil
   251  				},
   252  			), mockP2P.WithAddressesFunc(
   253  				func() ([]multiaddr.Multiaddr, error) {
   254  					ma, _ := multiaddr.NewMultiaddr("mock")
   255  					return []multiaddr.Multiaddr{ma}, nil
   256  				},
   257  			))
   258  		acc       = mockAccounting.NewAccounting()
   259  		kad       = mockTopology.NewTopologyDriver()
   260  		pseudoset = pseudosettle.New(nil, logger, stateStore, nil, big.NewInt(10000), big.NewInt(10000), p2ps)
   261  		mockSwap  = swapmock.New(swapmock.WithCashoutStatusFunc(
   262  			func(ctx context.Context, peer swarm.Address) (*chequebook.CashoutStatus, error) {
   263  				return &chequebook.CashoutStatus{
   264  					Last:           &chequebook.LastCashout{},
   265  					UncashedAmount: big.NewInt(0),
   266  				}, nil
   267  			},
   268  		), swapmock.WithLastSentChequeFunc(
   269  			func(a swarm.Address) (*chequebook.SignedCheque, error) {
   270  				return &chequebook.SignedCheque{
   271  					Cheque: chequebook.Cheque{
   272  						Beneficiary: common.Address{},
   273  						Chequebook:  common.Address{},
   274  					},
   275  				}, nil
   276  			},
   277  		), swapmock.WithLastReceivedChequeFunc(
   278  			func(a swarm.Address) (*chequebook.SignedCheque, error) {
   279  				return &chequebook.SignedCheque{
   280  					Cheque: chequebook.Cheque{
   281  						Beneficiary: common.Address{},
   282  						Chequebook:  common.Address{},
   283  					},
   284  				}, nil
   285  			},
   286  		))
   287  		mockChequebook = mockchequebook.NewChequebook(mockchequebook.WithChequebookBalanceFunc(
   288  			func(context.Context) (ret *big.Int, err error) {
   289  				return big.NewInt(0), nil
   290  			},
   291  		), mockchequebook.WithChequebookAvailableBalanceFunc(
   292  			func(context.Context) (ret *big.Int, err error) {
   293  				return big.NewInt(0), nil
   294  			},
   295  		), mockchequebook.WithChequebookWithdrawFunc(
   296  			func(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
   297  				return common.Hash{}, nil
   298  			},
   299  		), mockchequebook.WithChequebookDepositFunc(
   300  			func(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
   301  				return common.Hash{}, nil
   302  			},
   303  		))
   304  	)
   305  
   306  	var (
   307  		// syncStatusFn mocks sync status because complete sync is required in order to curl certain apis e.g. /stamps.
   308  		// this allows accessing those apis by passing true to isDone in devNode.
   309  		syncStatusFn = func() (isDone bool, err error) {
   310  			return true, nil
   311  		}
   312  	)
   313  
   314  	mockFeeds := factory.New(localStore.Download(true))
   315  	mockResolver := resolverMock.NewResolver()
   316  	mockSteward := new(mockSteward.Steward)
   317  
   318  	mockStaking := stakingContractMock.New(
   319  		stakingContractMock.WithDepositStake(func(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) {
   320  			return common.Hash{}, staking.ErrNotImplemented
   321  		}),
   322  		stakingContractMock.WithGetStake(func(ctx context.Context) (*big.Int, error) {
   323  			return nil, staking.ErrNotImplemented
   324  		}),
   325  		stakingContractMock.WithWithdrawStake(func(ctx context.Context) (common.Hash, error) {
   326  			return common.Hash{}, staking.ErrNotImplemented
   327  		}),
   328  		stakingContractMock.WithIsFrozen(func(ctx context.Context, block uint64) (bool, error) {
   329  			return false, staking.ErrNotImplemented
   330  		}),
   331  	)
   332  
   333  	debugOpts := api.ExtraOptions{
   334  		Pingpong:        pingPong,
   335  		TopologyDriver:  kad,
   336  		LightNodes:      lightNodes,
   337  		Accounting:      acc,
   338  		Pseudosettle:    pseudoset,
   339  		Swap:            mockSwap,
   340  		Chequebook:      mockChequebook,
   341  		BlockTime:       time.Second * 2,
   342  		Storer:          localStore,
   343  		Resolver:        mockResolver,
   344  		Pss:             pssService,
   345  		FeedFactory:     mockFeeds,
   346  		Post:            post,
   347  		AccessControl:   accesscontrol,
   348  		PostageContract: postageContract,
   349  		Staking:         mockStaking,
   350  		Steward:         mockSteward,
   351  		SyncStatus:      syncStatusFn,
   352  	}
   353  
   354  	var erc20 = erc20mock.New(
   355  		erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) {
   356  			return big.NewInt(0), nil
   357  		}),
   358  		erc20mock.WithTransferFunc(func(ctx context.Context, address common.Address, value *big.Int) (common.Hash, error) {
   359  			return common.Hash{}, nil
   360  		}),
   361  	)
   362  
   363  	apiService := api.New(mockKey.PublicKey, mockKey.PublicKey, overlayEthAddress, nil, logger, mockTransaction, batchStore, api.DevMode, true, true, chainBackend, o.CORSAllowedOrigins, inmemstore.New())
   364  
   365  	apiService.Configure(signer, tracer, api.Options{
   366  		CORSAllowedOrigins: o.CORSAllowedOrigins,
   367  		WsPingPeriod:       60 * time.Second,
   368  	}, debugOpts, 1, erc20)
   369  	apiService.MountTechnicalDebug()
   370  	apiService.MountDebug()
   371  	apiService.MountAPI()
   372  
   373  	apiService.SetProbe(probe)
   374  	apiService.SetP2P(p2ps)
   375  	apiService.SetSwarmAddress(&swarmAddress)
   376  
   377  	apiListener, err := net.Listen("tcp", o.APIAddr)
   378  	if err != nil {
   379  		return nil, fmt.Errorf("api listener: %w", err)
   380  	}
   381  
   382  	apiServer := &http.Server{
   383  		IdleTimeout:       30 * time.Second,
   384  		ReadHeaderTimeout: 3 * time.Second,
   385  		Handler:           apiService,
   386  		ErrorLog:          stdlog.New(b.errorLogWriter, "", 0),
   387  	}
   388  	go func() {
   389  		logger.Info("starting api server", "address", apiListener.Addr())
   390  
   391  		if err := apiServer.Serve(apiListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
   392  			logger.Debug("api server failed to start", "error", err)
   393  			logger.Error(nil, "api server failed to start")
   394  		}
   395  	}()
   396  
   397  	b.apiServer = apiServer
   398  	b.apiCloser = apiService
   399  
   400  	return b, nil
   401  }
   402  
   403  func (b *DevBee) Shutdown() error {
   404  	var mErr error
   405  
   406  	tryClose := func(c io.Closer, errMsg string) {
   407  		if c == nil {
   408  			return
   409  		}
   410  		if err := c.Close(); err != nil {
   411  			mErr = multierror.Append(mErr, fmt.Errorf("%s: %w", errMsg, err))
   412  		}
   413  	}
   414  
   415  	tryClose(b.apiCloser, "api")
   416  
   417  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   418  	defer cancel()
   419  
   420  	var eg errgroup.Group
   421  	if b.apiServer != nil {
   422  		eg.Go(func() error {
   423  			if err := b.apiServer.Shutdown(ctx); err != nil {
   424  				return fmt.Errorf("api server: %w", err)
   425  			}
   426  			return nil
   427  		})
   428  	}
   429  	if err := eg.Wait(); err != nil {
   430  		mErr = multierror.Append(mErr, err)
   431  	}
   432  
   433  	tryClose(b.pssCloser, "pss")
   434  	tryClose(b.accesscontrolCloser, "accesscontrol")
   435  	tryClose(b.tracerCloser, "tracer")
   436  	tryClose(b.stateStoreCloser, "statestore")
   437  	tryClose(b.localstoreCloser, ioutil.DataPathLocalstore)
   438  
   439  	return mErr
   440  }
   441  
   442  func pong(_ context.Context, _ swarm.Address, _ ...string) (rtt time.Duration, err error) {
   443  	return time.Millisecond, nil
   444  }
   445  
   446  func randomAddress() (swarm.Address, error) {
   447  
   448  	b := make([]byte, 32)
   449  
   450  	_, err := rand.Read(b)
   451  	if err != nil {
   452  		return swarm.ZeroAddress, err
   453  	}
   454  
   455  	return swarm.NewAddress(b), nil
   456  
   457  }