Conversation
53ba2fa to
cbe0341
Compare
cbe0341 to
1238fe5
Compare
1238fe5 to
241b13a
Compare
241b13a to
25efce9
Compare
25efce9 to
42cd176
Compare
42cd176 to
58a1c46
Compare
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
58a1c46 to
1f4b918
Compare
| PreRunE: func(_ *cobra.Command, args []string) error { | ||
| if !strings.HasPrefix(args[0], "0x") { | ||
| args[0] = "0x" + args[0] | ||
| } | ||
| if !strings.HasPrefix(args[1], "0x") { | ||
| args[1] = "0x" + args[1] | ||
| } | ||
| return nil |
There was a problem hiding this comment.
🟡 submitFibreCmd PreRunE corrupts non-hex blob data by prepending '0x'
The submitFibreCmd.PreRunE unconditionally prepends "0x" to args[1] (blob data) if it doesn't already have that prefix. Later in RunE, cmdnode.DecodeToBytes(args[1]) attempts hex decoding, and on failure falls back to []byte(args[1]). Because PreRunE already mutated the argument, the fallback raw bytes now include the spurious "0x" prefix. For example, a user running submit-fibre <ns> "hello" ends up submitting []byte("0xhello") instead of []byte("hello"). This matches the same pre-existing pattern in submitCmd, but submitFibreCmd is entirely new code introduced in this PR.
Prompt for agents
In nodebuilder/blob/cmd/blob.go, the submitFibreCmd.PreRunE (lines 230-237) should NOT prepend '0x' to args[1] (the blob data). The '0x' prefix should only be added for args[0] (the namespace), since blob data might be plain text. Remove the lines 233-235 that add '0x' to args[1]. The same bug also exists in the regular submitCmd's PreRunE and should be fixed there too for consistency.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
There are two competing fibre blob submission paths, and one of them is incomplete
modfibre.Upload
The newly introduced API, however, is incomplete as it doesn't send the PFF transactions. The comment mentions that users should use modblob.SubmitFibreBlob to send TXs; however, doing so requires reuploading the entire blob over the API, and modblob.SubmitFibreBlob reuploads it again.
Essentially, there is no way for fibre users of Upload to pay for their uploads
modblob.SubmitFibreBlob
The purpose of this method is unclear. It duplicates the entire fibre blob submission on the old blob service, however, the intention was clearly to introduce a new API as seen in the new fibre module, so why duplicate? If the intention was to allow sending PFFs through this module, then it is unclear why blob module would be responsible for that and not fibre.
Too many layers of indirections
With this PR we have:
- modfibre.Module
fibre.Clientfibre.client- txclient fibre methods
appfibre.Client
This is extremely messy and a pure pain to review. We can easily squash a bunch of them with no repercussions into:
- modfibre.Module
fibre.Serviceappfibre.Client.
Besides, for whatever reason, out of all those layers, the txclient turned out to be responsible for actually uploading. TxClient, Carl! It is supposed to send transactions and not call appfibre.Client.Upload.
Subscriptions
There is no way to subscribe for blobs from fibre in the order they'be landed through consensus.
nodebuilder/fibre/fibre.go
Outdated
| // It encodes the blob, constructs a payment promise, uploads encoded rows to FSPs, | ||
| // and aggregates validator availability signatures. It does NOT submit MsgPayForFibre on-chain. | ||
| // Use blob.SubmitFibreBlob for the full submit flow. | ||
| Upload(ctx context.Context, ns libshare.Namespace, data []byte) (*fibre.UploadResult, error) |
There was a problem hiding this comment.
interface definition should be together with types it uses for modules in case those types are API level heler types(Result/Response), instead of internal types(Blob)
nodebuilder/fibre/fibre.go
Outdated
| // It encodes the blob, constructs a payment promise, uploads encoded rows to FSPs, | ||
| // and aggregates validator availability signatures. It does NOT submit MsgPayForFibre on-chain. | ||
| // Use blob.SubmitFibreBlob for the full submit flow. | ||
| Upload(ctx context.Context, ns libshare.Namespace, data []byte) (*fibre.UploadResult, error) |
There was a problem hiding this comment.
This is UploadResult, but GetBlobResponse? Consistencty?
| func (m *blobMetrics) observeUpload(ctx context.Context, dur time.Duration, blobSize int, err error) { | ||
| if m == nil { | ||
| return | ||
| } | ||
| m.uploadDuration.Record(ctx, dur.Seconds(), blobAttrs(blobSize, err)) | ||
| } | ||
|
|
||
| func (m *blobMetrics) observeSubmit(ctx context.Context, dur time.Duration, blobSize int, err error) { | ||
| if m == nil { | ||
| return | ||
| } | ||
| m.submitDuration.Record(ctx, dur.Seconds(), blobAttrs(blobSize, err)) | ||
| } | ||
|
|
||
| func (m *blobMetrics) observeGet(ctx context.Context, dur time.Duration, err error) { |
There was a problem hiding this comment.
Do we really need those metrics if they are already part of the appfibre.Client, besides the Submit, but that's just tx submission metric?
There was a problem hiding this comment.
These metrics track different layers. fibre/metrics.go measures end-to-end node-level latency (upload, submit, get), txclient measures tx submission and gas estimation, and appfibre.Client tracks FSP networking internals. They complement each other — knowing that a submit took 5s total but only 200ms on tx submission tells you the bottleneck is in the upload phase.
state/txclient/fibre.go
Outdated
| cl, err := appfibre.NewClient(c.keyring, appfibre.DefaultClientConfig()) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to setup fibre client: %w", err) | ||
| } | ||
| if err := cl.Start(c.ctx); err != nil { | ||
| return fmt.Errorf("failed to start fibre client: %w", err) | ||
| } | ||
| c.fibreClient = cl |
There was a problem hiding this comment.
Please provide fibre client as a component to DI with full lifecycling. Its Start method is blocking and is meant to called while node starts and break node start if there is misconfiguration, rather then being called in runtime
|
This PR was done in accordance to the ADR, made by @cmwaters.
Auto-fetching full fibre data from FSPs needs design work first. Feel free to open an ADR update with a proposal |
|
@vgonkivs, I acknowledge that the PR implements the ADR. However, it does not change the fact that users can't pay for the uploads they make and we should fix this.
The subscription does not imply listening for data from FSPs, but listening to new fibre-blobs in the square and fetching respective fibre blobs by commitment. |
|
Feel free to open an ADR update with a proposal |
|
Nothing is stopping us from modifying the ADR in this PR. That's a normal feedback process where during implementation issues are discovered and spec/adr is updated accordingly. |
|
There is no final decision yet on the upload, so no changes were made to this part. |
4411e84 to
4b98cba
Compare
| allFibre := slices.IndexFunc(blobs, func(b *Blob) bool { return !b.IsFibreBlob() }) == -1 | ||
| anyFibre := slices.IndexFunc(blobs, func(b *Blob) bool { return b.IsFibreBlob() }) != -1 | ||
|
|
||
| if anyFibre && !allFibre { | ||
| return 0, ErrMixedBlobTypes | ||
| } | ||
|
|
||
| if allFibre { | ||
| return s.submitFibreBlobs(ctx, blobs, txConfig) |
There was a problem hiding this comment.
🟡 Empty blobs slice is silently routed through fibre path, returning success with height 0
When Submit is called with an empty blobs slice, slices.IndexFunc returns -1 for both lambda predicates, making allFibre = (-1 == -1) = true and anyFibre = (-1 != -1) = false. This causes the empty submission to enter submitFibreBlobs, which iterates zero times and returns (0, nil) — a silent success at height 0. Previously, the code would pass the empty slice directly to blobSubmitter.SubmitPayForBlob, which would handle it through the regular consensus path (likely returning an error). The new behavior silently succeeds with a meaningless height, which could confuse callers.
| allFibre := slices.IndexFunc(blobs, func(b *Blob) bool { return !b.IsFibreBlob() }) == -1 | |
| anyFibre := slices.IndexFunc(blobs, func(b *Blob) bool { return b.IsFibreBlob() }) != -1 | |
| if anyFibre && !allFibre { | |
| return 0, ErrMixedBlobTypes | |
| } | |
| if allFibre { | |
| return s.submitFibreBlobs(ctx, blobs, txConfig) | |
| allFibre := len(blobs) > 0 && slices.IndexFunc(blobs, func(b *Blob) bool { return !b.IsFibreBlob() }) == -1 | |
| anyFibre := slices.IndexFunc(blobs, func(b *Blob) bool { return b.IsFibreBlob() }) != -1 | |
| if anyFibre && !allFibre { | |
| return 0, ErrMixedBlobTypes | |
| } | |
| if allFibre { | |
| return s.submitFibreBlobs(ctx, blobs, txConfig) | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
| func (s *Service) submitFibreBlobs(ctx context.Context, blobs []*Blob, txConfig *SubmitOptions) (uint64, error) { | ||
| if s.fibreSubmitter == nil { | ||
| return 0, fmt.Errorf("fibre submitter is not available") | ||
| } | ||
|
|
||
| var height uint64 | ||
| for _, blob := range blobs { | ||
| res, _, err := s.fibreSubmitter.Submit(ctx, blob.Namespace(), blob.Data(), txConfig) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| height = res.Height | ||
| } | ||
| return height, nil | ||
| } |
There was a problem hiding this comment.
🔴 Non-atomic fibre blob submission loses track of successfully submitted blobs on partial failure
submitFibreBlobs submits each fibre blob individually in a loop via s.fibreSubmitter.Submit, which performs a full on-chain MsgPayForFibre transaction per blob (fibre/service.go:83). If the first blob succeeds (is permanently committed on-chain) but the second blob fails, the function returns (0, err), discarding the height of the successfully submitted first blob. The caller receives an error and has no way to discover that one blob was already included on-chain. This violates the "atomically" contract documented on Submit and can lead to lost blob tracking.
Loop that discards partial results on failure
var height uint64
for _, blob := range blobs {
res, _, err := s.fibreSubmitter.Submit(ctx, blob.Namespace(), blob.Data(), txConfig)
if err != nil {
return 0, err // discards previously successful submissions
}
height = res.Height
}
return height, nilPrompt for agents
The submitFibreBlobs function in blob/service.go submits fibre blobs one-by-one in a loop. Each call to s.fibreSubmitter.Submit performs a full on-chain MsgPayForFibre transaction (see fibre/service.go Submit method). If a blob in the middle of the loop fails, previously submitted blobs are already on-chain and cannot be rolled back, but the function returns (0, err) discarding any successfully submitted heights.
The Submit method's doc comment promises atomic submission, but fibre blobs are inherently submitted individually. There are several approaches to fix this:
1. Return partial results: change the return type or use a structured error that includes the heights of successfully submitted blobs.
2. Validate that only a single fibre blob is allowed per Submit call, since atomicity cannot be guaranteed for multiple fibre blobs.
3. Update the documentation to explicitly state that fibre blob submission is not atomic when multiple blobs are provided.
4. Collect all results and return the last successful height even on error, so the caller at least knows some blobs landed.
Option 2 is the safest approach since it maintains the atomicity guarantee by restricting the input.
Was this helpful? React with 👍 or 👎 to provide feedback.
Resolves https://linear.app/celestia/issue/PROTOCO-1332/implement-fibre-module