Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding hardhat #626

Merged
merged 19 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ frontend/vite.config.ts.timestamp-*
# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.py[cod]

# Hardhat files
hardhat/cache/
hardhat/artifacts/
hardhat/node_modules/
hardhat/coverage/
hardhat/.env
hardhat/coverage.json
hardhat/typechain-types/
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ VITE_PROXY_JSON_RPC_SERVER_URL = 'http://jsonrpc:4000'
VITE_PROXY_WS_SERVER_URL = 'ws://jsonrpc:4000'

FRONTEND_BUILD_TARGET = 'final' # change to 'dev' to run in dev mode

# Hardhat port
HARDHATPORT = '8545'
HARDHAT_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
25 changes: 25 additions & 0 deletions .github/workflows/backend_integration_tests_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,28 @@ jobs:
uses: ./.github/workflows/load-test-oha.yml
with:
oha-version: "v1.4.5"

hardhat-test:
needs: triggers
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' }}

runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-

- name: Run Docker Compose
run: docker compose -f tests/hardhat/docker-compose.yml --project-directory . up tests --build --force-recreate --always-recreate-deps

11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,13 @@ consensus/nodes/nodes.json
# npm
node_modules/
package-lock.json
package.json
/package.json

# Hardhat files
hardhat/cache
hardhat/artifacts
hardhat/.openzeppelin
hardhat/coverage
hardhat/coverage.json
hardhat/typechain
hardhat/typechain-types
1 change: 1 addition & 0 deletions backend/consensus/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ async def exec_transaction(
"data": {
"state": leader_receipt.contract_state,
"code": transaction.data["contract_code"],
"ghost_contract_address": transaction.ghost_contract_address,
},
}
contract_snapshot.register_contract(new_contract)
Expand Down
3 changes: 2 additions & 1 deletion backend/database_handler/contract_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class ContractSnapshot:
"""
Warning: if you initialize this class with a contract_address:
- The contract_address must exist in the database.
- `self.contract_data`, `self.contract_code` and `self.cencoded_state` will be loaded from the database **only once** at initialization.
- `self.contract_data`, `self.contract_code`, `self.encoded_state` and `self.ghost_contract_address` will be loaded from the database **only once** at initialization.
"""

def __init__(self, contract_address: str, session: Session):
Expand All @@ -22,6 +22,7 @@ def __init__(self, contract_address: str, session: Session):
self.contract_data = contract_account.data
self.contract_code = self.contract_data["code"]
self.encoded_state = self.contract_data["state"]
self.ghost_contract_address = self.contract_data["ghost_contract_address"]

