Skip to content

Commit

Permalink
Merge pull request #820 from oasisprotocol/ptrus/feature/consensus-ac…
Browse files Browse the repository at this point in the history
…counts-status

runtime: Handle status of consensus-accounts txs
  • Loading branch information
ptrus authored Dec 15, 2024
2 parents 31095c5 + c902d91 commit 1c85d9f
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 5 deletions.
16 changes: 16 additions & 0 deletions .changelog/820.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Improve Status Reporting for Consensus-Accounts Transactions

The handling of multi-step runtime transactions (`consensus.Deposit`,
`consensus.Withdraw`, `consensus.Delegate`, and `consensus.Undelegate`)
has been improved. Previously, these transactions were marked as successful
once they were included in a block, even though the second step (executed in
the next runtime block) could fail. This could result in misleading statuses
for users.

To address this:

- These transactions will now be reported without a status until the second
step is completed

- Once the relevant event is received, the transaction status will be updated
to either successful or failed
112 changes: 108 additions & 4 deletions analyzer/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,10 +515,10 @@ var (
SELECT epochs.id, epochs.start_height, prev_epoch.validators
FROM chain.epochs as epochs
LEFT JOIN history.validators as history
ON epochs.id = history.epoch
ON epochs.id = history.epoch
LEFT JOIN chain.epochs as prev_epoch
ON epochs.id = prev_epoch.id + 1
WHERE
WHERE
history.epoch IS NULL AND
epochs.id >= $1
ORDER BY epochs.id
Expand All @@ -531,7 +531,7 @@ var (
ValidatorStakingRewardUpdate = `
UPDATE history.validators
SET staking_rewards = $3
WHERE
WHERE
id = $1 AND
epoch = $2`

Expand All @@ -545,7 +545,7 @@ var (
FROM chain.epochs AS epochs
LEFT JOIN history.validators AS history
ON epochs.id = history.epoch
WHERE
WHERE
history.epoch IS NULL AND
epochs.id >= $1`

Expand Down Expand Up @@ -1147,4 +1147,108 @@ var (
WHERE
(evs.abi_parsed_at IS NULL OR evs.abi_parsed_at < abi_contracts.verification_info_downloaded_at)
LIMIT $2`

RuntimeConsensusAccountTransactionStatusUpdate = `
-- First, find the round in which the transaction was submitted.
-- This should be the first previous successful round. We currently don't
-- have the runtime-block status field in the DB, so we find the previous
-- successful round by taking the first round which has at least one transaction.
WITH tx_round AS (
SELECT
MAX(rt.round) AS round
FROM chain.runtime_transactions AS rt
WHERE
rt.runtime = $1 AND
rt.round < $2
),
matched_tx AS (
SELECT
rt.runtime,
rt.round,
rt.tx_index
FROM chain.runtime_transactions AS rt
JOIN chain.runtime_transaction_signers AS rts ON
rt.runtime = rts.runtime AND
rt.round = rts.round AND
rt.tx_index = rts.tx_index
WHERE
rt.runtime = $1 AND
rt.round = (SELECT round FROM tx_round) AND
rt.method = $3 AND
rts.signer_address = $4 AND
rts.signer_index = 0 AND
rts.nonce = $5
LIMIT 1
)
UPDATE chain.runtime_transactions rt
SET
success = $6,
error_module = $7,
error_code = $8,
error_message = $9
FROM matched_tx
WHERE
rt.runtime = matched_tx.runtime AND
rt.round = matched_tx.round AND
rt.tx_index = matched_tx.tx_index`

RuntimeConsensusAccountTransactionStatusUpdateFastSync = `
INSERT INTO todo_updates.transaction_status_updates (runtime, round, method, sender, nonce, success, error_module, error_code, error_message)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`

RuntimeTransactionStatusUpdatesRecompute = `
-- Find the previous successful round for each record in 'todo_updates.transaction_status_updates'.
WITH transaction_status_updates_round AS (
SELECT
tsu.runtime,
tsu.round,
tsu.method,
tsu.sender,
tsu.nonce,
tsu.success,
tsu.error_module,
tsu.error_code,
tsu.error_message,
(
SELECT MAX(rt.round)
FROM chain.runtime_transactions AS rt
WHERE rt.runtime = tsu.runtime
AND rt.round < tsu.round
) AS prev_round
FROM todo_updates.transaction_status_updates AS tsu
),
matched_txs AS (
SELECT
rt.runtime,
rt.round,
rt.tx_index,
tsu.success,
tsu.error_module,
tsu.error_code,
tsu.error_message
FROM transaction_status_updates_round AS tsu
JOIN chain.runtime_transactions AS rt
ON rt.runtime = tsu.runtime AND
rt.method = tsu.method AND
rt.round = tsu.prev_round
JOIN chain.runtime_transaction_signers AS rts
ON rt.runtime = rts.runtime AND
rt.round = rts.round AND
rt.tx_index = rts.tx_index
WHERE
rt.runtime = $1 AND
rts.signer_address = tsu.sender AND
rts.nonce = tsu.nonce
)
UPDATE chain.runtime_transactions rt
SET
success = matched_txs.success,
error_module = matched_txs.error_module,
error_code = matched_txs.error_code,
error_message = matched_txs.error_message
FROM matched_txs
WHERE
rt.runtime = matched_txs.runtime
AND rt.round = matched_txs.round
AND rt.tx_index = matched_txs.tx_index`
)
78 changes: 78 additions & 0 deletions analyzer/runtime/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import (
"math/big"

"github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/accounts"
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/consensusaccounts"
sdkTypes "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types"
"golang.org/x/exp/slices"

"github.com/oasisprotocol/nexus/analyzer"
"github.com/oasisprotocol/nexus/analyzer/queries"
"github.com/oasisprotocol/nexus/common"
"github.com/oasisprotocol/nexus/storage"
)

Expand Down Expand Up @@ -146,3 +148,79 @@ func (m *processor) queueTransfer(batch *storage.QueryBatch, round uint64, e acc
)
}
}

