-
Notifications
You must be signed in to change notification settings - Fork 253
[Spike] Direct TX #2514
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Spike] Direct TX #2514
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # direct TX | ||
|
|
||
| - What format can we expect for the direct TX on the DA? some wrapper type would be useful to unpack and distinct from | ||
| random bytes | ||
| - Should we always include direct TX although they may be duplicates to mempool TX? spike: yes | ||
| - Should we fill the block space with directTX if possible or reserve space for mempool TX | ||
|
|
||
| ## Smarter sequencer | ||
| build blocks by max bytes from the request rather than returning what was added as a batch before from the syncer. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ import ( | |
| "github.com/evstack/ev-node/pkg/p2p/key" | ||
| "github.com/evstack/ev-node/pkg/store" | ||
| "github.com/evstack/ev-node/sequencers/based" | ||
| "github.com/evstack/ev-node/sequencers/single" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
@@ -169,11 +170,13 @@ func NewExtendedRunNodeCmd(ctx context.Context) *cobra.Command { | |
| return fmt.Errorf("failed to create P2P client: %w", err) | ||
| } | ||
|
|
||
| directTXSeq := single.NewDirectTxSequencer(sequencer, logger, datastore, 100) // todo (Alex): what is a good max value | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // Pass the raw rollDA implementation to StartNode. | ||
| // StartNode might need adjustment if it strictly requires coreda.Client methods. | ||
| // For now, assume it can work with coreda.DA or will be adjusted later. | ||
| // We also need to pass the namespace config for rollDA. | ||
| return rollcmd.StartNode(logger, cmd, executor, sequencer, rollDA, p2pClient, datastore, nodeConfig, node.NodeOptions{}) | ||
| return rollcmd.StartNode(logger, cmd, executor, directTXSeq, rollDA, p2pClient, datastore, nodeConfig, node.NodeOptions{}) | ||
| }, | ||
| } | ||
|
|
||
|
|
||
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,7 +38,6 @@ var RunCmd = &cobra.Command{ | |
| } | ||
|
|
||
| logger := rollcmd.SetupLogger(nodeConfig.Log) | ||
|
|
||
| daJrpc, err := jsonrpc.NewClient(context.Background(), logger, nodeConfig.DA.Address, nodeConfig.DA.AuthToken, nodeConfig.DA.Namespace) | ||
| if err != nil { | ||
| return err | ||
|
|
@@ -68,6 +67,14 @@ var RunCmd = &cobra.Command{ | |
| return err | ||
| } | ||
|
|
||
| directTXSequencer := single.NewDirectTxSequencer( | ||
| sequencer, | ||
| logger, | ||
| datastore, | ||
| 100, // todo (Alex): what is a good value? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| nodeConfig.ForcedInclusion, | ||
| ) | ||
|
|
||
| nodeKey, err := key.LoadNodeKey(filepath.Dir(nodeConfig.ConfigPath())) | ||
| if err != nil { | ||
| return err | ||
|
|
@@ -78,7 +85,7 @@ var RunCmd = &cobra.Command{ | |
| return err | ||
| } | ||
|
|
||
| return rollcmd.StartNode(logger, cmd, executor, sequencer, &daJrpc.DA, p2pClient, datastore, nodeConfig, node.NodeOptions{}) | ||
| return rollcmd.StartNode(logger, cmd, executor, directTXSequencer, &daJrpc.DA, p2pClient, datastore, nodeConfig, node.NodeOptions{}) | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,12 +88,12 @@ var RunCmd = &cobra.Command{ | |
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| directTXSeq := single.NewDirectTxSequencer(sequencer, logger, datastore, 100) // todo (Alex): find a good default | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| p2pClient, err := p2p.NewClient(nodeConfig, nodeKey, datastore, logger, p2p.NopMetrics()) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| return rollcmd.StartNode(logger, cmd, executor, sequencer, &daJrpc.DA, p2pClient, datastore, nodeConfig, node.NodeOptions{}) | ||
| return rollcmd.StartNode(logger, cmd, executor, directTXSeq, &daJrpc.DA, p2pClient, datastore, nodeConfig, node.NodeOptions{}) | ||
| }, | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||||
| package block | ||||||||
|
|
||||||||
| import ( | ||||||||
| "context" | ||||||||
| "crypto/sha256" | ||||||||
| "errors" | ||||||||
| "github.com/evstack/ev-node/types" | ||||||||
| ) | ||||||||
|
|
||||||||
| type DirectTransaction struct { | ||||||||
| TxHash types.Hash | ||||||||
| FirstSeenDAHeight uint64 // DA block time when the tx was seen | ||||||||
| Included bool // Whether it has been included in a block | ||||||||
| IncludedAt uint64 // Height at which it was included | ||||||||
| TX []byte | ||||||||
| } | ||||||||
|
|
||||||||
| func (m *Manager) handlePotentialDirectTXs(ctx context.Context, bz []byte, daHeight uint64) bool { | ||||||||
| var unsignedData types.Data // todo (Alex): we need some type to separate from noise | ||||||||
| err := unsignedData.UnmarshalBinary(bz) | ||||||||
| if err != nil { | ||||||||
| m.logger.Debug("failed to unmarshal unsigned data, error", err) | ||||||||
| return false | ||||||||
| } | ||||||||
| if len(unsignedData.Txs) == 0 { | ||||||||
| m.logger.Debug("ignoring empty unsigned data, daHeight: ", daHeight) | ||||||||
| return false | ||||||||
| } | ||||||||
| if unsignedData.Metadata.ChainID != m.genesis.ChainID { | ||||||||
| m.logger.Debug("ignoring unsigned data from different chain, daHeight: ", daHeight) | ||||||||
| return false | ||||||||
| } | ||||||||
| //Early validation to reject junk data | ||||||||
| //if !m.isValidSignedData(&unsignedData) { | ||||||||
| // m.logger.Debug("invalid data signature, daHeight: ", daHeight) | ||||||||
| // return false | ||||||||
| //} | ||||||||
| h := m.headerCache.GetItem(daHeight) | ||||||||
| if h == nil { | ||||||||
| panic("header not found in cache") // todo (Alex): not sure if headers are always available before data. better assume not | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
|
||||||||
| //m.logger.Debug("header not found in cache, height:", daHeight) | ||||||||
| //return false | ||||||||
| } | ||||||||
| for _, tx := range unsignedData.Txs { | ||||||||
| txHash := sha256.New().Sum(tx) | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The transaction hashing logic is incorrect. txHashBytes := sha256.Sum256(tx)
txHash := txHashBytes[:] |
||||||||
| d := DirectTransaction{ | ||||||||
| TxHash: txHash, | ||||||||
| TX: unsignedData.Txs[0], | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a bug in this loop. The code is iterating over TX: tx, |
||||||||
| FirstSeenDAHeight: daHeight, | ||||||||
| Included: false, | ||||||||
| IncludedAt: 0, | ||||||||
| } | ||||||||
| _ = d | ||||||||
| } | ||||||||
| return true | ||||||||
| } | ||||||||
|
|
||||||||
| var ErrMissingDirectTx = errors.New("missing direct tx") | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| package block | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "strings" | ||
| "sync/atomic" | ||
| "time" | ||
|
|
||
| "github.com/evstack/ev-node/core/da" | ||
| coreda "github.com/evstack/ev-node/core/da" | ||
| "github.com/evstack/ev-node/core/sequencer" | ||
| "github.com/evstack/ev-node/types" | ||
| ds "github.com/ipfs/go-datastore" | ||
| kt "github.com/ipfs/go-datastore/keytransform" | ||
| "github.com/ipfs/go-log/v2" | ||
| ) | ||
|
|
||
| const ( | ||
| keyPrefixDirTXSeen = "dTX" | ||
| ) | ||
|
|
||
| // DirectTxReaper is responsible for periodically retrieving direct transactions from the DA layer, | ||
| // filtering out already seen transactions, and submitting new transactions to the sequencer. | ||
| type DirectTxReaper struct { | ||
| da da.DA | ||
| sequencer sequencer.DirectTxSequencer | ||
| chainID string | ||
| interval time.Duration | ||
| logger log.EventLogger | ||
| ctx context.Context | ||
| seenStore ds.Batching | ||
| manager *Manager | ||
| daHeight *atomic.Uint64 | ||
| } | ||
|
|
||
| // NewDirectTxReaper creates a new DirectTxReaper instance with persistent seenTx storage. | ||
| func NewDirectTxReaper( | ||
| ctx context.Context, | ||
| da coreda.DA, | ||
| sequencer sequencer.DirectTxSequencer, | ||
| manager *Manager, | ||
| chainID string, | ||
| interval time.Duration, | ||
| logger log.EventLogger, | ||
| store ds.Batching, | ||
| daStartHeight uint64, | ||
| ) *DirectTxReaper { | ||
| if daStartHeight == 0 { | ||
| daStartHeight = 1 | ||
| } | ||
| if interval <= 0 { | ||
| interval = 100 * time.Millisecond | ||
| } | ||
| daHeight := new(atomic.Uint64) | ||
| daHeight.Store(daStartHeight) | ||
| return &DirectTxReaper{ | ||
| da: da, | ||
| sequencer: sequencer, | ||
| chainID: chainID, | ||
| interval: interval, | ||
| logger: logger, | ||
| ctx: ctx, | ||
| seenStore: kt.Wrap(store, &kt.PrefixTransform{ | ||
| Prefix: ds.NewKey(keyPrefixDirTXSeen), | ||
| }), | ||
| manager: manager, | ||
| daHeight: daHeight, | ||
| } | ||
| } | ||
|
|
||
| // Start begins the reaping process at the specified interval. | ||
| func (r *DirectTxReaper) Start(ctx context.Context) { | ||
| r.ctx = ctx | ||
| ticker := time.NewTicker(r.interval) | ||
| defer ticker.Stop() | ||
|
|
||
| r.logger.Info("DirectTxReaper started", "interval", r.interval) | ||
|
|
||
| for { | ||
| select { | ||
| case <-ctx.Done(): | ||
| r.logger.Info("DirectTxReaper stopped") | ||
| return | ||
| case <-ticker.C: | ||
| daHeight := r.daHeight.Load() | ||
| if err := r.retrieveDirectTXs(daHeight); err != nil { | ||
| if strings.Contains(err.Error(), coreda.ErrHeightFromFuture.Error()) { | ||
| r.logger.Debug("IDs not found at height", "height", daHeight) | ||
| } else { | ||
| r.logger.Error("Submit direct txs to sequencer", "error", err) | ||
| } | ||
| continue | ||
| } | ||
| r.daHeight.Store(daHeight + 1) | ||
|
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| // retrieveDirectTXs retrieves direct transactions from the DA layer and submits them to the sequencer. | ||
| func (r *DirectTxReaper) retrieveDirectTXs(daHeight uint64) error { | ||
| // Get the latest DA height | ||
| // Get all blob IDs at the current DA height | ||
| result, err := r.da.GetIDs(r.ctx, daHeight, nil) | ||
| if err != nil { | ||
| return fmt.Errorf("get IDs from DA: %w", err) | ||
| } | ||
| if result == nil || len(result.IDs) == 0 { | ||
| r.logger.Debug("No blobs at current DA height", "height", daHeight) | ||
| return nil | ||
| } | ||
| r.logger.Debug("IDs at current DA height", "height", daHeight, "count", len(result.IDs)) | ||
|
|
||
| // Get the blobs for all IDs | ||
| blobs, err := r.da.Get(r.ctx, result.IDs, nil) | ||
| if err != nil { | ||
| return fmt.Errorf("get blobs from DA: %w", err) | ||
| } | ||
| r.logger.Debug("Blobs found at height", "height", daHeight, "count", len(blobs)) | ||
|
|
||
| var newTxs []sequencer.DirectTX | ||
| for _, blob := range blobs { | ||
| r.logger.Debug("Processing blob data") | ||
|
|
||
| // Process each blob to extract direct transactions | ||
| var data types.Data | ||
| err := data.UnmarshalBinary(blob) | ||
| if err != nil { | ||
| r.logger.Debug("Unexpected payload skipping ", "error", err) | ||
| continue | ||
| } | ||
|
|
||
| // Skip blobs from different chains | ||
| if data.Metadata.ChainID != r.chainID { | ||
| r.logger.Debug("Ignoring data from different chain", "chainID", data.Metadata.ChainID, "expectedChainID", r.chainID) | ||
| continue | ||
| } | ||
|
|
||
| // Process each transaction in the blob | ||
| for i, tx := range data.Txs { | ||
| txHash := hashTx(tx) | ||
| has, err := r.seenStore.Has(r.ctx, ds.NewKey(txHash)) | ||
| if err != nil { | ||
| return fmt.Errorf("check seenStore: %w", err) | ||
| } | ||
| if !has { | ||
| newTxs = append(newTxs, sequencer.DirectTX{ | ||
| TX: tx, | ||
| ID: result.IDs[i], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a critical bug in how the blob ID is being assigned to direct transactions. The code uses the transaction's index within a blob ( The logic should be to iterate through blobs with their index to get the corresponding blob ID, and then assign that same blob ID to all transactions found within that blob. |
||
| FirstSeenHeight: daHeight, | ||
| FirstSeenTime: result.Timestamp.Unix(), | ||
| }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if len(newTxs) == 0 { | ||
| r.logger.Debug("No new direct txs to submit") | ||
| return nil | ||
| } | ||
|
|
||
| r.logger.Debug("Submitting direct txs to sequencer", "txCount", len(newTxs)) | ||
| err = r.sequencer.SubmitDirectTxs(r.ctx, newTxs...) | ||
| if err != nil { | ||
| return fmt.Errorf("submit direct txs to sequencer: %w", err) | ||
| } | ||
| // Mark the transactions as seen | ||
| for _, v := range newTxs { | ||
| txHash := hashTx(v.TX) | ||
| if err := r.seenStore.Put(r.ctx, ds.NewKey(txHash), []byte{1}); err != nil { | ||
| return fmt.Errorf("persist seen tx: %w", err) | ||
| } | ||
| } | ||
| r.logger.Debug("Successfully submitted direct txs") | ||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What should we do when a sequencer was not able to add a direct-TX within the time window?