def _load_contract_account(self) -> CurrentState:
"""Load and return the current state of the contract from the database."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""hardhat

Revision ID: 3fb219d7a814
Revises: 579e86111b36
Create Date: 2024-11-20 09:41:33.865721

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "3fb219d7a814"
down_revision: Union[str, None] = "579e86111b36"
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(
"transactions",
sa.Column("ghost_contract_address", sa.String(length=255), nullable=True),
)
op.drop_table("rollup_transactions")
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("transactions", "ghost_contract_address")
op.create_table(
"rollup_transactions",
sa.Column(
"transaction_hash",
sa.VARCHAR(length=66),
autoincrement=False,
nullable=False,
),
sa.Column("from_", sa.VARCHAR(length=255), autoincrement=False, nullable=False),
sa.Column("to_", sa.VARCHAR(length=255), autoincrement=False, nullable=True),
sa.Column("gas", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("gas_price", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("value", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("input", sa.TEXT(), autoincrement=False, nullable=False),
sa.Column("nonce", sa.BIGINT(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint("transaction_hash", name="rollup_transactions_pkey"),
)
# ### end Alembic commands ###
33 changes: 1 addition & 32 deletions backend/database_handler/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class Transactions(Base):
r: Mapped[Optional[int]] = mapped_column(Integer)
s: Mapped[Optional[int]] = mapped_column(Integer)
v: Mapped[Optional[int]] = mapped_column(Integer)
ghost_contract_address: Mapped[Optional[str]] = mapped_column(String(255))

# Relationship for triggered transactions
triggered_by_hash: Mapped[Optional[str]] = mapped_column(
Expand All @@ -115,38 +116,6 @@ class Transactions(Base):
)


class RollupTransactions(Base):
__tablename__ = "rollup_transactions"
__table_args__ = (
PrimaryKeyConstraint("transaction_hash", name="rollup_transactions_pkey"),
)

transaction_hash: Mapped[str] = mapped_column(
String(66), primary_key=True, unique=True
)
from_: Mapped[str] = mapped_column(
String(255),
)
to_: Mapped[Optional[dict]] = mapped_column(
String(255),
)
gas: Mapped[int] = mapped_column(
Integer,
)
gas_price: Mapped[int] = mapped_column(
Integer,
)
value: Mapped[Optional[int]] = mapped_column(
Integer,
)
input: Mapped[str] = mapped_column(
Text,
)
nonce: Mapped[int] = mapped_column(
BigInteger,
)


class Validators(Base):
__tablename__ = "validators"
__table_args__ = (
Expand Down
126 changes: 105 additions & 21 deletions backend/database_handler/transactions_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from enum import Enum
import rlp

from .models import Transactions, RollupTransactions
from .models import Transactions
from sqlalchemy.orm import Session
from sqlalchemy import or_, and_

Expand All @@ -11,6 +11,10 @@
import json
import base64
import time
from backend.domain.types import TransactionType
from web3 import Web3
from backend.database_handler.contract_snapshot import ContractSnapshot
import os


class TransactionAddressFilter(Enum):
Expand All @@ -26,6 +30,11 @@ def __init__(
):
self.session = session

# Connect to Hardhat Network
port = os.environ.get("HARDHATPORT")
hardhat_url = f"http://hardhat:{port}"
self.web3 = Web3(Web3.HTTPProvider(hardhat_url))

@staticmethod
def _parse_transaction_data(transaction_data: Transactions) -> dict:
return {
Expand All @@ -49,6 +58,7 @@ def _parse_transaction_data(transaction_data: Transactions) -> dict:
transaction.hash
for transaction in transaction_data.triggered_transactions
],
"ghost_contract_address": transaction_data.ghost_contract_address,
}

@staticmethod
Expand Down Expand Up @@ -128,6 +138,64 @@ def insert_transaction(
from_address, to_address, data, value, type, nonce
)

if type == TransactionType.DEPLOY_CONTRACT.value:
# Hardhat account
account = self.web3.eth.accounts[0]
private_key = os.environ.get("HARDHAT_PRIVATE_KEY")

# Ghost contract
# Read contract ABI and bytecode from compiled contract
contract_file = os.path.join(
os.getcwd(),
"hardhat/artifacts/contracts/GhostContract.sol/GhostContract.json",
)

with open(contract_file, "r") as f:
contract_json = json.loads(f.read())
abi = contract_json["abi"]
bytecode = contract_json["bytecode"]

# Create the contract instance
contact = self.web3.eth.contract(abi=abi, bytecode=bytecode)
kstroobants marked this conversation as resolved.
Show resolved Hide resolved

# Build the transaction
gas_estimate = self.web3.eth.estimate_gas(
contact.constructor().build_transaction(
{
"from": account,
"nonce": self.web3.eth.get_transaction_count(account),
"gasPrice": 0,
}
)
)
transaction = contact.constructor().build_transaction(
{
"from": account,
"nonce": self.web3.eth.get_transaction_count(account),
"gas": gas_estimate,
"gasPrice": 0,
}
)

# Sign the transaction
signed_tx = self.web3.eth.account.sign_transaction(
transaction, private_key=private_key
)

# Send the transaction
tx_hash = self.web3.eth.send_raw_transaction(signed_tx.raw_transaction)

# Wait for the transaction receipt
receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
ghost_contract_address = receipt.contractAddress

elif type == TransactionType.RUN_CONTRACT.value:
genlayer_contract_address = to_address
contract_snapshot = ContractSnapshot(
genlayer_contract_address, self.session
)
ghost_contract_address = contract_snapshot.ghost_contract_address

new_transaction = Transactions(
hash=transaction_hash,
from_address=from_address,
Expand All @@ -150,6 +218,7 @@ def insert_transaction(
if triggered_by_hash
else None
),
ghost_contract_address=ghost_contract_address,
)

self.session.add(new_transaction)
Expand Down Expand Up @@ -190,29 +259,44 @@ def create_rollup_transaction(self, transaction_hash: str):
transaction = (
self.session.query(Transactions).filter_by(hash=transaction_hash).one()
)
rollup_input_data = self._transaction_data_to_str(
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")

gas_estimate = self.web3.eth.estimate_gas(
{
"from": account,
"to": transaction.ghost_contract_address,
"value": transaction.value,
"data": rollup_input_data,
}
)
rollup_nonce = int(time.time() * 1000)
rollup_transaction_hash = self._generate_transaction_hash(
transaction.from_address,
transaction.to_address,
rollup_input_data,
transaction.value,
0,
rollup_nonce,
)
rollup_transaction_record = RollupTransactions(
transaction_hash=rollup_transaction_hash,
from_=transaction.from_address,
to_=transaction.to_address,
gas=0,
gas_price=0,
value=transaction.value,
input=rollup_input_data,
nonce=rollup_nonce,

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
)
self.session.add(rollup_transaction_record)
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)

def get_transaction_count(self, address: str) -> int:
count = (
Expand Down
3 changes: 3 additions & 0 deletions backend/domain/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class Transaction:
leader_only: bool = (
False # Flag to indicate if this transaction should be processed only by the leader. Used for fast and cheap execution of transactions.
)
ghost_contract_address: str | None = None

def to_dict(self):
return {
Expand All @@ -97,6 +98,7 @@ def to_dict(self):
"s": self.s,
"v": self.v,
"leader_only": self.leader_only,
"ghost_contract_address": self.ghost_contract_address,
}


Expand All @@ -117,4 +119,5 @@ def transaction_from_dict(input: dict) -> Transaction:
s=input.get("s"),
v=input.get("v"),
leader_only=input.get("leader_only", False),
ghost_contract_address=input.get("ghost_contract_address"),
)
3 changes: 2 additions & 1 deletion backend/protocol_rpc/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ sentence-transformers==3.1.1
Flask-SQLAlchemy==3.1.1
jsf==0.11.2
jsonschema==4.23.0
loguru==0.7.2
loguru==0.7.2
web3==7.5.0
Loading