func (m *processor) queueConsensusAccountsEvents(batch *storage.QueryBatch, blockData *BlockData) {
for _, event := range blockData.EventData {
if event.WithScope.ConsensusAccounts == nil {
continue
}
if e := event.WithScope.ConsensusAccounts.Deposit; e != nil {
m.queueTransactionStatusUpdate(batch, blockData.Header.Round, "consensus.Deposit", e.From, e.Nonce, e.Error)
}
if e := event.WithScope.ConsensusAccounts.Withdraw; e != nil {
m.queueTransactionStatusUpdate(batch, blockData.Header.Round, "consensus.Withdraw", e.From, e.Nonce, e.Error)
}
if e := event.WithScope.ConsensusAccounts.Delegate; e != nil {
m.queueTransactionStatusUpdate(batch, blockData.Header.Round, "consensus.Delegate", e.From, e.Nonce, e.Error)
}
if e := event.WithScope.ConsensusAccounts.UndelegateStart; e != nil {
m.queueTransactionStatusUpdate(batch, blockData.Header.Round, "consensus.Undelegate", e.From, e.Nonce, e.Error)
}
// Nothing to do for 'UndelegateEnd'.
}
}

// Updates the status of a transaction based on the event result.
// These transactions are special in the way that the actual action is executed in the next round.
func (m *processor) queueTransactionStatusUpdate(
batch *storage.QueryBatch,
round uint64,
methodName string,
sender sdkTypes.Address,
nonce uint64,
e *consensusaccounts.ConsensusError,
) {
// We can only do this in slow-sync, because the event affects a transaction in previous round.
// For fast-sync, this is handled in the FinalizeFastSync step.
var errorModule *string
var errorCode *uint32
var errorMessage *string
success := true
if e != nil {
errorModule = &e.Module
errorCode = &e.Code
// The event doesn't contain the error message, so construct a human readable one here.
// TODO: We could try loading the error message, but Nexus currently doesn't have a mapping
// from error codes to error messages. This can also be done on the frontend.
errorMessage = common.Ptr("Consensus error: " + e.Module)
success = false
}
switch m.mode {
case analyzer.FastSyncMode:
batch.Queue(
queries.RuntimeConsensusAccountTransactionStatusUpdateFastSync,
m.runtime,
round,
methodName,
sender,
nonce,
success,
errorModule,
errorCode,
errorMessage,
)
case analyzer.SlowSyncMode:
batch.Queue(
queries.RuntimeConsensusAccountTransactionStatusUpdate,
m.runtime,
round,
methodName,
sender,
nonce,
success,
errorModule,
errorCode,
errorMessage,
)
}
}
12 changes: 12 additions & 0 deletions analyzer/runtime/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,9 @@ func ExtractRound(blockHeader nodeapi.RuntimeBlockHeader, txrs []nodeapi.Runtime
// Ref: https://github.com/oasisprotocol/oasis-sdk/blob/runtime-sdk/v0.8.4/runtime-sdk/src/modules/consensus_accounts/mod.rs#L418
to = blockTransactionData.SignerData[0].Address
}
// Set the 'Success' field to 'Pending' for deposits. This is because the outcome of the Deposit tx is only known in the next block.
blockTransactionData.Success = nil

