Skip to content

Commit

Permalink
feat(ARCO-320): force validation (#756)
Browse files Browse the repository at this point in the history
  • Loading branch information
shotasilagadzetaal authored Jan 23, 2025
1 parent 2bc498c commit 657dd57
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 199 deletions.
8 changes: 8 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,14 @@ A callback with status `MINED_IN_STALE_BLOCK` will be sent for that transaction
| In longest chain, not in stale chain | `MINED_IN_STALE_BLOCK` | Transaction will be rebroadcasted and cycle through statuses again until is found in the longest chain |
| In stale chain only (no reorg) | `MINED_IN_STALE_BLOCK` | Transaction will be rebroadcasted and cycle through statuses again until is found in the longest chain |

## Forcing validation

If the `X-ForceValidation` header is set, the tx will be validated regardless of the other header values.

Example usage:
```
X-ForceValidation: true
```

## Cumulative fees validation

Expand Down
8 changes: 8 additions & 0 deletions doc/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ This endpoint is used to send a raw transaction to a miner for inclusion in the
|X-FullStatusUpdates|header|boolean|false|Whether we should have full status updates in callback or not (including SEEN_IN_ORPHAN_MEMPOOL and SEEN_ON_NETWORK statuses).|
|X-MaxTimeout|header|integer|false|Timeout in seconds to wait for new transaction status before request expires (max 30 seconds, default 5)|
|X-SkipFeeValidation|header|boolean|false|Whether we should skip fee validation or not.|
|X-ForceValidation|header|boolean|false|Whether we should force submitted tx validation or not.|
|X-SkipScriptValidation|header|boolean|false|Whether we should skip script validation or not.|
|X-SkipTxValidation|header|boolean|false|Whether we should skip overall tx validation or not.|
|X-CumulativeFeeValidation|header|boolean|false|Whether we should perform cumulative fee validation for fee consolidation txs or not.|
Expand Down Expand Up @@ -817,6 +818,7 @@ X-CallbackUrl: string
X-FullStatusUpdates: true
X-MaxTimeout: 0
X-SkipFeeValidation: true
X-ForceValidation: true
X-SkipScriptValidation: true
X-SkipTxValidation: true
X-CumulativeFeeValidation: true
Expand All @@ -837,6 +839,7 @@ const headers = {
'X-FullStatusUpdates':'true',
'X-MaxTimeout':'0',
'X-SkipFeeValidation':'true',
'X-ForceValidation': 'true',
'X-SkipScriptValidation':'true',
'X-SkipTxValidation':'true',
'X-CumulativeFeeValidation':'true',
Expand Down Expand Up @@ -895,6 +898,7 @@ func main() {
"X-FullStatusUpdates": []string{"true"},
"X-MaxTimeout": []string{"0"},
"X-SkipFeeValidation": []string{"true"},
"X-ForceValidation": []string{"true"},
"X-SkipScriptValidation": []string{"true"},
"X-SkipTxValidation": []string{"true"},
"X-CumulativeFeeValidation": []string{"true"},
Expand Down Expand Up @@ -927,6 +931,7 @@ headers = {
'X-FullStatusUpdates' => 'true',
'X-MaxTimeout' => '0',
'X-SkipFeeValidation' => 'true',
'X-ForceValidation' => 'true',
'X-SkipScriptValidation' => 'true',
'X-SkipTxValidation' => 'true',
'X-CumulativeFeeValidation' => 'true',
Expand Down Expand Up @@ -954,6 +959,7 @@ headers = {
'X-FullStatusUpdates': 'true',
'X-MaxTimeout': '0',
'X-SkipFeeValidation': 'true',
'X-ForceValidation': 'true',
'X-SkipScriptValidation': 'true',
'X-SkipTxValidation': 'true',
'X-CumulativeFeeValidation': 'true',
Expand All @@ -979,6 +985,7 @@ curl -X POST https://arc.taal.com/v1/txs \
-H 'X-FullStatusUpdates: true' \
-H 'X-MaxTimeout: 0' \
-H 'X-SkipFeeValidation: true' \
-H 'X-ForceValidation: true' \
-H 'X-SkipScriptValidation: true' \
-H 'X-SkipTxValidation: true' \
-H 'X-CumulativeFeeValidation: true' \
Expand Down Expand Up @@ -1022,6 +1029,7 @@ This endpoint is used to send multiple raw transactions to a miner for inclusion
|X-FullStatusUpdates|header|boolean|false|Whether we should have full status updates in callback or not (including SEEN_IN_ORPHAN_MEMPOOL and SEEN_ON_NETWORK statuses).|
|X-MaxTimeout|header|integer|false|Timeout in seconds to wait for new transaction status before request expires (max 30 seconds, default 5)|
|X-SkipFeeValidation|header|boolean|false|Whether we should skip fee validation or not.|
|X-ForceValidation|header|boolean|false|Whether we should force submitted tx validation or not.|
|X-SkipScriptValidation|header|boolean|false|Whether we should skip script validation or not.|
|X-SkipTxValidation|header|boolean|false|Whether we should skip overall tx validation or not.|
|X-CumulativeFeeValidation|header|boolean|false|Whether we should perform cumulative fee validation for fee consolidation txs or not.|
Expand Down
171 changes: 90 additions & 81 deletions internal/api/handler/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,40 +228,42 @@ func (m ArcDefaultHandler) postTransaction(ctx echo.Context, params api.POSTTran
return PostResponse{e.Status, e}
}

// check if we already have the transaction in db (so no need to validate)
tx, err := m.getTransactionStatus(reqCtx, txIDs[0])
if err != nil {
// if we have error which is NOT ErrTransactionNotFound, return err
if !errors.Is(err, metamorph.ErrTransactionNotFound) {
e := api.NewErrorFields(api.ErrStatusGeneric, err.Error())
return PostResponse{e.Status, e}
}
} else {
// if we have found transaction skip the validation
transactionOptions.SkipTxValidation = true

// now check if we need to skip the processing of the transaction
callbackAlreadyExists := false
for _, cb := range tx.Callbacks {
if cb.CallbackUrl == transactionOptions.CallbackURL {
callbackAlreadyExists = true
if !transactionOptions.ForceValidation {
// check if we already have the transaction in db (so no need to validate)
tx, err := m.getTransactionStatus(reqCtx, txIDs[0])
if err != nil {
// if we have error which is NOT ErrTransactionNotFound, return err
if !errors.Is(err, metamorph.ErrTransactionNotFound) {
e := api.NewErrorFields(api.ErrStatusGeneric, err.Error())
return PostResponse{e.Status, e}
}
}
} else {
// if we have found transaction skip the validation
transactionOptions.SkipTxValidation = true

// if LastSubmitted doesn't need to be updated and we already have provided callbacks - skip everything and return current status
if time.Since(tx.LastSubmitted.AsTime()) < m.mapExpiryTime && callbackAlreadyExists {
return PostResponse{int(api.StatusOK), &api.TransactionResponse{
Status: int(api.StatusOK),
Title: "OK",
BlockHash: &tx.BlockHash,
BlockHeight: &tx.BlockHeight,
TxStatus: (api.TransactionResponseTxStatus)(tx.Status),
ExtraInfo: &tx.ExtraInfo,
CompetingTxs: &tx.CompetingTxs,
Timestamp: m.now(),
Txid: txIDs[0],
MerklePath: &tx.MerklePath,
}}
// now check if we need to skip the processing of the transaction
callbackAlreadyExists := false
for _, cb := range tx.Callbacks {
if cb.CallbackUrl == transactionOptions.CallbackURL {
callbackAlreadyExists = true
}
}

// if LastSubmitted doesn't need to be updated and we already have provided callbacks - skip everything and return current status
if time.Since(tx.LastSubmitted.AsTime()) < m.mapExpiryTime && callbackAlreadyExists {
return PostResponse{int(api.StatusOK), &api.TransactionResponse{
Status: int(api.StatusOK),
Title: "OK",
BlockHash: &tx.BlockHash,
BlockHeight: &tx.BlockHeight,
TxStatus: (api.TransactionResponseTxStatus)(tx.Status),
ExtraInfo: &tx.ExtraInfo,
CompetingTxs: &tx.CompetingTxs,
Timestamp: m.now(),
Txid: txIDs[0],
MerklePath: &tx.MerklePath,
}}
}
}
}

Expand Down Expand Up @@ -399,60 +401,62 @@ func (m ArcDefaultHandler) postTransactions(ctx echo.Context, params api.POSTTra
return PostResponse{e.Status, e}
}

// check if we already have the transactions in db (so no need to validate)
txStatuses, err := m.getTransactionStatuses(reqCtx, txIDs)
allTransactionsProcessed := false
if err != nil {
// if we have error which is NOT ErrTransactionNotFound, return err
if !errors.Is(err, metamorph.ErrTransactionNotFound) {
e := api.NewErrorFields(api.ErrStatusGeneric, err.Error())
return PostResponse{e.Status, e}
}
} else if len(txStatuses) == len(txIDs) {
// if we have found all the transactions, skip the validation
transactionOptions.SkipTxValidation = true

// now check if we need to skip the processing of the transaction
allProcessed := true
for _, tx := range txStatuses {
exists := false
for _, cb := range tx.Callbacks {
if cb.CallbackUrl == transactionOptions.CallbackURL {
exists = true
if !transactionOptions.ForceValidation {
// check if we already have the transactions in db (so no need to validate)
txStatuses, err := m.getTransactionStatuses(reqCtx, txIDs)
allTransactionsProcessed := false
if err != nil {
// if we have error which is NOT ErrTransactionNotFound, return err
if !errors.Is(err, metamorph.ErrTransactionNotFound) {
e := api.NewErrorFields(api.ErrStatusGeneric, err.Error())
return PostResponse{e.Status, e}
}
} else if len(txStatuses) == len(txIDs) {
// if we have found all the transactions, skip the validation
transactionOptions.SkipTxValidation = true

// now check if we need to skip the processing of the transaction
allProcessed := true
for _, tx := range txStatuses {
exists := false
for _, cb := range tx.Callbacks {
if cb.CallbackUrl == transactionOptions.CallbackURL {
exists = true
break
}
}
if time.Since(tx.LastSubmitted.AsTime()) > m.mapExpiryTime || !exists {
allProcessed = false
break
}
}
if time.Since(tx.LastSubmitted.AsTime()) > m.mapExpiryTime || !exists {
allProcessed = false
break
allTransactionsProcessed = allProcessed
}

// if nothing to update return
var successes []*api.TransactionResponse
if allTransactionsProcessed {
for _, tx := range txStatuses {
successes = append(successes, &api.TransactionResponse{
Status: int(api.StatusOK),
Title: "OK",
BlockHash: &tx.BlockHash,
BlockHeight: &tx.BlockHeight,
TxStatus: (api.TransactionResponseTxStatus)(tx.Status),
ExtraInfo: &tx.ExtraInfo,
CompetingTxs: &tx.CompetingTxs,
Timestamp: m.now(),
Txid: tx.TxID,
MerklePath: &tx.MerklePath,
})
}
// merge success and fail results
responses := make([]any, 0, len(successes))
for _, o := range successes {
responses = append(responses, o)
}
return PostResponse{int(api.StatusOK), responses}
}
allTransactionsProcessed = allProcessed
}

// if nothing to update return
var successes []*api.TransactionResponse
if allTransactionsProcessed {
for _, tx := range txStatuses {
successes = append(successes, &api.TransactionResponse{
Status: int(api.StatusOK),
Title: "OK",
BlockHash: &tx.BlockHash,
BlockHeight: &tx.BlockHeight,
TxStatus: (api.TransactionResponseTxStatus)(tx.Status),
ExtraInfo: &tx.ExtraInfo,
CompetingTxs: &tx.CompetingTxs,
Timestamp: m.now(),
Txid: tx.TxID,
MerklePath: &tx.MerklePath,
})
}
// merge success and fail results
responses := make([]any, 0, len(successes))
for _, o := range successes {
responses = append(responses, o)
}
return PostResponse{int(api.StatusOK), responses}
}

successes, fails, e := m.processTransactions(reqCtx, txsHex, transactionOptions)
Expand Down Expand Up @@ -586,6 +590,11 @@ func getTransactionsOptions(params api.POSTTransactionsParams, rejectedCallbackU
transactionOptions.FullStatusUpdates = *params.XFullStatusUpdates
}

if params.XForceValidation != nil && *params.XForceValidation {
transactionOptions.SkipTxValidation = false
transactionOptions.ForceValidation = true
}

return transactionOptions, nil
}

Expand Down
1 change: 1 addition & 0 deletions internal/metamorph/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ type TransactionOptions struct {
SkipFeeValidation bool `json:"X-SkipFeeValidation,omitempty"`
SkipScriptValidation bool `json:"X-SkipScriptValidation,omitempty"`
SkipTxValidation bool `json:"X-SkipTxValidation,omitempty"`
ForceValidation bool `json:"X-ForceValidation,omitempty"`
CumulativeFeeValidation bool `json:"X-CumulativeFeeValidation,omitempty"`
WaitForStatus metamorph_api.Status `json:"wait_for_status,omitempty"`
FullStatusUpdates bool `json:"full_status_updates,omitempty"`
Expand Down
Loading

0 comments on commit 657dd57

Please sign in to comment.