Repository: yeagerai/genlayer-studio
Pull request #1001: feat: send consensus steps transactions from the validators accounts
Author: login: kstroobants
Labels: name: run-tests
name: released
Created: 2025-03-14T19:10:00Z | Merged: 2025-04-03T14:58:34Z

<!-- This is a TEMPLATE, modify it to fit your needs. -->

Fixes #992

# What

<!-- Describe the changes you made. -->

- Introduced new external functions in the `ConsensusMain` contract to emit events related to consensus steps.
- Removed the `create_rollup_transaction` method, indicating a shift towards using the `consensus_service`.
- Added function calling to create emits from `consensus/base.py`.

# Why

<!-- Why are you making these changes? This should be related to the issue created, and the value we are adding with this PR -->

- To enhance the consensus mechanism by integrating a service for managing transaction events.
- To facilitate real-time event handling in the blockchain context.

# Testing done

<!-- Describe the tests you ran to verify your changes. -->

- Tested the new feature of sending consensus step transactions with UI and listening to events.
- TODO: see the events in the genlayer-explorer.

# Decisions made

<!-- Describe any decisions made during the implementation of this PR. This should in general be in the code, but sometimes they are more related to the issue. For example: decisions on PR workflow -->

- Decided to remove the `create_rollup_transaction` method.

# Checks

- [x] I have tested this code
- [x] I have reviewed my own PR
- [x] I have created an issue for this PR
- [x] I have set a descriptive PR title compliant with [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)

# Reviewing tips

<!-- What can you tell the reviewer to make the review easier? -->

- Focus on the integration of `consensus_service` and the new event listener functionality.
- Review the code changes.

# Depends on
It contains code from PR #994 

# User facing release notes

<!-- What should the user know about this change? Think of it going into public forums for end users to read -->

- Introduced a new feature to send consensus step transactions from validator accounts, enhancing the consensus process.

Linked issue #992: feat: send consensus steps transactions from the validators accounts
Every consensus step in the transaction consensus execution flow has one or more validator interactions. 
Those interactions are represented by transactions sent from the validator to the rollup ConsensusMain smart contract by calling its methods:
- Activate Transaction: https://github.com/yeagerai/genlayer-consensus/blob/main/contracts/v2_contracts/ConsensusMain.sol#L167
- Propose Receipt: https://github.com/yeagerai/genlayer-consensus/blob/main/contracts/v2_contracts/ConsensusMain.sol#L167
- Commit: https://github.com/yeagerai/genlayer-consensus/blob/main/contracts/v2_contracts/ConsensusMain.sol#L167
- Reveal: https://github.com/yeagerai/genlayer-consensus/blob/main/contracts/v2_contracts/ConsensusMain.sol#L265
... and so on.

Every time one of these transactions needs to be sent to the ConsensusMain contract from a validator, we need to get the validator's private key from its record in the database and assemble + sign a transaction with it.
The goal is that the Events are fired in the ConsensusMain smart contract so the indexer picks them and shows all the transaction consensus steps.
Linked issue comments: user: login: github-actions[bot]
created_at: 2025-04-07T08:03:48Z
body: :tada: This issue has been resolved in version 0.48.0 :tada:

The release is available on [GitHub release](https://github.com/yeagerai/genlayer-studio/releases/tag/v0.48.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: CONTRIBUTOR

Diff:
diff --git a/backend/consensus/base.py b/backend/consensus/base.py
index 2626963f9..e1fc19999 100644
--- a/backend/consensus/base.py
+++ b/backend/consensus/base.py
@@ -44,6 +44,7 @@
     EventType,
     EventScope,
 )