return nil
},
ConsensusAccountsWithdraw: func(body *consensusaccounts.Withdraw) error {
Expand All @@ -359,6 +362,9 @@ func ExtractRound(blockHeader nodeapi.RuntimeBlockHeader, txrs []nodeapi.Runtime
to = blockTransactionData.SignerData[0].Address
}
blockTransactionData.RelatedAccountAddresses[to] = struct{}{}
// Set the 'Success' field to 'Pending' for withdrawals. This is because the outcome of the Withdraw tx is only known in the next block.
blockTransactionData.Success = nil

return nil
},
ConsensusAccountsDelegate: func(body *consensusaccounts.Delegate) error {
Expand Down Expand Up @@ -392,6 +398,8 @@ func ExtractRound(blockHeader nodeapi.RuntimeBlockHeader, txrs []nodeapi.Runtime
return fmt.Errorf("to: %w", err)
}
blockTransactionData.RelatedAccountAddresses[to] = struct{}{}
// Set the 'Success' field to 'Pending' for delegations. This is because the outcome of the Delegate tx is only known in the next block.
blockTransactionData.Success = nil
return nil
},
ConsensusAccountsUndelegate: func(body *consensusaccounts.Undelegate) error {
Expand All @@ -408,6 +416,9 @@ func ExtractRound(blockHeader nodeapi.RuntimeBlockHeader, txrs []nodeapi.Runtime
// to convert `shares` to `amount` until the undelegation actually happens (= UndelegateDone event); in the meantime,
// the validator's token pool might change, e.g. because of slashing.
// Do not store `body.Shares` in DB's `amount` to avoid confusion. Clients can still look up the shares in the tx body if they really need it.

// Set the 'Success' field to 'Pending' for undelegations. This is because the outcome of the Undelegate tx is only known in the next block.
blockTransactionData.Success = nil
return nil
},
EVMCreate: func(body *sdkEVM.Create, ok *[]byte) error {
Expand Down Expand Up @@ -583,6 +594,7 @@ func ExtractRound(blockHeader nodeapi.RuntimeBlockHeader, txrs []nodeapi.Runtime
blockData.GasUsed += txGasUsed
blockData.Size += blockTransactionData.Size
}

return &blockData, nil
}

Expand Down
5 changes: 5 additions & 0 deletions analyzer/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ func (m *processor) FinalizeFastSync(ctx context.Context, lastFastSyncHeight int
batch.Queue(queries.RuntimeEVMTokenRecompute, m.runtime)
batch.Queue("DELETE FROM todo_updates.evm_tokens WHERE runtime = $1", m.runtime)

m.logger.Info("recomputing transaction statuses from transaction status updates")
batch.Queue(queries.RuntimeTransactionStatusUpdatesRecompute, m.runtime)
batch.Queue("DELETE from todo_updates.transaction_status_updates WHERE runtime = $1", m.runtime)

if err := m.target.SendBatch(ctx, batch); err != nil {
return err
}
Expand Down Expand Up @@ -213,6 +217,7 @@ func (m *processor) ProcessBlock(ctx context.Context, round uint64) error {
batch := &storage.QueryBatch{}
m.queueDbUpdates(batch, blockData)
m.queueAccountsEvents(batch, blockData)
m.queueConsensusAccountsEvents(batch, blockData)
analysisTimer.ObserveDuration()

// Update indexing progress.
Expand Down
2 changes: 1 addition & 1 deletion storage/migrations/01_runtimes.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ CREATE TABLE chain.runtime_transactions
oasis_encrypted_result_data BYTEA,

-- Error information.
success BOOLEAN, -- NULL means success is unknown (can happen in confidential runtimes)
success BOOLEAN, -- NULL means success is unknown (can happen in confidential runtimes, or for 'consensusaccounts' transactions which whose action is known only in the next round)
error_module TEXT,
error_code UINT63,
error_message TEXT,
Expand Down
13 changes: 13 additions & 0 deletions storage/migrations/04_fast_sync_temp_tables.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,18 @@ CREATE TABLE todo_updates.evm_tokens( -- Tracks updates to chain.evm_tokens(last
num_transfers UINT63 NOT NULL DEFAULT 0,
last_mutate_round UINT63 NOT NULL
);
-- Added in 09_fast_sync_temp_transaction_status_updates.up.sql.
-- CREATE TABLE todo_updates.transaction_status_updates( -- Tracks transaction status updates for consensus-accounts transactions.
-- runtime runtime NOT NULL,
-- round UINT63 NOT NULL,
-- method TEXT NOT NULL,
-- sender oasis_addr NOT NULL,
-- nonce UINT63 NOT NULL,

-- success BOOLEAN NOT NULL,
-- error_module TEXT,
-- error_code UINT63,
-- error_message TEXT
-- );

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
BEGIN;

CREATE TABLE todo_updates.transaction_status_updates( -- Tracks transaction status updates for consensus-accounts transactions.
runtime runtime NOT NULL,
round UINT63 NOT NULL,
method TEXT NOT NULL,
sender oasis_addr NOT NULL,
nonce UINT63 NOT NULL,

success BOOLEAN NOT NULL,
error_module TEXT,
error_code UINT63,
error_message TEXT
);

COMMIT;

0 comments on commit 1c85d9f

Please sign in to comment.