At the beginning of each workflow, a Transaction
has to be parsed and validated.
As these steps are the same for all workflows, this functionality has been extracted and made available independently.
All related classes can be found in the package com.hedera.node.app.workflows.onset
.
Details about the required pre-checks can be found here.
The package com.hedera.node.app.workflows.ingest
contains the ingest workflow. A rough overview can be seen in the diagram below.
When a new message arrives at the HAPI-endpoint, the byte-buffer that contains the transaction is sent to the ingest workflow.
The gRPC-server is responsible for Thread
-Management.
The ingest-workflow is single-threaded, but multiple calls can run in parallel.
The ingest workflow consists of the following steps:
- Check node The node is checked to ensure it is not in a state that prevents it from processing transactions.
- Parse Transaction Parse and check the transaction if it is valid and is not larger than allowed size.It also checks if there are unknown fields set in the transaction.
- Check Syntax The transaction arrives as a byte-array. The structure and syntax are validated.
- Validate Submitted Node Checks if the transaction is submitted to this node.
- Check timeBox Checks whether the transaction duration is valid as per the configuration for valid durations for the network, and whether the current node wall-clock time falls between the transaction start and the transaction end (transaction start + duration)
- Deduplicate The transaction is checked to ensure it has not been processed before.
- Check Throttles Throttling must be observed and checked as early as possible.
- Pure Checks Calls
pureChecks
method that does validations based on the respective handler's transaction body The checks performed here are independent of the state and configuration. This check will be removed in the future from IngestWorkflow. It is important to note thatpureChecks
andTransaction Prechecks
found here are different. - Get payer account. The account data of the payer is read from the latest immutable state.
- Verify payer's signature. The signature of the payer is checked. (Please note: other signatures are not checked here, but in later stages)
- Estimate fee Compute the fee that is required to pay for the transaction.
- Payer Solvency* Check the account balance of the payer to ensure it is able to pay the fee.
- Submit to platform The transaction is submitted to the platform for further processing.
- TransactionResponse Return
TransactionResponse
with result-code.
If all checks have been successful, the transaction has been submitted to the platform and the precheck-code of the returned TransactionResponse
is OK
.
Otherwise, the transaction is rejected with an appropriate response code.
In case of insufficient funds, the returned TransactionResponse
also contains an estimation of the required fee.
The com.hedera.node.app.workflows.prehandle
package contains the workflow for pre-handling transactions. A rough overview can be seen in the diagram below.
An Event
at a time is sent to the prehandle
with a reference to the latest immutable state.
It iterates through each transaction and initiates the pre-handle workflow in a separate thread.
The workflow consists of the following steps:
- Parse Transaction. The transaction arrives as a byte-array. The required parts are parsed and the common information is validated.
- Call PreTransactionHandler. Depending on the type of transaction, a specific
PreTransactionHandler
is called. It validates the transaction-specific parts and pre-loads data into the cache. It also creates aTransactionMetadata
and sets the required keys. - PureChecks
pureChecks
method does validations based on the respective handler's transaction body. The checks performed are that are independent of the state and configuration. - Prepare Signature-Data. The data for all signatures is loaded into memory. A signature consists of three parts:
- Some bytes that are signed; in our case, either the
bodyBytes
for an Ed25519 signature or the Keccak256 hash of thebodyBytes
for an ECDSA(secp256k1) signature. - An Ed25519 or secp256k1 public key that is supposed to have signed these bytes (these public keys come from e.g. the Hedera key of some
0.0.X
account). - The signature itself---which comes from the
SignatureMap
, based on existence of a uniqueSignaturePair
entry whosepubKeyPrefix
matches the public key in (ii.).
- Some bytes that are signed; in our case, either the
- Verify Signatures. The information prepared in the previous step is sent to the platform to validate the signatures.
- Transaction Metadata. The
TransactionMetadata
generated by thePreTransactionHandler
is attached to theSwirldsTransaction
.
If all checks have been successful, the status of the created TransactionMetadata
will be OK
.
Otherwise, the status is set to the response code providing the failure reason.
If the workflow terminates early (either because the parsing step (1.) fails or an unexpected Exception
occurs) an ErrorTransactionMetadata
is attached to the SwirldsTransaction
that contains the causing Exception
.
The query workflow is quite complex.
Unlike transaction processing, it is not split into several phases, but covers the whole query from receiving the request until sending the response.
Also, a query typically contains a CryptoTransfer
to cover the costs.
When a query arrives at the HAPI-endpoint, the byte-buffer that contains the query is sent to the query workflow.
The gRPC-server is responsible for Thread
-management.
The query-workflow is single-threaded, but multiple calls can run in parallel.
The query workflow consists of the following steps:
- Parse and check header. The query arrives as a byte-array. The required parts are parsed and the type of the query (header) is extracted and validated.
- Check node. The node is checked to ensure it is not in a state that prevents it from processing transactions.
- Check query throttles. Throttling must be observed and checked as early as possible.
- If a payment is required:
- Ingest checks. Run all checks of the ingest workflow on the
CryptoTransfer
. - Validate the CryptoTransfer. The contained
CryptoTransfer
is validated. - Check permissions. It is checked, if the requester is actually allowed to do the query.
- Calculate costs. The costs of the query are calculated.
- Check account balances. The accounts of all payers are checked.
- Ingest checks. Run all checks of the ingest workflow on the
- Check validity. The query is checked semantically.
- Submit payment to platform. If a payment is required, the contained
CryptoTransfer
is submitted to the platform. - The last step depends on what was requested
- Estimate costs. If only the costs are requested, they are estimated. (Requests for costs are free, therefore the costs have not been calculated in step 3.)
- Find response. If the result of the query is expected (and not only the costs), the actual query is performed.
Depending on what was requested, either the result of the query or the expected costs are returned to the caller. If at anytime an error occurs, a response with the error code is returned.
When a platform transaction reaches consensus and needs to be handled, the HandleWorkflow.handlePlatformTransaction()
is called.
Few terms that will be used in the following sections:
- Dispatch: The context needed for executing business logic of a service. All the transactions are handled by creating a Dispatch and executing business logic. This has two implementations
- user transactions scope called
UserTxnDispatch
- child transactions scope called
ChildDispatch
All the objects used while handling the transaction belong to one of the following Dagger scopes.
- Singleton - Objects that are created once and used for the entire lifecycle of the application.
Examples include the
NodeInfo
andWorkingStateAccessor
. - UserTxnScope - Objects that are created once for platform transaction.
Examples include the
Configuration
,RecordListBuilder
andTokenContext
. Dagger provides all the objects that can be constructed in this scope here and UserTxnComponent takes all the inputs that are needed to execute the user transaction. - UserDispatchScope - Objects that are created once for each user transaction that will be dispatched.
Examples include the
SingleTransactionRecordBuilder
for user transaction andFeeContext
. Dagger provides all the objects that can be constructed in this scope in UserDispatchModule andUserDispatchComponent
. and UserDispatchComponent takes all the inputs that are needed to create the user dispatch. - ChildDispatchScope - Objects that are created once for each child transaction dispatch.
Examples include the
ReadableStoreFactory
andChildFeeContext
. Dagger provides all the objects that can be constructed in the ChildDispatchModule and ChildDispatchComponent takes all the inputs that are needed to create the child dispatch.
The HandleWorkflow
class is responsible for handling the platform transaction and providing the record stream.
The overall high level steps are as follows:
- Calls
BlockRecordManager
to update the new consensus time for the user transaction, puts the lastBlockInfo in state if needed. UserTxnWorkflow
is called to handle the transaction and provide record stream- Externalizes the record stream items
- Update metrics for the handled user transaction
- SkipHandleWorkflow: If the transaction is from older software, the transaction not be handled.
A record with
BUSY
status is added to record cache by callingSkipHandleWorkflow
. - DefaultHandleWorkflow: If the transaction is from a valid software version, we call
DefaultHandleWorkflow
. This workflow handles valid user transaction. It has the following steps:- Exports synthetic records of system accounts creation that need to be externalized on genesis start
- Process staking period hook that is responsible for staking updates and staking rewards distribution
- Advances consensus clock by updating the last consensus time that node has handled
- Expire schedules if any
- Creates a
Dispatch
for the user transaction - Finalizes hollow accounts
- Gives the created dispatch to the
DispatchProcessor
to process it. TheDispatchProcessor
will execute the business logic for the dispatch. This is common logic that is executed for all user and child transactions, since the user dispatch child dispatches are treated the same way to avoid duplicating any logic.
The DispatchProcessor.processDispatch
will be called for user and child dispatches. This avoids duplicating
any logic between user and child transactions, since both are treated as dispatches.
For the child transactions, when a service calls one of the dispatchXXXTransaction
methods in DispatchHandleContext
, a new child dispatch is created and DispatchProcessor.processDispatch
is called.
- Error Validation: Checks if there is any error by node or user by re-assessing preHandleResult. It validates the following:
- Checks the preHandleStatus is
NODE_DUE_DILIGENCE_FAILURE
. If so, creates an error report with node error. So, node pays the fees and returns. - Verifies payer signature. If it is invalid or missing, creates an error report with node error where node pays the fee and returns.
- Checks if the transaction is duplicate. If the transaction is duplicate on same node, creates error report with node error where node pays the fee. If it is duplicate from other node, creates an error report with payer error where payer pays the fee and returns.
- Checks the preHandleStatus is
- If there is node error report in 1, charges the node. Steps 3-5 are skipped.
- Else, charges the payer for the transaction.
- Handling Transaction: If it has not already failed in steps 2 and 3 the transaction is handled.
The following steps are executed:
- Checks if there is capacity to handle the transaction. This is only used for contract operations.
If the previous transaction is gas throttled, this throws a
ThrottleException
and next steps under 4 are skipped. - Dispatches the transaction to the appropriate handler with appropriate handleContext constructed based on the transaction scope. The handler will execute the business logic of the transaction.
- Updates the record to SUCCESS status
- If it is a user transaction, notifies if system file is updated and platform state is modified to the appropriate facilities
- Checks if there is capacity to handle the transaction. This is only used for contract operations.
If the previous transaction is gas throttled, this throws a
- Exception Handling: When any exception is thrown in the steps under 4, the following steps are executed:
- If any HandleException is thrown, rolls back the complete stack and charges the payer. The payer is charged again because when stack is rolled back the previous charges to the payer are rolled back as well.
- If there is a
ThrottleException
thrown, stack is rolled back and payer is charged without serviceFee. - If there is any unhandled exception, stack is rolled back and payer is charged. The record is set to
FAIL_INVALID
status.
- Track Utilization: Tracks network utilization for the transaction handled
- Record Finalization: Finalizes the record from state changes
- Commit: Commits all the changes to the state
- Metrics Update: Updates any metrics that need to be updated
- Record Stream: Adds all the records to the record cache