+from backend.rollup.consensus_service import ConsensusService
 
 
 def node_factory(
@@ -76,6 +77,7 @@ def node_factory(
         msg_handler=msg_handler,
         validator=Validator(
             address=validator["address"],
+            private_key=validator["private_key"],
             stake=validator["stake"],
             llmprovider=LLMProvider(
                 provider=validator["provider"],
@@ -192,6 +194,7 @@ class TransactionContext:
         votes (dict): Dictionary of votes.
         validator_nodes (list): List of validator nodes.
         validation_results (list): List of validation results.
+        consensus_service (ConsensusService): Consensus service to interact with the rollup.
     """
 
     def __init__(
@@ -214,6 +217,7 @@ def __init__(
             Node,
         ],
         msg_handler: MessageHandler,
+        consensus_service: ConsensusService,
     ):
         """
         Initialize the TransactionContext.
@@ -226,6 +230,7 @@ def __init__(
             contract_snapshot_factory (Callable[[str], ContractSnapshot]): Factory function to create contract snapshots.
             node_factory (Callable[[dict, ExecutionMode, ContractSnapshot, Receipt | None, MessageHandler, Callable[[str], ContractSnapshot]], Node]): Factory function to create nodes.
             msg_handler (MessageHandler): Handler for messaging.
+            consensus_service (ConsensusService): Consensus service to interact with the rollup.
         """
         self.transaction = transaction
         self.transactions_processor = transactions_processor
@@ -246,6 +251,7 @@ def __init__(
         self.validator_nodes: list = []
         self.validation_results: list = []
         self.rotation_count: int = 0
+        self.consensus_service = consensus_service
 
 
 class ConsensusAlgorithm:
@@ -255,6 +261,7 @@ class ConsensusAlgorithm:
     Attributes:
         get_session (Callable[[], Session]): Function to get a database session.
         msg_handler (MessageHandler): Handler for messaging.
+        consensus_service (ConsensusService): Consensus service to interact with the rollup.
         pending_queues (dict[str, asyncio.Queue]): Dictionary of pending_queues for transactions.
         finality_window_time (int): Time in seconds for the finality window.
         consensus_sleep_time (int): Time in seconds for the consensus sleep time.
@@ -264,6 +271,7 @@ def __init__(
         self,
         get_session: Callable[[], Session],
         msg_handler: MessageHandler,
+        consensus_service: ConsensusService,
     ):
         """
         Initialize the ConsensusAlgorithm.
@@ -271,9 +279,11 @@ def __init__(
         Args:
             get_session (Callable[[], Session]): Function to get a database session.
             msg_handler (MessageHandler): Handler for messaging.
+            consensus_service (ConsensusService): Consensus service to interact with the rollup.
         """
         self.get_session = get_session
         self.msg_handler = msg_handler
+        self.consensus_service = consensus_service
         self.pending_queues: dict[str, asyncio.Queue] = {}
         self.finality_window_time = int(os.getenv("VITE_FINALITY_WINDOW"))
         self.finality_window_appeal_failed_reduction = float(
@@ -570,6 +580,7 @@ async def exec_transaction(
             contract_processor=contract_processor,
             node_factory=node_factory,
             msg_handler=self.msg_handler,
+            consensus_service=self.consensus_service,
         )
 
         # Begin state transitions starting from PendingState
@@ -651,7 +662,6 @@ def execute_transfer(
                     msg_handler,
                 )
 
-                # transactions_processor.create_rollup_transaction(transaction.hash)
                 return
 
             # Update the balance of the sender account
@@ -677,8 +687,6 @@ def execute_transfer(
             msg_handler,
         )
 
-        # transactions_processor.create_rollup_transaction(transaction.hash)
-
     def run_appeal_window_loop(
         self,
         chain_snapshot_factory: Callable[
@@ -984,6 +992,7 @@ async def process_finalization(
             contract_processor=contract_processor,
             node_factory=node_factory,
             msg_handler=self.msg_handler,
+            consensus_service=self.consensus_service,
         )
 
         # Transition to the FinalizingState
@@ -1031,6 +1040,7 @@ async def process_leader_appeal(
             contract_processor=contract_processor,
             node_factory=node_factory,
             msg_handler=self.msg_handler,
+            consensus_service=self.consensus_service,
         )
 
         transactions_processor.set_transaction_appeal(transaction.hash, False)
@@ -1137,6 +1147,7 @@ async def process_validator_appeal(
             contract_processor=contract_processor,
             node_factory=node_factory,
             msg_handler=self.msg_handler,
+            consensus_service=self.consensus_service,
         )
 
         # Set the leader receipt in the context
@@ -1194,6 +1205,16 @@ async def process_validator_appeal(
                 )
             )
 
+            # Send events in rollup to communicate the appeal is started
+            context.consensus_service.emit_transaction_event(
+                "emitAppealStarted",
+                context.remaining_validators[0],
+                context.transaction.hash,
+                context.remaining_validators[0]["address"],
+                0,
+                [v["address"] for v in context.remaining_validators],
+            )
+
             # Begin state transitions starting from CommittingState
             state = CommittingState()
             while True:
@@ -1210,9 +1231,12 @@ async def process_validator_appeal(
                     )
 
                     # Get the previous state of the contract
-                    previous_contact_state = (
-                        context.transaction.contract_snapshot.encoded_state
-                    )
+                    if context.transaction.contract_snapshot:
+                        previous_contact_state = (
+                            context.transaction.contract_snapshot.encoded_state
+                        )
+                    else:
+                        previous_contact_state = {}
 
                     # Restore the contract state
                     context.contract_processor.update_contract_state(
@@ -1592,6 +1616,16 @@ async def handle(self, context):
             )
             context.involved_validators = current_validators + extra_validators
 
+            # Send events in rollup to communicate the appeal is started
+            context.consensus_service.emit_transaction_event(
+                "emitAppealStarted",
+                context.involved_validators[0],
+                context.transaction.hash,
+                context.involved_validators[0]["address"],
+                0,
+                [v["address"] for v in context.involved_validators],
+            )
+
         else:
             # If there was no validator appeal or leader appeal
             if context.transaction.consensus_data:
@@ -1609,7 +1643,9 @@ async def handle(self, context):
                 )
 
         # Transition to the ProposingState
-        return ProposingState()
+        return ProposingState(
+            activate=False if context.transaction.appeal_undetermined else True
+        )
 
 
 class ProposingState(TransactionState):
@@ -1617,6 +1653,9 @@ class ProposingState(TransactionState):
     Class representing the proposing state of a transaction.
     """
 
+    def __init__(self, activate: bool = False):
+        self.activate = activate
+
     async def handle(self, context):
         """
         Handle the proposing state transition.
@@ -1635,10 +1674,6 @@ async def handle(self, context):
             context.msg_handler,
         )
 
-        # context.transactions_processor.create_rollup_transaction(
-        #     context.transaction.hash
-        # )
-
         # The leader is elected randomly
         random.shuffle(context.involved_validators)
 
@@ -1649,6 +1684,17 @@ async def handle(self, context):
         if context.transaction.leader_only:
             context.remaining_validators = []
 
+        # Send event in rollup to communicate the transaction is activated
+        if self.activate:
+            context.consensus_service.emit_transaction_event(
+                "emitTransactionActivated",
+                leader,
+                context.transaction.hash,
+                leader["address"],
+                [leader["address"]]
+                + [v["address"] for v in context.remaining_validators],
+            )
+
         # Create a contract snapshot for the transaction if not exists
         if context.transaction.contract_snapshot:
             contract_snapshot = deepcopy(context.transaction.contract_snapshot)
@@ -1671,9 +1717,16 @@ async def handle(self, context):
 
         # Execute the transaction and obtain the leader receipt
         leader_receipt = await leader_node.exec_transaction(context.transaction)
-        votes = {leader["address"]: leader_receipt.vote.value}
+
+        # Send event in rollup to communicate the receipt proposed
+        context.consensus_service.emit_transaction_event(
+            "emitTransactionReceiptProposed",
+            leader,
+            context.transaction.hash,
+        )
 
         # Update the consensus data with the leader's vote and receipt
+        votes = {leader["address"]: leader_receipt.vote.value}
         context.consensus_data.votes = votes
         context.consensus_data.leader_receipt = leader_receipt
         context.consensus_data.validators = []
@@ -1712,10 +1765,6 @@ async def handle(self, context):
             context.msg_handler,
         )
 
-        # context.transactions_processor.create_rollup_transaction(
-        #     context.transaction.hash
-        # )
-
         # Create validator nodes for each validator
         context.validator_nodes = [
             context.node_factory(
@@ -1745,6 +1794,23 @@ async def run_single_validator(validator: Node) -> Receipt:
         ]
         context.validation_results = await asyncio.gather(*validation_tasks)
 
+        # Send events in rollup to communicate the votes are committed
+        context.consensus_service.emit_transaction_event(
+            "emitVoteCommitted",
+            context.consensus_data.leader_receipt.node_config,
+            context.transaction.hash,
+            context.consensus_data.leader_receipt.node_config["address"],
+            False,
+        )
+        for i, validator in enumerate(context.remaining_validators):
+            context.consensus_service.emit_transaction_event(
+                "emitVoteCommitted",
+                validator,
+                context.transaction.hash,
+                validator["address"],
+                True if i == len(context.remaining_validators) - 1 else False,
+            )
+
         # Transition to the RevealingState
         return RevealingState()
 
@@ -1772,10 +1838,6 @@ async def handle(self, context):
             context.msg_handler,
         )
 
-        # context.transactions_processor.create_rollup_transaction(
-        #     context.transaction.hash
-        # )
-
         # Process each validation result and update the context
         for i, validation_result in enumerate(context.validation_results):
             # Store the vote from each validator node
@@ -1783,30 +1845,50 @@ async def handle(self, context):
                 validation_result.vote.value
             )
 
-            # Create a dictionary of votes for the current reveal so the rollup transaction contains leader vote and one validator vote (done for each validator)
-            # create_rollup_transaction() is removed but we keep this code for future use
-            single_reveal_votes = {
-                context.consensus_data.leader_receipt.node_config[
-                    "address"
-                ]: context.consensus_data.leader_receipt.vote.value,
-                context.validator_nodes[i].address: validation_result.vote.value,
-            }
-
-            # Update consensus data with the current reveal vote and validator
-            context.consensus_data.votes = single_reveal_votes
-            context.consensus_data.validators = [validation_result]
-
-            # Set the consensus data of the transaction
-            context.transactions_processor.set_transaction_result(
-                context.transaction.hash, context.consensus_data.to_dict()
-            )
-
         # Determine if the majority of validators agree
         majority_agrees = (
             len([vote for vote in context.votes.values() if vote == Vote.AGREE.value])
             > context.num_validators // 2
         )
 
+        # Send event in rollup to communicate the votes are revealed
+        context.consensus_service.emit_transaction_event(
+            "emitVoteRevealed",
+            context.consensus_data.leader_receipt.node_config,
+            context.transaction.hash,
+            context.consensus_data.leader_receipt.node_config["address"],
+            1,
+            False,
+            0,
+        )
+        for i, validation_result in enumerate(context.validation_results):
+            if validation_result.vote == Vote.AGREE:
+                type_vote = 1
+            elif validation_result.vote == Vote.DISAGREE:
+                type_vote = 2
+            else:
+                type_vote = 0
+
+            if i == len(context.validation_results) - 1:
+                last_vote = True
+                if majority_agrees:
+                    result_vote = 6
+                else:
+                    result_vote = 7
+            else:
+                last_vote = False
+                result_vote = 0
+
+            context.consensus_service.emit_transaction_event(
+                "emitVoteRevealed",
+                validation_result.node_config,
+                context.transaction.hash,
+                validation_result.node_config["address"],
+                type_vote,
+                last_vote,
+                result_vote,
+            )
+
         if context.transaction.appealed:
 
             # Update the consensus results with all new votes and validators
@@ -1846,10 +1928,6 @@ async def handle(self, context):
                     context.transaction.hash, context.consensus_data.to_dict()
                 )
 
-                context.transactions_processor.create_rollup_transaction(
-                    context.transaction.hash
-                )
-
                 context.transactions_processor.set_transaction_appeal_failed(
                     context.transaction.hash,
                     0,
@@ -1931,6 +2009,14 @@ async def handle(self, context):
                     )
                 )
 
+                # Send events in rollup to communicate the leader rotation
+                context.consensus_service.emit_transaction_event(
+                    "emitTransactionLeaderRotated",
+                    context.consensus_data.leader_receipt.node_config,
+                    context.transaction.hash,
+                    context.involved_validators[0]["address"],
+                )
+
                 # Update the consensus history
                 if context.transaction.appeal_undetermined:
                     consensus_round = "Leader Rotation Appeal"
@@ -2024,9 +2110,12 @@ async def handle(self, context):
             context.validation_results,
         )
 
-        # context.transactions_processor.create_rollup_transaction(
-        #     context.transaction.hash
-        # )
+        # Send events in rollup to communicate the transaction is accepted
+        context.consensus_service.emit_transaction_event(
+            "emitTransactionAccepted",
+            context.consensus_data.leader_receipt.node_config,
+            context.transaction.hash,
+        )
 
         # Send a message indicating consensus was reached
         context.msg_handler.send_message(
@@ -2193,10 +2282,6 @@ async def handle(self, context):
             context.consensus_data.validators,
         )
 
-        # context.transactions_processor.create_rollup_transaction(
-        #     context.transaction.hash
-        # )
-
         return None
 
 
@@ -2235,9 +2320,12 @@ async def handle(self, context):
             context.msg_handler,
         )
 
-        # context.transactions_processor.create_rollup_transaction(
-        #     context.transaction.hash
-        # )
+        # Send events in rollup to communicate the transaction is finalized
+        context.consensus_service.emit_transaction_event(
+            "emitTransactionFinalized",
+            leader_receipt.node_config,
+            context.transaction.hash,
+        )
 
         if context.transaction.status != TransactionStatus.UNDETERMINED:
             # Insert pending transactions generated by contract-to-contract calls
diff --git a/backend/database_handler/migration/versions/47519c0bf986_validators_private_key.py b/backend/database_handler/migration/versions/47519c0bf986_validators_private_key.py
new file mode 100644
index 000000000..3281af10a
--- /dev/null
+++ b/backend/database_handler/migration/versions/47519c0bf986_validators_private_key.py
@@ -0,0 +1,33 @@
+"""validators_private_key
+
+Revision ID: 47519c0bf986
+Revises: 130836786511
+Create Date: 2025-03-11 18:18:33.157960
+
+"""
+
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = "47519c0bf986"
+down_revision: Union[str, None] = "130836786511"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column(
+        "validators", sa.Column("private_key", sa.String(length=255), nullable=True)
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column("validators", "private_key")
+    # ### end Alembic commands ###
diff --git a/backend/database_handler/models.py b/backend/database_handler/models.py
index 84c22a9b7..3dbaa5e3f 100644
--- a/backend/database_handler/models.py
+++ b/backend/database_handler/models.py
@@ -146,6 +146,7 @@ class Validators(Base):
     created_at: Mapped[Optional[datetime.datetime]] = mapped_column(
         DateTime(True), server_default=func.current_timestamp(), init=False
     )
+    private_key: Mapped[Optional[str]] = mapped_column(String(255))
 
 
 class LLMProviderDBModel(Base):
diff --git a/backend/database_handler/transactions_processor.py b/backend/database_handler/transactions_processor.py
index 7ee77d397..fb1addd73 100644
--- a/backend/database_handler/transactions_processor.py
+++ b/backend/database_handler/transactions_processor.py
@@ -17,7 +17,6 @@
 import os
 from sqlalchemy.orm.attributes import flag_modified
 from backend.domain.types import MAX_ROTATIONS
-
 from backend.rollup.consensus_service import ConsensusService
 
 
@@ -181,6 +180,7 @@ def insert_transaction(
         triggered_by_hash: (
             str | None
         ) = None,  # If filled, the transaction must be present in the database (committed)
+        transaction_hash: str | None = None,
     ) -> str:
         current_nonce = self.get_transaction_count(from_address)
 
@@ -191,9 +191,11 @@ def insert_transaction(
         #         f"Unexpected nonce. Provided: {nonce}, expected: {current_nonce}"
         #     )
 
-        transaction_hash = self._generate_transaction_hash(
-            from_address, to_address, data, value, type, current_nonce
-        )
+        if transaction_hash is None:
+            transaction_hash = self._generate_transaction_hash(
+                from_address, to_address, data, value, type, current_nonce
+            )
+
         ghost_contract_address = None
 
         new_transaction = Transactions(
@@ -278,53 +280,6 @@ def set_transaction_result(
         transaction.consensus_data = consensus_data
         self.session.commit()
 
-    def create_rollup_transaction(self, transaction_hash: str):
-        transaction = (
-            self.session.query(Transactions).filter_by(hash=transaction_hash).one()
-        )
-        rollup_input_data = json.dumps(
-            self._parse_transaction_data(transaction)
-        ).encode("utf-8")
-
-        # Hardhat transaction
-        account = self.web3.eth.accounts[0]
-        private_key = os.environ.get("HARDHAT_PRIVATE_KEY")
-
-        try:
-            gas_estimate = self.web3.eth.estimate_gas(
-                {
-                    "from": account,
-                    "to": transaction.ghost_contract_address,
-                    "value": transaction.value,
-                    "data": rollup_input_data,
-                }
-            )
-
-            transaction = {
-                "from": account,
-                "to": transaction.ghost_contract_address,
-                "value": transaction.value,
-                "data": rollup_input_data,
-                "nonce": self.web3.eth.get_transaction_count(account),
-                "gas": gas_estimate,
-                "gasPrice": 0,
-            }
-
-            # Sign and send the transaction
-            signed_tx = self.web3.eth.account.sign_transaction(
-                transaction, private_key=private_key
-            )
-            tx_hash = self.web3.eth.send_raw_transaction(signed_tx.raw_transaction)
-
-            # Wait for transaction to be actually mined and get the receipt
-            receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
-
-            # Get full transaction details including input data
-            transaction = self.web3.eth.get_transaction(tx_hash)
-
-        except Exception as e:
-            print(f"Error creating rollup transaction: {e}")
-
     def get_transaction_count(self, address: str) -> int:
         count = (
             self.session.query(Transactions)
diff --git a/backend/database_handler/validators_registry.py b/backend/database_handler/validators_registry.py
index d1c687d1b..e21b5fccc 100644
--- a/backend/database_handler/validators_registry.py
+++ b/backend/database_handler/validators_registry.py
@@ -21,6 +21,7 @@ def to_dict(validator: Validators) -> dict:
         "plugin": validator.plugin,
         "plugin_config": validator.plugin_config,
         "created_at": validator.created_at.isoformat(),
+        "private_key": validator.private_key,
     }
 
 
@@ -105,4 +106,5 @@ def _to_db_model(validator: Validator) -> Validators:
         config=validator.llmprovider.config,
         plugin=validator.llmprovider.plugin,
         plugin_config=validator.llmprovider.plugin_config,
+        private_key=validator.private_key.to_0x_hex(),
     )
diff --git a/backend/domain/types.py b/backend/domain/types.py
index 1a2c34690..0e9eee5ff 100644
--- a/backend/domain/types.py
+++ b/backend/domain/types.py
@@ -40,6 +40,7 @@ class Validator:
     stake: int
     llmprovider: LLMProvider
     id: int | None = None
+    private_key: str | None = None
 
     def to_dict(self):
         result = {
@@ -55,6 +56,9 @@ def to_dict(self):
         if self.id:
             result["id"] = self.id
 
+        if self.private_key:
+            result["private_key"] = self.private_key
+
         return result
 
 
diff --git a/backend/protocol_rpc/endpoints.py b/backend/protocol_rpc/endpoints.py
index 92c836181..89cbfc286 100644
--- a/backend/protocol_rpc/endpoints.py
+++ b/backend/protocol_rpc/endpoints.py
@@ -167,10 +167,12 @@ def create_validator(
         )
         validate_provider(llm_provider)
 
-    new_address = accounts_manager.create_new_account().address
+    account = accounts_manager.create_new_account()
+
     return validators_registry.create_validator(
         Validator(
-            address=new_address,
+            address=account.address,
+            private_key=account.key,
             stake=stake,
             llmprovider=llm_provider,
         )
@@ -221,10 +223,15 @@ async def create_random_validators(
     response = []
     for detail in details:
         stake = random.randint(min_stake, max_stake)
-        validator_address = accounts_manager.create_new_account().address
+        validator_account = accounts_manager.create_new_account()
 
         validator = validators_registry.create_validator(
-            Validator(address=validator_address, stake=stake, llmprovider=detail)
+            Validator(
+                address=validator_account.address,
+                private_key=validator_account.key,
+                stake=stake,
+                llmprovider=detail,
+            )
         )
         response.append(validator)
 
@@ -488,7 +495,7 @@ def send_raw_transaction(
     if not transaction_signature_valid:
         raise InvalidTransactionError("Transaction signature verification failed")
 
-    rollup_transaction_details = consensus_service.forward_transaction(
+    rollup_transaction_details = consensus_service.add_transaction(
         signed_rollup_transaction, from_address
     )
 
@@ -534,6 +541,12 @@ def send_raw_transaction(
         to_address = genlayer_transaction.to_address
         transaction_data = {"calldata": genlayer_transaction.data.calldata}
 
+    # Obtain transaction hash from new transaction event
+    if rollup_transaction_details and "tx_id_hex" in rollup_transaction_details:
+        transaction_hash = rollup_transaction_details["tx_id_hex"]
+    else:
+        transaction_hash = None
+
     # Insert transaction into the database
     transaction_hash = transactions_processor.insert_transaction(
         genlayer_transaction.from_address,
@@ -543,6 +556,8 @@ def send_raw_transaction(
         genlayer_transaction.type.value,
         nonce,
         leader_only,
+        None,
+        transaction_hash,
     )
 
     return transaction_hash
diff --git a/backend/protocol_rpc/server.py b/backend/protocol_rpc/server.py
index ae4da207b..0b82d1071 100644
--- a/backend/protocol_rpc/server.py
+++ b/backend/protocol_rpc/server.py
@@ -74,7 +74,7 @@ def create_app():
     initialize_validators_db_session.commit()
 
     consensus = ConsensusAlgorithm(
-        lambda: Session(engine, expire_on_commit=False), msg_handler
+        lambda: Session(engine, expire_on_commit=False), msg_handler, consensus_service
     )
     return (
         app,
diff --git a/backend/rollup/consensus_service.py b/backend/rollup/consensus_service.py
index d49558426..0883fc918 100644
--- a/backend/rollup/consensus_service.py
+++ b/backend/rollup/consensus_service.py
@@ -3,7 +3,7 @@
 from web3 import Web3
 from typing import Optional, Dict, Any
 from pathlib import Path
-from backend.protocol_rpc.message_handler.types import EventType, EventScope, LogEvent
+from hexbytes import HexBytes
 import re
 
 
@@ -18,64 +18,61 @@ def __init__(self):
         hardhat_url = f"{url}:{port}"
         self.web3 = Web3(Web3.HTTPProvider(hardhat_url))
 
-        self.web3_connected = self.web3.is_connected()
+        if not self.web3.is_connected():
+            raise ConnectionError(f"Failed to connect to Hardhat node at {hardhat_url}")
 
-    def load_contract(self, contract_name: str) -> Optional[dict]:
+    def _get_contract(self, contract_name: str):
         """
-        Load contract deployment data and compiled contract data
-
-        Args:
-            contract_name (str): The name of the contract to load
+        Get a contract instance
 
         Returns:
-            Optional[dict]: The contract deployment data or None if loading fails
-        """
-        try:
-            # compiled_data = self._load_compiled_contract(contract_name)
-            deployment_data = self._load_deployment_data(contract_name)
-
-            # if not compiled_data or not deployment_data:
-            #     return None
+            Contract: The contract instance
 
-            return {
-                "address": deployment_data["address"],
-                "abi": deployment_data["abi"],
-                "bytecode": deployment_data["bytecode"],
-            }
+        Raises:
+            Exception: If the contract deployment data cannot be loaded or the contract does not exist
+        """
+        # Load deployment data
+        deployment_data = self._load_deployment_data(contract_name)
+        if not deployment_data:
+            raise Exception(f"Failed to load {contract_name} deployment data")
+
+        # Verify contract exists on chain
+        code = self.web3.eth.get_code(deployment_data["address"])
+        if code == b"" or code == "0x":
+            raise Exception(
+                f"No contract code found at address {deployment_data['address']}"
+            )
 
-        except Exception as e:
-            print(f"[CONSENSUS_SERVICE] Error loading contract: {str(e)}")
-            return None
+        return self.web3.eth.contract(
+            address=deployment_data["address"], abi=deployment_data["abi"]
+        )
 
-    def _load_compiled_contract(self, contract_name: str) -> Optional[Dict[str, Any]]:
+    def load_contract(self, contract_name: str):
         """
-        Load compiled contract data from artifacts
+        Load a contract from deployment data
 
         Args:
-            contract_name (str): The name of the contract to load
+            contract_name (str): Name of the contract to load
 
         Returns:
-            Optional[Dict[str, Any]]: The compiled contract data or None if loading fails
+            dict: Contract data including address, abi and functions
+            None: If there was an error loading the contract
         """
         try:
-            compiled_contract_path = (
-                Path(
-                    f"/app/hardhat/artifacts/contracts/v2_contracts/{contract_name}.sol"
-                )
-                / f"{contract_name}.json"
-            )
-
-            if not compiled_contract_path.exists():
-                print(
-                    f"[CONSENSUS_SERVICE]: Compiled contract not found at {compiled_contract_path}"
-                )
-                return None
+            contract = self._get_contract(contract_name)
+            deployment_data = self._load_deployment_data(contract_name)
 
-            with open(compiled_contract_path, "r") as f:
-                return json.load(f)
+            return {
+                "address": contract.address,
+                "abi": contract.abi,
+                "functions": contract.functions,
+                "bytecode": (
+                    deployment_data.get("bytecode") if deployment_data else None
+                ),
+            }
 
         except Exception as e:
-            print(f"[CONSENSUS_SERVICE]: Error loading compiled contract: {str(e)}")
+            print(f"[CONSENSUS_SERVICE]: Error loading contract: {str(e)}")
             return None
 
     def _load_deployment_data(self, contract_name: str) -> Optional[Dict[str, Any]]:
@@ -90,7 +87,8 @@ def _load_deployment_data(self, contract_name: str) -> Optional[Dict[str, Any]]:
         """
         try:
             deployment_path = (
-                Path("/app/hardhat/deployments/localhost") / f"{contract_name}.json"
+                Path("/app/hardhat/deployments/genlayer_network")
+                / f"{contract_name}.json"
             )
 
             if not deployment_path.exists():
@@ -106,59 +104,60 @@ def _load_deployment_data(self, contract_name: str) -> Optional[Dict[str, Any]]:
             print(f"[CONSENSUS_SERVICE]: Error loading deployment data: {str(e)}")
             return None
 
-    def forward_transaction(
+    def forward_transaction(self, transaction: str | HexBytes) -> dict:
+        """
+        Forward a transaction to the consensus rollup
+        """
+        tx_hash = self.web3.eth.send_raw_transaction(transaction)
+        receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
+        return receipt
+
+    def wait_new_transaction_event(self, receipt: dict) -> dict:
+        """
+        Wait for NewTransaction event from receipt
+        """
+        consensus_main_contract = self._get_contract("ConsensusMain")
+
+        # Get NewTransaction events from receipt
+        new_tx_events = consensus_main_contract.events.NewTransaction().process_receipt(
+            receipt
+        )
+
+        if new_tx_events:
+            # Extract event data
+            tx_id = new_tx_events[0]["args"]["txId"]
+            recipient = new_tx_events[0]["args"]["recipient"]
+            activator = new_tx_events[0]["args"]["activator"]
+
+            # Convert tx_id from bytes to hex string for better readability
+            tx_id_hex = "0x" + tx_id.hex() if isinstance(tx_id, bytes) else tx_id
+
+            return {
+                "receipt": receipt,
+                "tx_id": tx_id,
+                "tx_id_hex": tx_id_hex,  # Adding hex version for easier reading
+                "recipient": recipient,
+                "activator": activator,
+            }
+        else:
+            print("[CONSENSUS_SERVICE]: No NewTransaction event found in receipt")
+            return receipt
+
+    def add_transaction(
         self, transaction: dict, from_address: str, retry: bool = True
     ) -> str:
         """
         Forward a transaction to the consensus rollup and wait for NewTransaction event
         """
-
-        if not self.web3_connected:
+        if not self.web3.is_connected():
             print(
                 "[CONSENSUS_SERVICE]: Not connected to Hardhat node, skipping transaction forwarding"
             )
             return None
 
         try:
-            tx_hash = self.web3.eth.send_raw_transaction(transaction)
-            receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
-
-            # Load ConsensusMain contract
-            consensus_contract_data = self.load_contract("ConsensusMain")
-            if not consensus_contract_data:
-                print("[CONSENSUS_SERVICE]: Failed to load ConsensusMain contract")
-                return None
-
-            # Create contract instance
-            consensus_contract = self.web3.eth.contract(
-                address=consensus_contract_data["address"],
-                abi=consensus_contract_data["abi"],
-            )
-
-            # Get NewTransaction events from receipt
-            new_tx_events = consensus_contract.events.NewTransaction().process_receipt(
-                receipt
-            )
-
-            if new_tx_events:
-                # Extract event data
-                tx_id = new_tx_events[0]["args"]["txId"]
-                recipient = new_tx_events[0]["args"]["recipient"]
-                activator = new_tx_events[0]["args"]["activator"]
-
-                # Convert tx_id from bytes to hex string for better readability
-                tx_id_hex = "0x" + tx_id.hex() if isinstance(tx_id, bytes) else tx_id
-
-                return {
-                    "receipt": receipt,
-                    "tx_id": tx_id,
-                    "tx_id_hex": tx_id_hex,  # Adding hex version for easier reading
-                    "recipient": recipient,
-                    "activator": activator,
-                }
-            else:
-                print("[CONSENSUS_SERVICE]: No NewTransaction event found in receipt")
-                return receipt
+            receipt = self.forward_transaction(transaction)
+            return self.wait_new_transaction_event(receipt)
 
         except Exception as e:
             error_str = str(e)
@@ -184,7 +183,7 @@ def forward_transaction(
                     )
 
                     if retry:
-                        return self.forward_transaction(
+                        return self.add_transaction(
                             transaction, from_address, retry=False
                         )
                 else:
@@ -194,3 +193,54 @@ def forward_transaction(
 
             print(f"[CONSENSUS_SERVICE]: Error forwarding transaction: {error_str}")
             return None
+
+    def emit_transaction_event(self, event_name: str, account: dict, *args):
+        """
+        Generic method to emit transaction events
+
+        Args:
+            event_name (str): Name of the event function to call
+            account (dict): Account object containing address and private key
+            *args: Arguments to pass to the event function
+        """
+        if not self.web3.is_connected():
+            print(
+                "[CONSENSUS_SERVICE]: Not connected to Hardhat node, skipping transaction forwarding"
+            )
+            return None
+
+        if "private_key" in account:
+            account_address = account["address"]
+            account_private_key = account["private_key"]
+        else:
+            print(
+                f"[CONSENSUS_SERVICE]: Error emitting {event_name}: Account object must contain private_key"
+            )
+            return None
+
+        consensus_main_contract = self._get_contract("ConsensusMain")
+
+        try:
+            # Get the function from the contract
+            event_function = getattr(consensus_main_contract.functions, event_name)
+
+            # Build and send transaction
+            tx = event_function(*args).build_transaction(
+                {
+                    "from": account_address,
+                    "gas": 500000,
+                    "gasPrice": 0,
+                    "nonce": self.web3.eth.get_transaction_count(account_address),
+                }
+            )
+
+            # Sign and send transaction
+            signed_tx = self.web3.eth.account.sign_transaction(
+                tx, private_key=account_private_key
+            )
+
+            return self.forward_transaction(signed_tx.raw_transaction)
+
+        except Exception as e:
+            print(f"[CONSENSUS_SERVICE]: Error emitting {event_name}: {str(e)}")
+            return None
diff --git a/docker-compose.yml b/docker-compose.yml
index 719a8ea8d..b33fccc38 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -224,7 +224,11 @@ services:
       - HARDHAT_NETWORK=hardhat
     restart: always
     healthcheck:
-      test: ["CMD", "curl", "-X", "POST", "-H", "Content-Type: application/json", "--fail", "http://localhost:8545", "-d", '{"jsonrpc":"2.0","method":"net_version","params":[],"id":1}']
+      test: |
+        /bin/sh -c '
+        curl -X POST -H "Content-Type: application/json" --fail http://localhost:8545 -d "{\"jsonrpc\":\"2.0\",\"method\":\"net_version\",\"params\":[],\"id\":1}" &&
+        test -f /app/deployments/genlayer_network/ConsensusMain.json
+        '
       interval: 60s
       timeout: 30s
       retries: 5
diff --git a/docker/Dockerfile.hardhat b/docker/Dockerfile.hardhat
index 59186504e..36b57830b 100644
--- a/docker/Dockerfile.hardhat
+++ b/docker/Dockerfile.hardhat
@@ -14,8 +14,7 @@ COPY ./hardhat .
 RUN npx hardhat compile || true
 
 # Create directories and set permissions
-RUN mkdir -p /app/deployments/localhost && \
-    mkdir -p /app/deployments/hardhat && \
+RUN mkdir -p /app/deployments/hardhat && \
     mkdir -p /app/artifacts/build-info && \
     mkdir -p /app/artifacts/contracts && \
     mkdir -p /app/cache && \
@@ -63,10 +62,6 @@ RUN echo '#!/bin/sh' > /app/start.sh && \
     echo '  echo "[$(date +"%Y-%m-%d %H:%M:%S")] Creating initial snapshot..."' >> /app/start.sh && \
     echo '  npx hardhat run scripts/snapshot.js --network genlayer_network' >> /app/start.sh && \
     echo 'fi' >> /app/start.sh && \
-    echo 'echo "[$(date +"%Y-%m-%d %H:%M:%S")] Running tests..."' >> /app/start.sh && \
-    echo 'npx hardhat test --network genlayer_network' >> /app/start.sh && \
-    echo 'echo "[$(date +"%Y-%m-%d %H:%M:%S")] Tests completed"' >> /app/start.sh && \
-    echo 'echo "[$(date +"%Y-%m-%d %H:%M:%S")] Starting automatic snapshot service..."' >> /app/start.sh && \
     echo '/app/snapshot.sh &' >> /app/start.sh && \
     echo 'wait' >> /app/start.sh && \
     chmod +x /app/start.sh
diff --git a/hardhat/contracts/v2_contracts/ConsensusMain.sol b/hardhat/contracts/v2_contracts/ConsensusMain.sol
index b788a7168..8ffc04ec7 100644
--- a/hardhat/contracts/v2_contracts/ConsensusMain.sol
+++ b/hardhat/contracts/v2_contracts/ConsensusMain.sol
@@ -127,6 +127,66 @@ contract ConsensusMain is
 
 	// EXTERNAL FUNCTIONS - STATE CHANGING
 
+	function emitTransactionLeaderRotated(
+		bytes32 txId,
+		address newLeader
+	) external {
+		emit TransactionLeaderRotated(txId, newLeader);
+	}
+
+	function emitTransactionActivated(
+		bytes32 txId,
+		address leader,
+		address[] calldata validators
+	) external {
+		emit TransactionActivated(txId, leader, validators);
+	}
+
+	function emitTransactionReceiptProposed(
+		bytes32 txId
+	) external {
+		emit TransactionReceiptProposed(txId);
+	}
+
+	function emitVoteCommitted(
+		bytes32 txId,
+		address validator,
+		bool isLastVote
+	) external {
+		emit VoteCommitted(txId, validator, isLastVote);
+	}
+
+	function emitVoteRevealed(
+		bytes32 txId,
+		address validator,
+		ITransactions.VoteType voteType,
+		bool isLastVote,
+		ITransactions.ResultType result
+	) external {
+		emit VoteRevealed(txId, validator, voteType, isLastVote, result);
+	}
+
+	function emitTransactionAccepted(
+		bytes32 txId
+	) external {
+		emit TransactionAccepted(txId);
+	}
+
+	function emitTransactionFinalized(
+		bytes32 txId
+	) external {
+		emit TransactionFinalized(txId);
+	}
+
+	function emitAppealStarted(
+		bytes32 txId,
+		address appealer,
+		uint256 appealBond,
+		address[] calldata appealValidators
+	) external {
+		emit AppealStarted(txId, appealer, appealBond, appealValidators);
+	}
+
 	/**
 	 * @notice Adds a new transaction to the system
 	 * @param _sender Transaction sender address
diff --git a/hardhat/deploy/001_deploy_fixture.js b/hardhat/deploy/001_deploy_fixture.js
index 0aa1f4fb3..5f2fc6369 100644
--- a/hardhat/deploy/001_deploy_fixture.js
+++ b/hardhat/deploy/001_deploy_fixture.js
@@ -2,40 +2,6 @@ const fs = require("fs-extra");
 const path = require("path");
 const { ethers } = require("hardhat");
 
-
-async function saveDeploymentWithHardhat(name, folder = "deployments/localhost") {
-  try {
-    // Get deployment from hardhat-deploy
-    const deployment = await deployments.get(name);
-
-    // Get bytecode from chain
-    let bytecodeOnChain;
-    try {
-      bytecodeOnChain = await ethers.provider.getCode(deployment.address);
-    } catch (bytecodeError) {
-      console.warn(`Could not get bytecode for ${name}: ${bytecodeError.message}`);
-      bytecodeOnChain = "";
-    }
-
-    // Create deployment object with standard format
-    const deploymentData = {
-      address: deployment.address,
-      abi: deployment.abi,
-      bytecode: bytecodeOnChain,
-    };
-
-    // Ensure folder exists
-    await fs.ensureDir(folder);
-    const savePath = path.join(folder, `${name}.json`);
-
-    // Save file
-    await fs.writeJson(savePath, deploymentData, { spaces: 2 });
-    console.log(`Saved deployment for ${name} at ${savePath}`);
-  } catch (error) {
-    console.error(`Error trying to save ${name}:`, error.message);
-  }
-}
-
 module.exports = async ({ getNamedAccounts, deployments, ethers }) => {
   const { deploy, execute, log } = deployments;
   const { deployer } = await getNamedAccounts();
@@ -399,37 +365,6 @@ module.exports = async ({ getNamedAccounts, deployments, ethers }) => {
   }
 
   log("All contracts deployed and configured!\n");
-
-  // Save deployments
-  const contractNames = [
-    'GhostContract',
-    "ArrayUtils",
-    "RandomnessUtils",
-    "Transactions",
-    "ConsensusManager",
-    "GhostFactory",
-    "GhostBlueprint",
-    "MockGenStaking",
-    "Queues",
-    "Messages",
-    "FeeManager",
-    "Utils",
-    "Rounds",
-    "Voting",
-    "Idleness",
-    "ConsensusMain",
-    "ConsensusData",
-  ];
-
-  for (const name of contractNames) {
-    try {
-      await saveDeploymentWithHardhat(name, "deployments/localhost");
-    } catch (err) {
-      console.error(`Error saving contract ${name}:`, err.message);
-    }
-  }
-
-  log("All contracts deployed, executed inits, and saved in deployments/localhost!\n");
 };
 
 module.exports.tags = ["All"];
\ No newline at end of file
diff --git a/tests/unit/consensus/test_helpers.py b/tests/unit/consensus/test_helpers.py
index 347a7731e..b16c25bf4 100644
--- a/tests/unit/consensus/test_helpers.py
+++ b/tests/unit/consensus/test_helpers.py
@@ -15,6 +15,7 @@
 from backend.node.types import ExecutionMode, ExecutionResultStatus, Receipt, Vote
 from backend.protocol_rpc.message_handler.base import MessageHandler
 from typing import Optional
+from backend.rollup.consensus_service import ConsensusService
 
 DEFAULT_FINALITY_WINDOW = 5
 DEFAULT_CONSENSUS_SLEEP_TIME = 2
@@ -116,9 +117,6 @@ def get_accepted_undetermined_transactions(self):
             transactions_by_address[address].append(transaction)
         return transactions_by_address
 
-    def create_rollup_transaction(self, transaction_hash: str):
-        pass
-
     def set_transaction_appeal_failed(self, transaction_hash: str, appeal_failed: int):
         if appeal_failed < 0:
             raise ValueError("appeal_failed must be a non-negative integer")
@@ -302,6 +300,7 @@ def get_nodes_specs(number_of_nodes: int):
             "provider": f"provider{i}",
             "model": f"model{i}",
             "config": f"config{i}",
+            "private_key": f"private_key{i}",
         }
         for i in range(number_of_nodes)
     ]
@@ -321,6 +320,7 @@ def node_factory(
     mock.validator_mode = mode
     mock.address = node["address"]
     mock.leader_receipt = receipt
+    mock.private_key = node["private_key"]
 
     mock.exec_transaction = AsyncMock(
         return_value=Receipt(
@@ -330,7 +330,10 @@ def node_factory(
             gas_used=0,
             contract_state={},
             result=DEFAULT_EXEC_RESULT,
-            node_config={"address": node["address"]},
+            node_config={
+                "address": node["address"],
+                "private_key": node["private_key"],
+            },
             eq_outputs={},
             execution_result=ExecutionResultStatus.SUCCESS,
         )
@@ -395,7 +398,9 @@ def send_message(self, log_event, log_to_terminal: bool = True):
     mock_msg_handler = MessageHandlerMock()
 
     consensus_algorithm = ConsensusAlgorithm(
-        get_session=lambda: mock_session, msg_handler=mock_msg_handler
+        get_session=lambda: mock_session,
+        msg_handler=mock_msg_handler,
+        consensus_service=MagicMock(),
     )
     consensus_algorithm.finality_window_time = DEFAULT_FINALITY_WINDOW
     consensus_algorithm.consensus_sleep_time = DEFAULT_CONSENSUS_SLEEP_TIME


Reviews:
user: login: cristiam86
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T09:44:37Z
user: login: kstroobants
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T09:52:20Z
user: login: kstroobants
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T09:53:28Z
user: login: mpaya5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T10:58:10Z
user: login: cristiam86
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T12:27:53Z
user: login: mpaya5
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T13:14:07Z

Review comments:
path: hardhat/deploy/001_deploy_fixture.js
user: login: cristiam86
body: @kstroobants why are these methods commented?  could you please check with @mpaya5? 
created_at: 2025-04-03T09:43:56Z
author_association: CONTRIBUTOR
path: backend/rollup/consensus_service.py
user: login: cristiam86
body: @kstroobants we have lost the nonce management to set the account right one and try again
created_at: 2025-04-03T09:44:34Z
author_association: CONTRIBUTOR
path: backend/rollup/consensus_service.py
user: login: kstroobants
body: No stress, it is still there in function add_transaction line 162. I did a refactor of functions as I was also using a part.
created_at: 2025-04-03T09:52:20Z
author_association: CONTRIBUTOR
path: hardhat/deploy/001_deploy_fixture.js
user: login: kstroobants
body: @cristiam86 he told me to do it.
@mpaya5 can you clean this?
created_at: 2025-04-03T09:53:28Z
author_association: CONTRIBUTOR
path: hardhat/deploy/001_deploy_fixture.js
user: login: mpaya5
body: Sure! @kstroobants 
created_at: 2025-04-03T10:58:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-14T19:11:03Z
body: ## [Codecov](https://app.codecov.io/gh/yeagerai/genlayer-studio/pull/1001?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yeagerai) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 14.83%. Comparing base [(`6fbeaac`)](https://app.codecov.io/gh/yeagerai/genlayer-studio/commit/6fbeaac2aca55535271f6c842d66f0f327261999?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yeagerai) to head [(`7dec12a`)](https://app.codecov.io/gh/yeagerai/genlayer-studio/commit/7dec12a8a40d127c656f0e8380b890d0bdf04201?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yeagerai).
> Report is 3 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1001   +/-   ##
=======================================
  Coverage   14.83%   14.83%           
=======================================
  Files         123      123           
  Lines        7355     7355           
  Branches      254      254           
=======================================
  Hits         1091     1091           
  Misses       6185     6185           
  Partials       79       79           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/yeagerai/genlayer-studio/pull/1001?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yeagerai).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yeagerai).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-03T13:05:36Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=yeagerai_genlayer-simulator&pullRequest=1001) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [45.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=yeagerai_genlayer-simulator&pullRequest=1001&metric=new_coverage&view=list) (required ≥ 80%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=yeagerai_genlayer-simulator&pullRequest=1001)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-07T08:03:30Z
body: :tada: This PR is included in version 0.48.0 :tada:

The release is available on [GitHub release](https://github.com/yeagerai/genlayer-studio/releases/tag/v0.48.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: CONTRIBUTOR