Skip to content

Conversation

Chengxuan
Copy link
Contributor

The existing FFTM lacks the functionality of updating the values of transaction parameters. The values are completely controlled by the transaction handler to avoid unexpected outcomes due to user intervention.

However, for users who are familiar with the transaction lifecycle and have more advanced tuning requirements, an API allowing them to update the transaction parameter values directly is valuable, especially when TM is stuck in a situation that requires manual resolution.

This PR introduces:

  1. A Patch API for transactions,
  2. A new function HandleTransactionUpdate in the Transaction Handler interface that needs to be implemented.
  3. An implementation in the Simple Transaction Handler that handles transaction update synchronously

Signed-off-by: Chengxuan Xing <[email protected]>
@Chengxuan Chengxuan marked this pull request as ready for review August 6, 2025 09:07
@Chengxuan Chengxuan requested a review from a team as a code owner August 6, 2025 09:07
Comment on lines +278 to +280
if syncRequest.txUpdates.GasPrice != nil {
return nil, i18n.NewError(ctx, tmmsgs.MsgTxHandlerUnsupportedFieldForUpdate, "gasPrice")
}
Copy link
Contributor Author

@Chengxuan Chengxuan Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order for gasPrice field to be updatable, the process transaction logic needs to change to do more sophisticated gas price tracking and management. Therefore, returning an error for the simple tx handler for now.

This constraint could be lifted with a separate PR if further enhancement to this simple example handler is required.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting (per above discussions) this is a reference implementation, and the expectation is that providers of enterprise versions of FFTM are likely extending this simple engine with a much more sophisticated engine

Signed-off-by: Chengxuan Xing <[email protected]>
@peterbroadhurst peterbroadhurst self-requested a review August 13, 2025 15:05
Copy link
Contributor

@peterbroadhurst peterbroadhurst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some commentary for discussion on this great looking change before I'm confident it's ready to go in, plus just one explicitly request for a change where we had "new"/"old" strings being used rather than a Go structure... but more than that I didn't understand the purpose of that go structure.

// ApplyExternalTxUpdates applies the external updates to the managed transaction
// and returns the updates that were applied, whether any update occurred, and a map
// of changed fields with their old and new values for easy JSON marshalling.
func (mtx *ManagedTX) ApplyExternalTxUpdates(txUpdate TXUpdatesExternal) (txUpdates TXUpdates, updated bool, valueChangeMap map[string]map[string]string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the design I believe is that this is a helper function that policy implementations (which hold the complex threading model) can call back into, at the point the "task" of editing a transaction has made it to the right locked in-memory code that is safe to make the change.

Will validate this understanding through the rest of the review.

valueChangeMap = make(map[string]map[string]string)

if txUpdate.To != nil && mtx.To != *txUpdate.To {
valueChangeMap["to"] = map[string]string{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old/new item needs to be an API structured type, rather than string fields please.

I haven't reached the point in understanding what this is used for:

... and a map
// of changed fields with their old and new values for easy JSON marshalling.

Gas *fftypes.FFBigInt `json:"gas,omitempty"`
Value *fftypes.FFBigInt `json:"value,omitempty"`
GasPrice *fftypes.JSONAny `json:"gasPrice,omitempty"`
TransactionData *string `json:"transactionData,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please can we ensure we have a very clear knowledge (documented would be great in field-level docs) for how you clear this field.

Noting that means distinguishing between:

  • Not changing the transactionData
  • Setting the transactionData to be empty (simple transfer transaction)

We must ensure this works - even with the reference OSS handler.

The most common edit for experienced users fixing transaction that is broken, will be to turn it into a null transaction:

  • Leaving the nonce unchanged
  • Bumping the gas price artificially to allow a replacement transaction to go in
  • Changing the value to be zero (if it wasn't before)
  • Clearing the transactionData

This type of inert transaction that can be mined at very minimal execution transaction, is the safest way to clear bubbles where a transaction is breaking the pipe.

Noting a really sticky derivation of this made possible by this PR is:

  1. Add a new transaction that is inert, but gets shoved at the end of the nonce list
  2. Edit that transaction to have a nonce that fills a gap

if txUpdate == nil {
return nil, i18n.NewError(ctx, tmmsgs.MsgInvalidSortDirection)
}
updatedTx, err := m.txHandler.HandleTransactionUpdate(ctx, txID, *txUpdate)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok - so the whole of the job of thread management etc. is delegated down into the policy engine implementation 👍

Comment on lines +278 to +280
if syncRequest.txUpdates.GasPrice != nil {
return nil, i18n.NewError(ctx, tmmsgs.MsgTxHandlerUnsupportedFieldForUpdate, "gasPrice")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting (per above discussions) this is a reference implementation, and the expectation is that providers of enterprise versions of FFTM are likely extending this simple engine with a much more sophisticated engine

Copy link
Contributor

@EnriqueL8 EnriqueL8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @Chengxuan - I would love to see a document explaining when a TX can be patched, what does it mean to resume/pause/suspend a transaction, overall what does the transaction lifecycle look like. We should put this in the main docs possibly under a new section.

@peterbroadhurst
Copy link
Contributor

@EnriqueL8 - couple of things to think about there with your ask to @Chengxuan :

  1. There's no exposure of this very valuable contribution into the wider API of FireFly (this is just an EVM Connector feature)
  2. The answers to your questions are really about the behavior of the Policy Manager plugin that implements the behavior enabled by this new plug point. The documentation for that would be for the implementer of that enterprise extension.

@EnriqueL8
Copy link
Contributor

Makes sense - it would be on the implementation and when we expose this in the FireFly API then we can write a doc/relase notes explaining the behavior

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants