Skip to content

Commit 0009bcf

Browse files
feat: consensus appeals use wrong contract state and restore contract state after appeal successful (#799)
* feat: add rollup transaction db table * fix: drop audit table, change id and nonce of rollup, only make one finalized rollup transaction * create rollup transaction for every validator * add function to mock in consensus test * feat: add appeal window, accepted queue, basic test * fix: old tests were stuck on accepted state, add usage of thread * refactor consensus into states * feat: added appeal flow when transaction is accepted including the loop when appeal failed and succeeded * fix: adding tests for appeals and fixing minor bugs * refactor: merge main and PR #573 into this branch * feat: add appeal_failed in db, select new validators when appealed based on formula * docs: cleanup consensus mechanism base file * test: checking the number of validators for different appeals * feat: adding appeal window to undetermined state * feat: change timestamp_accepted and add appeal_undetermined in database * feat: undetermined to pending, activate frontend button in undetermined state and add button also to modal * feat: leader only has no appeal button and no appeal window * feat: implement the state transitions to process the appeal - Add N+2 validators, remove leader - Use latest data of transaction when in pending state and not the old one from the crawler - Write consensus data before setting status to have it updated in frontend when going to transaction info modal - Do not deploy contract when transaction was in the undetermined state * test: add test for leader appeals * refactor: merge 593-appeals-add-validators-when-appealed into 604-appeals-implement-sequential-appeals-fail * refactor: merging changed file permissions * fix: appealing a write method gave a KeyError because of wrong conversation of transaction * docs: update transaction_processor argument description * fix: all appeals disagreed because of pending_transactions type * refactor: undo change because of merge * fix: do not reset finality window when leader appeal failed, leader_only check in appeal_window * fix: set value in database migration file * fix: add leader_only check for modal appeal button, comment out modal appeal button * fix: do not show appeal button when finality window is finished, checking on finalized state gave a small delay in showing when appeal failed and finality window was exceeded * fix: 2nd tx should not finish before 1st tx * feat: add stop event async tasks, empty pending queue, rollback newer transactions to pending * refactor: remove print statements wrt queue * fix: keep the same validator set when tx is rollbacked * fix: we do not emit messages when the transaction goes from undetermined to finalized * fix: complete merge * fix: redirect leader appeal is processed by appeal queue, not pending queue * fix: test increase wait time to get to finalized * fix: add rollback to pending of newer transactions for leader appeals * fix: use appeal property of the transaction in frontend * fix: comment out appeal buttons * feat: contract contains accepted and finalized state * fix: update old version of state of an existing contract, use accepted contract state in encoded_state for _SnapshotView reads and writes * feat:restore contract state after validator appeal success * fix: reusing validators after rollback clean up * feat: add contractsnapshot to transaction, leader and validators use it inclusing for appeals, rollback restores the state with it as it is the state before applying the write method * fix: remove print statement * fix: leader appeal should keep the contract_snapshot * refactor: clean up prints * refactor: clean up prints * fix: typo in test_helpers for consensus * fix: resolve consensus base test fails * fix: check status typo * fix: update consensus test * fix: update test_get_highest_timestamp * fix: add activated to status match check in consensus test * fix: add none check when transforming to dict for contract_snapshot * fix: increase timings for failed test * fix: put contract_snapshot to none in rollback function; set contract_snapshot in accepted state intead of proposing state * Trigger GitHub Actions workflow * fix: remove temporary code testing --------- Co-authored-by: Cristiam Da Silva <cristiam86@gmail.com>
1 parent cbf2dd7 commit 0009bcf

File tree

8 files changed

+196
-42
lines changed

8 files changed

+196
-42
lines changed

backend/consensus/base.py

+71-41
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def contract_snapshot_factory(
116116
ret.balance = transaction.value or 0
117117
ret.states = {"accepted": {}, "finalized": {}}
118118
ret.encoded_state = ret.states["accepted"]
119+
ret.ghost_contract_address = transaction.ghost_contract_address
119120
return ret
120121

121122
# Return a ContractSnapshot instance for an existing contract
@@ -1146,6 +1147,23 @@ async def process_validator_appeal(
11461147
context.msg_handler,
11471148
)
11481149

1150+
# Get the previous state of the contract
1151+
previous_contact_state = (
1152+
context.transaction.contract_snapshot.encoded_state
1153+
)
1154+
1155+
# Restore the contract state
1156+
if previous_contact_state:
1157+
# Get the contract snapshot for the transaction's target address
1158+
leaders_contract_snapshot = context.contract_snapshot_factory(
1159+
context.transaction.to_address
1160+
)
1161+
1162+
# Update the contract state with the previous state
1163+
leaders_contract_snapshot.update_contract_state(
1164+
accepted_state=previous_contact_state
1165+
)
1166+
11491167
# Transaction will be picked up by _crawl_snapshot
11501168
break
11511169
state = next_state
@@ -1178,6 +1196,11 @@ def rollback_transactions(self, context: TransactionContext):
11781196
context.msg_handler,
11791197
)
11801198

1199+
# Reset the contract snapshot for the transaction
1200+
context.transactions_processor.set_transaction_contract_snapshot(
1201+
future_transaction["hash"], None
1202+
)
1203+
11811204
# Start the queue loop again
11821205
self.start_pending_queue_task(address)
11831206

@@ -1655,6 +1678,7 @@ async def handle(self, context):
16551678
)
16561679

