Skip to content

Commit 69bf4ea

Browse files
mpaya5kstroobantscristiam86
authored
feat(simulator): add hardhat network and fix JsonRpcService import (#676)
* feat: add hardhat node and web3 package - Add Hardhat node service with Docker configuration - Configure hardhat.config.js with local network settings - Add 20 test accounts with 10,000 ETH each - Add web3 Python package to backend requirements - Update .gitignore for Hardhat artifacts and cache * feat: add contract and try script * feat: setup contract compilation and artifact handling - Add contract compilation setup with hardhat - Configure project structure for smart contracts - Fix file path for GhostContract.json artifact - Add necessary dependencies for contract compilation - Setup directory structure for contracts and artifacts * feat: creating hardhat transactions in consensus mechanism - Moved web3 python package into backend requirements file - Added access to compiled hardhat contract in jsonrpc service - Added database migration file so that a transaction has the ghost contract address of hardhat network - When a genlayer contract is deployed then a hardhat contract is deployed, both are linked in the CurrentState table - When a genlayer write method is executed then the new transaction gets the hardhat contract from the CurrentState table - When a genlayer transaction changes from status then a rollup transaction is created on the hardhat network - todo: remove rollup transaction table, put hardhat port in env, link genlayer account to hardhat account, check for out of gas, remove prints * feat: put hardhat port in env * feat: remove rollup transactions database table * feat: free transactions on hardhat, one hardhat account * test: add hardhat test with code from the transaction_processor * fix: resolve pre-commit error * test: moved test to integration tests, upgrade web3 version in requirements as it gave an import error * feat(hardhat): add genlayer-consensus contracts and setup compilation - Add genlayer-consensus smart contracts to hardhat/contracts directory - Update hardhat.config.js to enable new code generator (viaIR: true) - Add @openzeppelin/contracts and @openzeppelin/contracts-upgradeable dependencies - Configure hardhat Docker container for contract compilation - Verify hardhat node functionality in genlayer-studio This commit sets up the smart contract development environment with the necessary dependencies and configurations to compile and deploy genlayer-consensus contracts. * feat: deleted amm_adaptive.py - Deleted amm_adaptive.py because is not using the new syntax I will upload the new amm_adaptive.py whenevir will be ready * fix: contract interaction - Fixed contract deployment state updates for frontend synchronization - Improved contract method interactions and state management - Ensured proper state updates after contract value changes * fix: pre-commit errors solved * fix: solved black pre-commit issue * fix(migrations): resolve multiple head revisions issue - Identified and merged multiple migration heads - Ensured database schema consistency - Updated migration scripts to prevent future conflicts This change resolves the issue with multiple Alembic head revisions, ensuring smooth database migrations and consistent schema state. * fix: restore JsonRpcService import removed in last PR Restores the JsonRpcService import that was accidentally removed in the last PR. This fixes the build error where JsonRpcService was undefined. --------- Co-authored-by: kstroobants <stroobants.kristof@hotmail.com> Co-authored-by: Cristiam Da Silva <cristiam86@gmail.com>
1 parent 1f5f58b commit 69bf4ea

36 files changed

+1611
-56
lines changed

.dockerignore

+9
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,12 @@ frontend/vite.config.ts.timestamp-*
2727
# Byte-compiled / optimized / DLL files
2828
**/__pycache__/
2929
**/*.py[cod]
30+
31+
# Hardhat files
32+
hardhat/cache/
33+
hardhat/artifacts/
34+
hardhat/node_modules/
35+
hardhat/coverage/
36+
hardhat/.env
37+
hardhat/coverage.json
38+
hardhat/typechain-types/

.env.example

+6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ VITE_PROXY_JSON_RPC_SERVER_URL = 'http://jsonrpc:4000'
5151
VITE_PROXY_WS_SERVER_URL = 'ws://jsonrpc:4000'
5252
VITE_IS_HOSTED = 'false'
5353

54+
FRONTEND_BUILD_TARGET = 'final' # change to 'dev' to run in dev mode
55+
56+
# Hardhat port
57+
HARDHAT_URL = 'http://hardhat'
58+
HARDHATPORT = '8545'
59+
HARDHAT_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
5460
# LLM Providers Configuration
5561
# If you want to use OpenAI LLMs, add your key here
5662
OPENAIKEY = '<add_your_openai_api_key_here>'

.github/workflows/backend_integration_tests_pr.yml

+25
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,28 @@ jobs:
140140
uses: ./.github/workflows/load-test-oha.yml
141141
with:
142142
oha-version: "v1.4.5"
143+
144+
hardhat-test:
145+
needs: triggers
146+
if: ${{ needs.triggers.outputs.is_pull_request_opened == 'true' || needs.triggers.outputs.is_pull_request_review_approved == 'true' || needs.triggers.outputs.is_pull_request_labeled_with_run_tests == 'true' }}
147+
148+
runs-on: ubuntu-latest
149+
150+
steps:
151+
- name: Checkout code
152+
uses: actions/checkout@v4
153+
154+
- name: Set up Docker Buildx
155+
uses: docker/setup-buildx-action@v3
156+
157+
- name: Cache Docker layers
158+
uses: actions/cache@v4
159+
with:
160+
path: /tmp/.buildx-cache
161+
key: ${{ runner.os }}-buildx-${{ github.sha }}
162+
restore-keys: |
163+
${{ runner.os }}-buildx-
164+
165+
- name: Run Docker Compose
166+
run: docker compose -f tests/hardhat/docker-compose.yml --project-directory . up tests --build --force-recreate --always-recreate-deps
167+

.gitignore

+10-1
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,17 @@ consensus/nodes/nodes.json
147147
# npm
148148
node_modules/
149149
package-lock.json
150-
package.json
150+
/package.json
151151

152152
# Nginx TLS certificates and keys
153153
nginx/ssl/*.key
154154
nginx/ssl/*.crt
155+
156+
# Hardhat files
157+
hardhat/cache
158+
hardhat/artifacts
159+
hardhat/.openzeppelin
160+
hardhat/coverage
161+
hardhat/coverage.json
162+
hardhat/typechain
163+
hardhat/typechain-types

backend/consensus/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ def finalize_transaction(
420420
"data": {
421421
"state": leader_receipt["contract_state"],
422422
"code": transaction.data["contract_code"],
423+
"ghost_contract_address": transaction.ghost_contract_address,
423424
},
424425
}
425426
leaders_contract_snapshot.register_contract(new_contract)

backend/database_handler/contract_snapshot.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ def __init__(self, contract_address: str | None, session: Session):
2626
self.contract_data = contract_account.data
2727
self.contract_code = self.contract_data["code"]
2828
self.encoded_state = self.contract_data["state"]
29+
self.ghost_contract_address = (
30+
self.contract_data["ghost_contract_address"]
31+
if "ghost_contract_address" in self.contract_data
32+
else None
33+
)
2934

3035
def _load_contract_account(self) -> CurrentState:
3136
"""Load and return the current state of the contract from the database."""
32-
3337
result = (
3438
self.session.query(CurrentState)
3539
.filter(CurrentState.id == self.contract_address)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""add ghost contract address column
2+
3+
Revision ID: cb34b6b353ed
4+
Revises: 0d9538be0318
5+
Create Date: 2024-11-28 14:00:00
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "cb34b6b353ed"
17+
down_revision: Union[str, None] = "37196a51038e"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
op.add_column(
24+
"transactions",
25+
sa.Column("ghost_contract_address", sa.String(length=255), nullable=True),
26+
)
27+
28+
29+
def downgrade() -> None:
30+
op.drop_column("transactions", "ghost_contract_address")

backend/database_handler/models.py

+1-32
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class Transactions(Base):
9494
r: Mapped[Optional[int]] = mapped_column(Integer)
9595
s: Mapped[Optional[int]] = mapped_column(Integer)
9696
v: Mapped[Optional[int]] = mapped_column(Integer)
97+
ghost_contract_address: Mapped[Optional[str]] = mapped_column(String(255))
9798

9899
# Relationship for triggered transactions
99100
triggered_by_hash: Mapped[Optional[str]] = mapped_column(
@@ -117,38 +118,6 @@ class Transactions(Base):
117118
timestamp_accepted: Mapped[Optional[int]] = mapped_column(BigInteger, default=None)
118119

119120

120-
class RollupTransactions(Base):
121-
__tablename__ = "rollup_transactions"
122-
__table_args__ = (
123-
PrimaryKeyConstraint("transaction_hash", name="rollup_transactions_pkey"),
124-
)
125-
126-
transaction_hash: Mapped[str] = mapped_column(
127-
String(66), primary_key=True, unique=True
128-
)
129-
from_: Mapped[str] = mapped_column(
130-
String(255),
131-
)
132-
to_: Mapped[Optional[dict]] = mapped_column(
133-
String(255),
134-
)
135-
gas: Mapped[int] = mapped_column(
136-
Integer,
137-
)
138-
gas_price: Mapped[int] = mapped_column(
139-
Integer,
140-
)
141-
value: Mapped[Optional[int]] = mapped_column(
142-
Integer,
143-
)
144-
input: Mapped[str] = mapped_column(
145-
Text,
146-
)
147-
nonce: Mapped[int] = mapped_column(
148-
BigInteger,
149-
)
150-
151-
152121
class Validators(Base):
153122
__tablename__ = "validators"
154123
__table_args__ = (

backend/database_handler/transactions_processor.py

+106-21
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from enum import Enum
33
import rlp
44

5-
from .models import Transactions, RollupTransactions
5+
from .models import Transactions
66
from sqlalchemy.orm import Session
77
from sqlalchemy import or_, and_
88

@@ -11,6 +11,10 @@
1111
import json
1212
import base64
1313
import time
14+
from backend.domain.types import TransactionType
15+
from web3 import Web3
16+
from backend.database_handler.contract_snapshot import ContractSnapshot
17+
import os
1418

1519

1620
class TransactionAddressFilter(Enum):
@@ -26,6 +30,12 @@ def __init__(
2630
):
2731
self.session = session
2832

33+
# Connect to Hardhat Network
34+
port = os.environ.get("HARDHAT_PORT")
35+
url = os.environ.get("HARDHAT_URL")
36+
hardhat_url = f"{url}:{port}"
37+
self.web3 = Web3(Web3.HTTPProvider(hardhat_url))
38+
2939
@staticmethod
3040
def _parse_transaction_data(transaction_data: Transactions) -> dict:
3141
return {
@@ -49,6 +59,7 @@ def _parse_transaction_data(transaction_data: Transactions) -> dict:
4959
transaction.hash
5060
for transaction in transaction_data.triggered_transactions
5161
],
62+
"ghost_contract_address": transaction_data.ghost_contract_address,
5263
"appealed": transaction_data.appealed,
5364
"timestamp_accepted": transaction_data.timestamp_accepted,
5465
}
@@ -130,6 +141,64 @@ def insert_transaction(
130141
from_address, to_address, data, value, type, nonce
131142
)
132143

144+
if type == TransactionType.DEPLOY_CONTRACT.value:
145+
# Hardhat account
146+
account = self.web3.eth.accounts[0]
147+
private_key = os.environ.get("HARDHAT_PRIVATE_KEY")
148+
149+
# Ghost contract
150+
# Read contract ABI and bytecode from compiled contract
151+
contract_file = os.path.join(
152+
os.getcwd(),
153+
"hardhat/artifacts/contracts/GhostContract.sol/GhostContract.json",
154+
)
155+
156+
with open(contract_file, "r") as f:
157+
contract_json = json.loads(f.read())
158+
abi = contract_json["abi"]
159+
bytecode = contract_json["bytecode"]
160+
161+
# Create the contract instance
162+
contract = self.web3.eth.contract(abi=abi, bytecode=bytecode)
163+
164+
# Build the transaction
165+
gas_estimate = self.web3.eth.estimate_gas(
166+
contract.constructor().build_transaction(
167+
{
168+
"from": account,
169+
"nonce": self.web3.eth.get_transaction_count(account),
170+
"gasPrice": 0,
171+
}
172+
)
173+
)
174+
transaction = contract.constructor().build_transaction(
175+
{
176+
"from": account,
177+
"nonce": self.web3.eth.get_transaction_count(account),
178+
"gas": gas_estimate,
179+
"gasPrice": 0,
180+
}
181+
)
182+
183+
# Sign the transaction
184+
signed_tx = self.web3.eth.account.sign_transaction(
185+
transaction, private_key=private_key
186+
)
187+
188+
# Send the transaction
189+
tx_hash = self.web3.eth.send_raw_transaction(signed_tx.raw_transaction)
190+
191+
# Wait for the transaction receipt
192+
receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
193+
ghost_contract_address = receipt.contractAddress
194+
195+
elif type == TransactionType.RUN_CONTRACT.value:
196+
genlayer_contract_address = to_address
197+
contract_snapshot = ContractSnapshot(
198+
genlayer_contract_address, self.session
199+
)
200+
ghost_contract_address = contract_snapshot.ghost_contract_address
201+
133202
new_transaction = Transactions(
134203
hash=transaction_hash,
135204
from_address=from_address,
@@ -152,6 +221,7 @@ def insert_transaction(
152221
if triggered_by_hash
153222
else None
154223
),
224+
ghost_contract_address=ghost_contract_address,
155225
appealed=False,
156226
timestamp_accepted=None,
157227
)
@@ -195,29 +265,44 @@ def create_rollup_transaction(self, transaction_hash: str):
195265
transaction = (
196266
self.session.query(Transactions).filter_by(hash=transaction_hash).one()
197267
)
198-
rollup_input_data = self._transaction_data_to_str(
268+
rollup_input_data = json.dumps(
199269
self._parse_transaction_data(transaction)
270+
).encode("utf-8")
271+
272+
# Hardhat transaction
273+
account = self.web3.eth.accounts[0]
274+
private_key = os.environ.get("HARDHAT_PRIVATE_KEY")
275+
276+
gas_estimate = self.web3.eth.estimate_gas(
277+
{
278+
"from": account,
279+
"to": transaction.ghost_contract_address,
280+
"value": transaction.value,
281+
"data": rollup_input_data,
282+
}
200283
)
201-
rollup_nonce = int(time.time() * 1000)
202-
rollup_transaction_hash = self._generate_transaction_hash(
203-
transaction.from_address,
204-
transaction.to_address,
205-
rollup_input_data,
206-
transaction.value,
207-
0,
208-
rollup_nonce,
209-
)
210-
rollup_transaction_record = RollupTransactions(
211-
transaction_hash=rollup_transaction_hash,
212-
from_=transaction.from_address,
213-
to_=transaction.to_address,
214-
gas=0,
215-
gas_price=0,
216-
value=transaction.value,
217-
input=rollup_input_data,
218-
nonce=rollup_nonce,
284+
285+
transaction = {
286+
"from": account,
287+
"to": transaction.ghost_contract_address,
288+
"value": transaction.value,
289+
"data": rollup_input_data,
290+
"nonce": self.web3.eth.get_transaction_count(account),
291+
"gas": gas_estimate,
292+
"gasPrice": 0,
293+
}
294+
295+
# Sign and send the transaction
296+
signed_tx = self.web3.eth.account.sign_transaction(
297+
transaction, private_key=private_key
219298
)
220-
self.session.add(rollup_transaction_record)
299+
tx_hash = self.web3.eth.send_raw_transaction(signed_tx.raw_transaction)
300+
301+
# Wait for transaction to be actually mined and get the receipt
302+
receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
303+
304+
# Get full transaction details including input data
305+
transaction = self.web3.eth.get_transaction(tx_hash)
221306

222307
def get_transaction_count(self, address: str) -> int:
223308
count = (

backend/domain/types.py

+3
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class Transaction:
7979
leader_only: bool = (
8080
False # Flag to indicate if this transaction should be processed only by the leader. Used for fast and cheap execution of transactions.
8181
)
82+
ghost_contract_address: str | None = None
8283
appealed: bool = False
8384
timestamp_accepted: int | None = None
8485

@@ -99,6 +100,7 @@ def to_dict(self):
99100
"s": self.s,
100101
"v": self.v,
101102
"leader_only": self.leader_only,
103+
"ghost_contract_address": self.ghost_contract_address,
102104
"appealed": self.appealed,
103105
"timestamp_accepted": self.timestamp_accepted,
104106
}
@@ -121,6 +123,7 @@ def transaction_from_dict(input: dict) -> Transaction:
121123
s=input.get("s"),
122124
v=input.get("v"),
123125
leader_only=input.get("leader_only", False),
126+
ghost_contract_address=input.get("ghost_contract_address"),
124127
appealed=input.get("appealed"),
125128
timestamp_accepted=input.get("timestamp_accepted"),
126129
)

backend/protocol_rpc/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ Flask-SQLAlchemy==3.1.1
2121
jsf==0.11.2
2222
jsonschema==4.23.0
2323
loguru==0.7.2
24+
web3==7.5.0

0 commit comments

Comments
 (0)