diff --git a/Makefile b/Makefile index ecba2ab90f..7a68ec3885 100644 --- a/Makefile +++ b/Makefile @@ -168,6 +168,20 @@ sequencer-with-accounts: --seq-genesis-file "./genesis/genesis_prefund_accounts.json" \ --rpc-call-max-steps=4123000 + +sequencer-shadow-sepolia: + ./build/juno \ + --http \ + --http-port=6060 \ + --http-host=0.0.0.0 \ + --db-path=./seq-db \ + --log-level=debug \ + --seq-enable \ + --seq-shadow-mode \ + --seq-block-time=5 \ + --network sepolia \ + --rpc-call-max-steps=4123000 + pathfinder: juno-cached ./build/juno \ --network=sepolia \ diff --git a/builder/builder.go b/builder/builder.go index 565a8d2d6a..e2ff9ee431 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -3,6 +3,7 @@ package builder import ( "context" "errors" + "fmt" stdsync "sync" "time" @@ -15,6 +16,7 @@ import ( "github.com/NethermindEth/juno/feed" "github.com/NethermindEth/juno/mempool" "github.com/NethermindEth/juno/service" + "github.com/NethermindEth/juno/starknetdata" "github.com/NethermindEth/juno/sync" "github.com/NethermindEth/juno/utils" "github.com/NethermindEth/juno/vm" @@ -42,17 +44,27 @@ type Builder struct { pendingBlock blockchain.Pending headState core.StateReader headCloser blockchain.StateCloser + + shadowMode bool + starknetData starknetdata.StarknetData + chanNumTxnsToShadow chan int + chanFinaliseShadow chan struct{} + + chanFinalise chan struct{} + chanFinalised chan struct{} } func New(privKey *ecdsa.PrivateKey, ownAddr *felt.Felt, bc *blockchain.Blockchain, builderVM vm.VM, blockTime time.Duration, pool *mempool.Pool, log utils.Logger, ) *Builder { return &Builder{ - ownAddress: *ownAddr, - privKey: privKey, - blockTime: blockTime, - log: log, - listener: &SelectiveListener{}, + ownAddress: *ownAddr, + privKey: privKey, + blockTime: blockTime, + log: log, + listener: &SelectiveListener{}, + chanFinalise: make(chan struct{}), + chanFinalised: make(chan struct{}, 1), bc: bc, pool: pool, @@ -61,12 +73,42 @@ func New(privKey *ecdsa.PrivateKey, ownAddr *felt.Felt, bc *blockchain.Blockchai } } +func NewShadow(privKey *ecdsa.PrivateKey, ownAddr *felt.Felt, bc *blockchain.Blockchain, builderVM vm.VM, + blockTime time.Duration, pool *mempool.Pool, log utils.Logger, starknetData starknetdata.StarknetData, +) *Builder { + return &Builder{ + ownAddress: *ownAddr, + privKey: privKey, + blockTime: blockTime, + log: log, + listener: &SelectiveListener{}, + chanFinalise: make(chan struct{}, 1), + chanFinalised: make(chan struct{}, 1), + + bc: bc, + pool: pool, + vm: builderVM, + newHeads: feed.New[*core.Header](), + + shadowMode: true, + starknetData: starknetData, + chanNumTxnsToShadow: make(chan int, 1), + chanFinaliseShadow: make(chan struct{}, 1), + } +} + func (b *Builder) WithEventListener(l EventListener) *Builder { b.listener = l return b } func (b *Builder) Run(ctx context.Context) error { + if b.shadowMode { + if err := b.syncStore(1); err != nil { + return err + } + } + if err := b.InitPendingBlock(); err != nil { return err } @@ -84,17 +126,45 @@ func (b *Builder) Run(ctx context.Context) error { } close(doneListen) }() + if b.shadowMode { + go func() { + if pErr := b.shadowTxns(ctx); pErr != nil { + b.log.Errorw("shadowTxns", "err", pErr) + } + }() + } + go func() { + if b.shadowMode { + for { + select { + case <-b.chanFinaliseShadow: + b.chanFinalise <- struct{}{} + case <-ctx.Done(): + return + } + } + } + for { + select { + case <-time.After(b.blockTime): + b.chanFinalise <- struct{}{} + case <-ctx.Done(): + return + } + } + }() for { select { case <-ctx.Done(): <-doneListen return nil - case <-time.After(b.blockTime): + case <-b.chanFinalise: b.log.Debugw("Finalising new block") if err := b.Finalise(); err != nil { return err } + <-b.chanFinalised } } } @@ -314,6 +384,17 @@ func (b *Builder) depletePool(ctx context.Context) error { b.log.Debugw("failed txn", "hash", userTxn.Transaction.Hash().String(), "err", err.Error()) } + if b.shadowMode { + fmt.Println("<-chanNumTxnsToShadow ") + numTxnsToExecute := <-b.chanNumTxnsToShadow + fmt.Println("chanNumTxnsToShadow <- <- ") + b.chanNumTxnsToShadow <- numTxnsToExecute - 1 + if numTxnsToExecute-1 == 0 { + b.chanFinaliseShadow <- struct{}{} + <-b.chanNumTxnsToShadow + } + } + select { case <-ctx.Done(): return nil @@ -322,6 +403,26 @@ func (b *Builder) depletePool(ctx context.Context) error { } } +func getPaidOnL1Fees(txn *mempool.BroadcastedTransaction) ([]*felt.Felt, error) { + if tx, ok := (txn.Transaction).(*core.L1HandlerTransaction); ok { + handleDepositEPS, err := new(felt.Felt).SetString("0x2d757788a8d8d6f21d1cd40bce38a8222d70654214e96ff95d8086e684fbee5") + if err != nil { + return nil, err + } + handleTokenDepositEPS, err := new(felt.Felt).SetString("0x1b64b1b3b690b43b9b514fb81377518f4039cd3e4f4914d8a6bdf01d679fb19") + if err != nil { + return nil, err + } + if tx.EntryPointSelector.Equal(handleDepositEPS) { + return []*felt.Felt{tx.CallData[2]}, nil + } else if tx.EntryPointSelector.Equal(handleTokenDepositEPS) { + return []*felt.Felt{tx.CallData[4]}, nil + } + return nil, fmt.Errorf("failed to get fees_paid_on_l1, unkmown entry point selector") + } + return []*felt.Felt{}, nil +} + func (b *Builder) runTxn(txn *mempool.BroadcastedTransaction) error { b.pendingLock.Lock() defer b.pendingLock.Unlock() @@ -331,6 +432,10 @@ func (b *Builder) runTxn(txn *mempool.BroadcastedTransaction) error { classes = append(classes, txn.DeclaredClass) } + feesPaidOnL1, err := getPaidOnL1Fees(txn) + if err != nil { + return err + } blockInfo := &vm.BlockInfo{ Header: &core.Header{ Number: b.pendingBlock.Block.Number, @@ -341,7 +446,7 @@ func (b *Builder) runTxn(txn *mempool.BroadcastedTransaction) error { }, } - fee, _, trace, err := b.vm.Execute([]core.Transaction{txn.Transaction}, classes, []*felt.Felt{}, blockInfo, state, + fee, _, trace, err := b.vm.Execute([]core.Transaction{txn.Transaction}, classes, feesPaidOnL1, blockInfo, state, b.bc.Network(), false, false, false, false) if err != nil { return err @@ -400,3 +505,94 @@ func mergeStateDiffs(oldStateDiff, newStateDiff *core.StateDiff) *core.StateDiff return oldStateDiff } + +func (b *Builder) shadowTxns(ctx context.Context) error { + for { + b.chanFinalised <- struct{}{} + builderHeadBlock, err := b.bc.Head() + if err != nil { + return err + } + snHeadBlock, err := b.starknetData.BlockLatest(ctx) + if err != nil { + return err + } + b.log.Debugw(fmt.Sprintf("Juno head at block %d, Sepolia at block %d, attempting to sequence next block", builderHeadBlock.Number, snHeadBlock.Number)) + if builderHeadBlock.Number < snHeadBlock.Number { + block, _, classes, err := b.getSyncData(builderHeadBlock.Number + 1) // todo: don't need state updates here + if err != nil { + return err + } + fmt.Println("chanNumTxnsToShadow <- ") + b.chanNumTxnsToShadow <- int(block.TransactionCount) + fmt.Println(" not blocking chanNumTxnsToShadow <- ") + for i, txn := range block.Transactions { + var declaredClass core.Class + declareTxn, ok := txn.(*core.DeclareTransaction) + if ok { + declaredClass = classes[*declareTxn.ClassHash] + } + err = b.pool.Push( + &mempool.BroadcastedTransaction{ + Transaction: txn, + DeclaredClass: declaredClass, + }) + if err != nil { + return err + } + qwe := declaredClass == nil + b.log.Debugw(fmt.Sprintf("Pushed txn number %d, %v", i, qwe)) // Todo : remove + } + + } else { + var sleepTime uint = 1 + b.log.Debugw("Juno Sequencer is at Sepolia chain head. Sleeping for %ds before querying for a new block.", sleepTime) + time.Sleep(time.Duration(sleepTime)) + } + } +} + +func (b *Builder) syncStore(toBlockNum uint64) error { + var i uint64 + for i = 0; i < toBlockNum; i++ { + b.log.Infow("Sequencer, syncing block", "blockNumber", i) + block, su, classes, err := b.getSyncData(i) + if err != nil { + return err + } + commitments, err := b.bc.SanityCheckNewHeight(block, su, classes) + if err != nil { + return err + } + err = b.bc.Store(block, commitments, su, classes) + if err != nil { + return err + } + } + return nil +} + +func (b *Builder) getSyncData(blockNumber uint64) (*core.Block, *core.StateUpdate, + map[felt.Felt]core.Class, error, +) { + block, err := b.starknetData.BlockByNumber(context.Background(), blockNumber) + if err != nil { + return nil, nil, nil, err + } + su, err := b.starknetData.StateUpdate(context.Background(), blockNumber) + if err != nil { + return nil, nil, nil, err + } + txns := block.Transactions + classes := make(map[felt.Felt]core.Class) + for _, txn := range txns { + if t, ok := txn.(*core.DeclareTransaction); ok { + class, err := b.starknetData.Class(context.Background(), t.ClassHash) + if err != nil { + return nil, nil, nil, err + } + classes[*t.ClassHash] = class + } + } + return block, su, classes, nil +} diff --git a/builder/builder_test.go b/builder/builder_test.go index 21a74e71c4..04f86474d0 100644 --- a/builder/builder_test.go +++ b/builder/builder_test.go @@ -487,3 +487,65 @@ func TestPrefundedAccounts(t *testing.T) { require.Equal(t, len(expectedExnsInBlock), numExpectedBalance, "Accounts don't have the expected balance") require.True(t, foundExpectedBalance) } + +func TestShadowSepolia(t *testing.T) { + mockCtrl := gomock.NewController(t) + snData := mocks.NewMockStarknetData(mockCtrl) + network := &utils.Sepolia + bc := blockchain.New(pebble.NewMemTest(t), network) + p := mempool.New(pebble.NewMemTest(t)) + log := utils.NewNopZapLogger() + vmm := vm.New(false, log) + seqAddr := utils.HexToFelt(t, "0xDEADBEEF") + privKey, err := ecdsa.GenerateKey(rand.Reader) + require.NoError(t, err) + + blockTime := time.Second + testBuilder := builder.NewShadow(privKey, seqAddr, bc, vmm, blockTime, p, log, snData) + gw := adaptfeeder.New(feeder.NewTestClient(t, network)) + + const numTestBlocks = 3 // Note: depends on the number of blocks that the buidler syncStores (see Run()) + var blocks [numTestBlocks]*core.Block + for i := 0; i < numTestBlocks; i++ { + block, err2 := gw.BlockByNumber(context.Background(), uint64(i)) + require.NoError(t, err2) + blocks[i] = block + su, err2 := gw.StateUpdate(context.Background(), uint64(i)) + require.NoError(t, err2) + snData.EXPECT().BlockByNumber(context.Background(), uint64(i)).Return(block, nil) + snData.EXPECT().StateUpdate(context.Background(), uint64(i)).Return(su, nil) + } + ctx, cancel := context.WithTimeout(context.Background(), numTestBlocks*blockTime) + defer cancel() + // We sync store block 0, then sequence blocks 1 and 2 + snData.EXPECT().BlockLatest(ctx).Return(blocks[1], nil) + snData.EXPECT().BlockLatest(ctx).Return(blocks[2], nil) + snData.EXPECT().BlockLatest(ctx).Return(nil, errors.New("only sequence up to block 2")) + classHashes := []string{ + "0x5c478ee27f2112411f86f207605b2e2c58cdb647bac0df27f660ef2252359c6", + "0xd0e183745e9dae3e4e78a8ffedcce0903fc4900beace4e0abf192d4c202da3", + "0x1b661756bf7d16210fc611626e1af4569baa1781ffc964bd018f4585ae241c1", + "0x4f23a756b221f8ce46b72e6a6b10ee7ee6cf3b59790e76e02433104f9a8c5d1", + } + for _, hash := range classHashes { + classHash := utils.HexToFelt(t, hash) + class, err2 := gw.Class(context.Background(), classHash) + require.NoError(t, err2) + snData.EXPECT().Class(context.Background(), classHash).Return(class, nil) + } + err = testBuilder.Run(ctx) + require.NoError(t, err) + runTest := func(t *testing.T, wantBlockNum uint64, wantBlock *core.Block) { + gotBlock, err := bc.BlockByNumber(wantBlockNum) + require.NoError(t, err) + require.Equal(t, wantBlock.Number, gotBlock.Number) + require.Equal(t, wantBlock.TransactionCount, gotBlock.TransactionCount, "TransactionCount diff") + require.Equal(t, wantBlock.GlobalStateRoot.String(), gotBlock.GlobalStateRoot.String(), "GlobalStateRoot diff") + } + for i := range numTestBlocks { + runTest(t, uint64(i), blocks[i]) + } + head, err := bc.Head() + require.NoError(t, err) + require.Equal(t, uint64(2), head.Number) +} diff --git a/cmd/juno/juno.go b/cmd/juno/juno.go index b7f907585a..547807c111 100644 --- a/cmd/juno/juno.go +++ b/cmd/juno/juno.go @@ -84,6 +84,7 @@ const ( seqEnF = "seq-enable" seqBlockTimeF = "seq-block-time" seqGenesisFileF = "seq-genesis-file" + seqShadowModeF = "seq-shadow-mode" defaultConfig = "" defaulHost = "localhost" @@ -123,6 +124,7 @@ const ( defaultSeqEn = false defaultSeqBlockTime = 60 defaultSeqGenesisFile = "" + defaultSeqShadowMode = false configFlagUsage = "The YAML configuration file." logLevelFlagUsage = "Options: trace, debug, info, warn, error." @@ -176,6 +178,7 @@ const ( seqEnUsage = "Enables sequencer mode of operation" seqBlockTimeUsage = "Time to build a block, in seconds" seqGenesisFileUsage = "Path to the genesis file" + seqShadowModeUsage = "Launches the sequencer in shadow mode (note: network must be set to Sepolia)" ) var Version string @@ -364,6 +367,7 @@ func NewCmd(config *node.Config, run func(*cobra.Command, []string) error) *cobr junoCmd.Flags().Bool(seqEnF, defaultSeqEn, seqEnUsage) junoCmd.Flags().Uint(seqBlockTimeF, defaultSeqBlockTime, seqBlockTimeUsage) junoCmd.Flags().String(seqGenesisFileF, defaultSeqGenesisFile, seqGenesisFileUsage) + junoCmd.Flags().Bool(seqShadowModeF, defaultSeqShadowMode, seqShadowModeUsage) junoCmd.AddCommand(DBSize()) return junoCmd diff --git a/node/genesis.go b/node/genesis.go index 2e9a7b87b8..b6ca6ddb2a 100644 --- a/node/genesis.go +++ b/node/genesis.go @@ -11,7 +11,7 @@ import ( "github.com/NethermindEth/juno/vm" ) -func buildGenesis(genesisPath string, sequencerMode bool, bc *blockchain.Blockchain, v vm.VM, maxSteps uint64) error { +func buildGenesis(genesisPath string, sequencerMode bool, shadowMode bool, bc *blockchain.Blockchain, v vm.VM, maxSteps uint64) error { if _, err := bc.Height(); !errors.Is(err, db.ErrKeyNotFound) { return err } @@ -29,6 +29,8 @@ func buildGenesis(genesisPath string, sequencerMode bool, bc *blockchain.Blockch if err != nil { return err } + case shadowMode: + return nil case sequencerMode: diff = core.EmptyStateDiff() default: diff --git a/node/node.go b/node/node.go index d50669c0f2..e6d55aa57b 100644 --- a/node/node.go +++ b/node/node.go @@ -92,9 +92,10 @@ type Config struct { GatewayAPIKey string `mapstructure:"gw-api-key"` GatewayTimeout time.Duration `mapstructure:"gw-timeout"` - Sequencer bool `mapstructure:"seq-enable"` - SeqBlockTime uint `mapstructure:"seq-block-time"` - GenesisFile string `mapstructure:"seq-genesis-file"` + Sequencer bool `mapstructure:"seq-enable"` + SeqBlockTime uint `mapstructure:"seq-block-time"` + SeqGenesisFile string `mapstructure:"seq-genesis-file"` + SeqShadowMode bool `mapstructure:"seq-shadow-mode"` } type Node struct { @@ -165,6 +166,9 @@ func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen starknetData := adaptfeeder.New(client) var rpcHandler *rpc.Handler if cfg.Sequencer { + if cfg.SeqShadowMode && chain.Network().L2ChainID != utils.Sepolia.L2ChainID { + return nil, fmt.Errorf("the sequencers shadow mode can only be used for %v network. Provided network: %v", utils.Sepolia, cfg.Network) + } pKey, kErr := ecdsa.GenerateKey(rand.Reader) if kErr != nil { return nil, kErr @@ -173,6 +177,10 @@ func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen p := mempool.New(poolDB) sequencer := builder.New(pKey, new(felt.Felt).SetUint64(1337), chain, nodeVM, //nolint:mnd time.Second*time.Duration(cfg.SeqBlockTime), p, log) + if cfg.SeqShadowMode { + sequencer = builder.NewShadow(pKey, new(felt.Felt).SetUint64(1337), chain, nodeVM, time.Second*time.Duration(cfg.SeqBlockTime), p, //nolint: gomnd,lll + log, starknetData) + } rpcHandler = rpc.New(chain, sequencer, throttledVM, version, log).WithMempool(p).WithCallMaxSteps(uint64(cfg.RPCCallMaxSteps)) services = append(services, sequencer) } else { @@ -374,7 +382,7 @@ func (n *Node) Run(ctx context.Context) { n.log.Errorw("Error while migrating the DB", "err", err) return } - if err = buildGenesis(n.cfg.GenesisFile, n.cfg.Sequencer, n.blockchain, vm.New(false, n.log), uint64(n.cfg.RPCCallMaxSteps)); err != nil { + if err = buildGenesis(n.cfg.SeqGenesisFile, n.cfg.Sequencer, n.cfg.SeqShadowMode, n.blockchain, vm.New(false, n.log), uint64(n.cfg.RPCCallMaxSteps)); err != nil { n.log.Errorw("Error building genesis state", "err", err) return }