16571680
if context.transaction.appealed:
1681+
16581682
# Update the consensus results with all new votes and validators
16591683
context.consensus_data.votes = (
16601684
context.transaction.consensus_data.votes | context.votes
@@ -1779,7 +1803,6 @@ async def handle(self, context):
17791803
context.transactions_processor.set_transaction_appeal(
17801804
context.transaction.hash, False
17811805
)
1782-
context.transaction.appealed = False
17831806

17841807
# Set the transaction result
17851808
context.transactions_processor.set_transaction_result(
@@ -1824,51 +1847,59 @@ async def handle(self, context):
18241847
)
18251848
)
18261849

1827-
# Update contract state
18281850
# Retrieve the leader's receipt from the consensus data
18291851
leader_receipt = context.consensus_data.leader_receipt
18301852

1831-
# Get the contract snapshot for the transaction's target address
1832-
leaders_contract_snapshot = context.contract_snapshot_supplier()
1833-
1834-
# Do not deploy the contract if the execution failed
1835-
if leader_receipt.execution_result == ExecutionResultStatus.SUCCESS:
1853+
# Do not deploy or update the contract if validator appeal failed
1854+
if not context.transaction.appealed:
18361855
# Get the contract snapshot for the transaction's target address
18371856
leaders_contract_snapshot = context.contract_snapshot_supplier()
18381857

1839-
# Register contract if it is a new contract
1840-
if context.transaction.type == TransactionType.DEPLOY_CONTRACT:
1841-
new_contract = {
1842-
"id": context.transaction.data["contract_address"],
1843-
"data": {
1844-
"state": {
1845-
"accepted": leader_receipt.contract_state,
1846-
"finalized": {},
1847-
},
1848-
"code": context.transaction.data["contract_code"],
1849-
"ghost_contract_address": context.transaction.ghost_contract_address,
1850-
},
1851-
}
1852-
leaders_contract_snapshot.register_contract(new_contract)
1858+
# Set the contract snapshot for the transaction for a future rollback
1859+
context.transactions_processor.set_transaction_contract_snapshot(
1860+
context.transaction.hash, leaders_contract_snapshot.to_dict()
1861+
)
18531862

1854-
# Send a message indicating successful contract deployment
1855-
context.msg_handler.send_message(
1856-
LogEvent(
1857-
"deployed_contract",
1858-
EventType.SUCCESS,
1859-
EventScope.GENVM,
1860-
"Contract deployed",
1861-
new_contract,
1862-
transaction_hash=context.transaction.hash,
1863+
# Do not deploy or update the contract if the execution failed
1864+
if leader_receipt.execution_result == ExecutionResultStatus.SUCCESS:
1865+
# Register contract if it is a new contract
1866+
if context.transaction.type == TransactionType.DEPLOY_CONTRACT:
1867+
new_contract = {
1868+
"id": context.transaction.data["contract_address"],
1869+
"data": {
1870+
"state": {
1871+
"accepted": leader_receipt.contract_state,
1872+
"finalized": {},
1873+
},
1874+
"code": context.transaction.data["contract_code"],
1875+
"ghost_contract_address": context.transaction.ghost_contract_address,
1876+
},
1877+
}
1878+
leaders_contract_snapshot.register_contract(new_contract)
1879+
1880+
# Send a message indicating successful contract deployment
1881+
context.msg_handler.send_message(
1882+
LogEvent(
1883+
"deployed_contract",
1884+
EventType.SUCCESS,
1885+
EventScope.GENVM,
1886+
"Contract deployed",
1887+
new_contract,
1888+
transaction_hash=context.transaction.hash,
1889+
)
18631890
)
1864-
)
1865-
# Update contract state if it is an existing contract
1866-
else:
1867-
leaders_contract_snapshot.update_contract_state(
1868-
accepted_state=leader_receipt.contract_state
1891+
# Update contract state if it is an existing contract
1892+
else:
1893+
leaders_contract_snapshot.update_contract_state(
1894+
accepted_state=leader_receipt.contract_state
1895+
)
1896+
1897+
_emit_transactions(
1898+
context, leader_receipt.pending_transactions, "accepted"
18691899
)
18701900

1871-
_emit_transactions(context, leader_receipt.pending_transactions, "accepted")
1901+
else:
1902+
context.transaction.appealed = False
18721903

18731904
# Set the transaction appeal undetermined status to false and return appeal status
18741905
if context.transaction.appeal_undetermined:
@@ -1991,15 +2022,14 @@ async def handle(self, context):
19912022
# Retrieve the leader's receipt from the consensus data
19922023
leader_receipt = context.transaction.consensus_data.leader_receipt
19932024

1994-
# Get the contract snapshot for the transaction's target address
1995-
leaders_contract_snapshot = context.contract_snapshot_factory(
1996-
context.transaction.to_address
1997-
)
1998-
19992025
# Update contract state
20002026
if (context.transaction.status == TransactionStatus.ACCEPTED) and (
20012027
leader_receipt.execution_result == ExecutionResultStatus.SUCCESS
20022028
):
2029+
# Get the contract snapshot for the transaction's target address
2030+
leaders_contract_snapshot = context.contract_snapshot_factory(
2031+
context.transaction.to_address
2032+
)
20032033
leaders_contract_snapshot.update_contract_state(
20042034
finalized_state=leader_receipt.contract_state
20052035
)

backend/database_handler/contract_snapshot.py

+28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# database_handler/contract_snapshot.py
22
from .models import CurrentState
33
from sqlalchemy.orm import Session
4+
from typing import Optional
45

56

67
# TODO: should ContractSnapshot be a dataclass with just the contract data? Snapshots shouldn't be allowed to be modified, so it doesn't make sense to modify the database
@@ -45,6 +46,33 @@ def __init__(self, contract_address: str | None, session: Session):
4546
else None
4647
)
4748

49+
def to_dict(self):
50+
return {
51+
"contract_address": (
52+
self.contract_address if self.contract_address else None
53+
),
54+
"contract_code": self.contract_code if self.contract_code else None,
55+
"encoded_state": self.encoded_state if self.encoded_state else {},
56+
"states": self.states if self.states else {"accepted": {}, "finalized": {}},
57+
"ghost_contract_address": (
58+
self.ghost_contract_address if self.ghost_contract_address else None
59+
),
60+
}
61+
62+
@classmethod
63+
def from_dict(cls, input: dict | None) -> Optional["ContractSnapshot"]:
64+
if input:
65+
instance = cls.__new__(cls)
66+
instance.session = None
67+
instance.contract_address = input.get("contract_address", None)
68+
instance.contract_code = input.get("contract_code", None)
69+
instance.encoded_state = input.get("encoded_state", {})
70+
instance.states = input.get("states", {"accepted": {}, "finalized": {}})
71+
instance.ghost_contract_address = input.get("ghost_contract_address", None)
72+
return instance
73+
else:
74+
return None
75+
4876
def _load_contract_account(self) -> CurrentState:
4977
"""Load and return the current state of the contract from the database."""
5078
result = (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""contract_snapshot
2+
3+
Revision ID: 15fde6faebaf
4+
Revises: d932a5fef8b1
5+
Create Date: 2025-01-06 10:42:19.972610
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
from sqlalchemy.dialects import postgresql
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "15fde6faebaf"
17+
down_revision: Union[str, None] = "d932a5fef8b1"
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+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.add_column(
25+
"transactions",
26+
sa.Column(
27+
"contract_snapshot", postgresql.JSONB(astext_type=sa.Text()), nullable=True
28+
),
29+
)
30+
# ### end Alembic commands ###
31+
32+
33+
def downgrade() -> None:
34+
# ### commands auto generated by Alembic - please adjust! ###
35+
op.drop_column("transactions", "contract_snapshot")
36+
# ### end Alembic commands ###

backend/database_handler/models.py

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class Transactions(Base):
100100
consensus_history: Mapped[Optional[dict]] = mapped_column(JSONB)
101101
timestamp_appeal: Mapped[Optional[int]] = mapped_column(BigInteger)
102102
appeal_processing_time: Mapped[Optional[int]] = mapped_column(Integer)
103+
contract_snapshot: Mapped[Optional[dict]] = mapped_column(JSONB)
103104

104105
# Relationship for triggered transactions
105106
triggered_by_hash: Mapped[Optional[str]] = mapped_column(

backend/database_handler/transactions_processor.py

+19
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def _parse_transaction_data(transaction_data: Transactions) -> dict:
7070
"consensus_history": transaction_data.consensus_history,
7171
"timestamp_appeal": transaction_data.timestamp_appeal,
7272
"appeal_processing_time": transaction_data.appeal_processing_time,
73+
"contract_snapshot": transaction_data.contract_snapshot,
7374
}
7475

7576
@staticmethod
@@ -182,6 +183,7 @@ def insert_transaction(
182183
consensus_history={},
183184
timestamp_appeal=None,
184185
appeal_processing_time=0,
186+
contract_snapshot=None,
185187
)
186188

187189
self.session.add(new_transaction)
@@ -483,3 +485,20 @@ def reset_transaction_appeal_processing_time(self, transaction_hash: str):
483485
)
484486
transaction.appeal_processing_time = 0
485487
self.session.commit()
488+
489+
def set_transaction_contract_snapshot(
490+
self, transaction_hash: str, contract_snapshot: dict | None
491+
):
492+
transaction = (
493+
self.session.query(Transactions).filter_by(hash=transaction_hash).one()
494+
)
495+
transaction.contract_snapshot = contract_snapshot
496+
self.session.commit()
497+
498+
def get_transaction_contract_snapshot(
499+
self, transaction_hash: str
500+
) -> ContractSnapshot | None:
501+
transaction = (
502+
self.session.query(Transactions).filter_by(hash=transaction_hash).one()
503+
)
504+
return ContractSnapshot.from_dict(transaction.contract_snapshot)

backend/domain/types.py

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from backend.database_handler.models import TransactionStatus
1010
from backend.database_handler.types import ConsensusData
11+
from backend.database_handler.contract_snapshot import ContractSnapshot
1112

1213

1314
@dataclass()
@@ -89,6 +90,7 @@ class Transaction:
8990
consensus_history: dict = field(default_factory=dict)
9091
timestamp_appeal: int | None = None
9192
appeal_processing_time: int = 0
93+
contract_snapshot: ContractSnapshot | None = None
9294

9395
def to_dict(self):
9496
return {
@@ -118,6 +120,9 @@ def to_dict(self):
118120
"consensus_history": self.consensus_history,
119121
"timestamp_appeal": self.timestamp_appeal,
120122
"appeal_processing_time": self.appeal_processing_time,
123+
"contract_snapshot": (
124+
self.contract_snapshot.to_dict() if self.contract_snapshot else None
125+
),
121126
}
122127

123128
@classmethod
@@ -149,4 +154,7 @@ def from_dict(cls, input: dict) -> "Transaction":
149154
consensus_history=input.get("consensus_history"),
150155
timestamp_appeal=input.get("timestamp_appeal"),
151156
appeal_processing_time=input.get("appeal_processing_time", 0),
157+
contract_snapshot=ContractSnapshot.from_dict(
158+
input.get("contract_snapshot")
159+
),
152160
)

tests/unit/consensus/test_base.py

+2
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,8 @@ async def test_exec_accepted_appeal_fail_three_times(consensus_algorithm):
793793
transactions_processor = TransactionsProcessorMock(
794794
[transaction_to_dict(transaction)]
795795
)
796+
consensus_algorithm.consensus_sleep_time = 5
797+
consensus_algorithm.finality_window_time = 15
796798

797799
def get_vote():
798800
return Vote.AGREE

0 commit comments

Comments
 (0)