From 5d8d1ce5d74308dc772458a13c81bfff3ce56e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Nem=C5=A1e?= Date: Mon, 8 Jun 2026 18:59:13 +0100 Subject: [PATCH 01/12] fix: serialize fee payloads for studio estimates (#88) --- genlayer_py/contracts/actions.py | 17 ++++++- genlayer_py/transactions/fees.py | 28 +++++++++- tests/unit/contracts/test_contract_actions.py | 51 +++++++++++++++++-- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/genlayer_py/contracts/actions.py b/genlayer_py/contracts/actions.py index f2f62fc..3f27908 100644 --- a/genlayer_py/contracts/actions.py +++ b/genlayer_py/contracts/actions.py @@ -442,7 +442,22 @@ def _transaction_fees_to_rpc( } if normalized["fee_value"] is not None: rpc_fees["feeValue"] = normalized["fee_value"] - return rpc_fees + return _json_safe_rpc_value(rpc_fees) + + +def _json_safe_rpc_value(value): + if isinstance(value, (bytes, bytearray)): + return "0x" + bytes(value).hex() + if isinstance(value, list): + return [_json_safe_rpc_value(item) for item in value] + if isinstance(value, tuple): + return [_json_safe_rpc_value(item) for item in value] + if isinstance(value, dict): + return { + key: _json_safe_rpc_value(item) + for key, item in value.items() + } + return value def _encode_submit_appeal_data( diff --git a/genlayer_py/transactions/fees.py b/genlayer_py/transactions/fees.py index e05c529..88b9275 100644 --- a/genlayer_py/transactions/fees.py +++ b/genlayer_py/transactions/fees.py @@ -131,6 +131,7 @@ class TransactionFeeEstimate(TypedDict, total=False): fee_value: int policy: FeePolicyQuote observed: dict[str, int] + simulation: Any class SimulationFeeEstimateOptions(FeeEstimateOptions, total=False): @@ -193,6 +194,7 @@ class NormalizedTransactionFees(TypedDict): DEFAULT_LEADER_TIMEUNITS_ALLOCATION = 100 DEFAULT_VALIDATOR_TIMEUNITS_ALLOCATION = 200 DEFAULT_TRANSACTION_EXECUTION_BUDGET_PER_ROUND = 500_000 +DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM = 10_000 MIN_RECEIPT_BYTES = 512 DEFAULT_RECEIPT_SLOTS_CHANGED = 7 DEFAULT_INTRINSIC_GAS = 21_000 @@ -967,6 +969,7 @@ def transaction_fee_estimate_from_studio_estimate( "feeValue": to_uint(fee_value, "recommendedPreset.feeValue"), "fee_value": to_uint(fee_value, "recommendedPreset.feeValue"), "policy": policy, + "simulation": simulation, "observed": observed_simulation_fee_usage( {"simulation": simulation}, policy, @@ -1058,17 +1061,38 @@ def build_estimated_fees_distribution( "priceCapHeadroomBps", DEFAULT_PRICE_CAP_HEADROOM_BPS, ) - execution_budget_default = _default_execution_budget_per_round(policy) + base_execution_budget_default = _default_execution_budget_per_round(policy) total_message_fees = _get(options, "totalMessageFees", "total_message_fees") message_allocations = _get(options, "messageAllocations", "message_allocations") + normalized_message_allocations = ( + normalize_message_fee_allocations(message_allocations) + if message_allocations is not None + else None + ) if total_message_fees is None and message_allocations is not None: total_message_fees = sum( allocation["budget"] - for allocation in normalize_message_fee_allocations(message_allocations) + for allocation in normalized_message_allocations or [] if allocation["messageType"] == int(MessageType.External) or allocation["parentIndex"] == MESSAGE_ALLOCATION_ROOT_PARENT_INDEX ) + emits_messages = ( + ( + normalized_message_allocations is not None + and len(normalized_message_allocations) > 0 + ) + or ( + total_message_fees is not None + and to_uint(total_message_fees, "totalMessageFees") > 0 + ) + ) + execution_budget_default = ( + base_execution_budget_default + + policy["receiptGasPrice"] * DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM + if emits_messages + else base_execution_budget_default + ) return create_fees_distribution( { diff --git a/tests/unit/contracts/test_contract_actions.py b/tests/unit/contracts/test_contract_actions.py index cc58d44..12c2e22 100644 --- a/tests/unit/contracts/test_contract_actions.py +++ b/tests/unit/contracts/test_contract_actions.py @@ -28,6 +28,9 @@ ) +DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM = 10_000 + + ADD_TRANSACTION_ABI_V5 = [ { "type": "function", @@ -385,6 +388,10 @@ def test_simulate_write_contract_passes_fee_policy_and_value_to_sim_call(): MessageType.Internal ) assert request_params["fees"]["messageAllocations"][0]["budget"] == 5 + assert ( + request_params["fees"]["messageAllocations"][0]["callKey"] + == "0x" + "00" * 32 + ) def test_encode_add_transaction_uses_v5_signature_when_abi_has_5_inputs(): @@ -584,13 +591,42 @@ def test_build_estimated_fees_distribution_adds_caps_and_message_bucket(): assert distribution["leaderTimeunitsAllocation"] == 100 assert distribution["validatorTimeunitsAllocation"] == 200 - assert distribution["executionBudgetPerRound"] == 500_000 + assert distribution["executionBudgetPerRound"] == ( + 500_000 + 30 * DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM + ) assert distribution["totalMessageFees"] == 80 assert distribution["maxPriceGenPerTimeUnit"] == 12 assert distribution["storageFeeMaxGasPrice"] == 24 assert distribution["receiptFeeMaxGasPrice"] == 36 +def test_build_estimated_fees_distribution_preserves_explicit_execution_budget_with_messages(): + policy = { + "enabled": True, + "genPerTimeUnit": 10, + "storageUnitPrice": 20, + "receiptGasPrice": 30, + "executionBudgetFloor": 1_234, + } + + distribution = build_estimated_fees_distribution( + { + "executionBudgetPerRound": 42, + "messageAllocations": [ + { + "messageType": MessageType.Internal, + "recipient": RECIPIENT, + "budget": 50, + "feeParams": "0x1234", + }, + ], + }, + policy, + ) + + assert distribution["executionBudgetPerRound"] == 42 + + def test_calculate_local_round_fees_matches_consensus_initial_round(): distribution = create_fees_distribution( { @@ -702,7 +738,10 @@ def test_estimate_transaction_fees_derives_message_bucket_from_allocations(): ) assert estimate["distribution"]["totalMessageFees"] == 80 - assert estimate["feeValue"] == 9_196_840 + assert estimate["distribution"]["executionBudgetPerRound"] == ( + 9_185_760 + 30 * DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM + ) + assert estimate["feeValue"] == 9_496_840 assert estimate["messageAllocations"] == message_allocations assert estimate["message_allocations"] == message_allocations @@ -882,12 +921,18 @@ def make_request(method, params): ) request_params = sim_call.kwargs["params"][0] assert request_params["value"] == hex(7) - assert request_params["fees"]["feeValue"] == 511_110 + assert request_params["fees"]["feeValue"] == 521_110 assert request_params["fees"]["distribution"]["totalMessageFees"] == 110 assert request_params["fees"]["messageAllocations"][0]["budget"] == 110 + assert ( + request_params["fees"]["messageAllocations"][0]["callKey"] + == "0x" + "00" * 32 + ) assert estimate["observed"]["recommendedExecutionBudgetPerRound"] == 602_117 assert estimate["observed"]["messageFeeBudget"] == 110 assert estimate["observed"]["messageFeeConsumed"] == 50 + assert estimate["simulation"]["feeAccounting"]["message_fee_budget"] == "110" + assert estimate["simulation"]["feeReport"]["totalEstimatedFee"] == "501664" assert estimate["distribution"]["executionBudgetPerRound"] == 700_000 assert estimate["distribution"]["totalMessageFees"] == 110 assert estimate["messageAllocations"][0]["budget"] == 110 From 61dd2c0c3b4f0a83771aeb030a0361a141f51e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Nem=C5=A1e?= Date: Tue, 9 Jun 2026 23:41:27 +0100 Subject: [PATCH 02/12] fix: include execution budget floor in fee estimates (#89) --- genlayer_py/transactions/fees.py | 2 ++ tests/unit/contracts/test_contract_actions.py | 22 +++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/genlayer_py/transactions/fees.py b/genlayer_py/transactions/fees.py index 88b9275..9916866 100644 --- a/genlayer_py/transactions/fees.py +++ b/genlayer_py/transactions/fees.py @@ -194,6 +194,7 @@ class NormalizedTransactionFees(TypedDict): DEFAULT_LEADER_TIMEUNITS_ALLOCATION = 100 DEFAULT_VALIDATOR_TIMEUNITS_ALLOCATION = 200 DEFAULT_TRANSACTION_EXECUTION_BUDGET_PER_ROUND = 500_000 +DEFAULT_TRANSACTION_EXECUTION_GAS = 100_000_000 DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM = 10_000 MIN_RECEIPT_BYTES = 512 DEFAULT_RECEIPT_SLOTS_CHANGED = 7 @@ -705,6 +706,7 @@ def _default_execution_budget_per_round(policy: FeePolicyQuote) -> int: return max( DEFAULT_TRANSACTION_EXECUTION_BUDGET_PER_ROUND, policy["executionBudgetFloor"], + policy["receiptGasPrice"] * DEFAULT_TRANSACTION_EXECUTION_GAS, ) diff --git a/tests/unit/contracts/test_contract_actions.py b/tests/unit/contracts/test_contract_actions.py index 12c2e22..7887bed 100644 --- a/tests/unit/contracts/test_contract_actions.py +++ b/tests/unit/contracts/test_contract_actions.py @@ -592,7 +592,7 @@ def test_build_estimated_fees_distribution_adds_caps_and_message_bucket(): assert distribution["leaderTimeunitsAllocation"] == 100 assert distribution["validatorTimeunitsAllocation"] == 200 assert distribution["executionBudgetPerRound"] == ( - 500_000 + 30 * DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM + 3_000_000_000 + 30 * DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM ) assert distribution["totalMessageFees"] == 80 assert distribution["maxPriceGenPerTimeUnit"] == 12 @@ -683,8 +683,8 @@ def test_estimate_transaction_fees_uses_studio_fee_config(): }, } ) - assert estimate["distribution"]["executionBudgetPerRound"] == 9_185_760 - assert estimate["feeValue"] == 9_196_760 + assert estimate["distribution"]["executionBudgetPerRound"] == 3_000_000_000 + assert estimate["feeValue"] == 3_000_011_000 def test_estimate_transaction_fees_derives_message_bucket_from_allocations(): @@ -739,9 +739,9 @@ def test_estimate_transaction_fees_derives_message_bucket_from_allocations(): assert estimate["distribution"]["totalMessageFees"] == 80 assert estimate["distribution"]["executionBudgetPerRound"] == ( - 9_185_760 + 30 * DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM + 3_000_000_000 + 30 * DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM ) - assert estimate["feeValue"] == 9_496_840 + assert estimate["feeValue"] == 3_000_311_080 assert estimate["messageAllocations"] == message_allocations assert estimate["message_allocations"] == message_allocations @@ -866,7 +866,7 @@ def make_request(method, params): "leaderTimeunitsAllocation": "100", "validatorTimeunitsAllocation": "200", "appealRounds": "0", - "executionBudgetPerRound": "700000", + "executionBudgetPerRound": "100000000", "executionConsumed": "0", "totalMessageFees": "110", "rotations": ["0"], @@ -885,7 +885,7 @@ def make_request(method, params): "feeParams": fee_params, } ], - "feeValue": "711110", + "feeValue": "100011110", }, } } @@ -921,22 +921,22 @@ def make_request(method, params): ) request_params = sim_call.kwargs["params"][0] assert request_params["value"] == hex(7) - assert request_params["fees"]["feeValue"] == 521_110 + assert request_params["fees"]["feeValue"] == 100_021_110 assert request_params["fees"]["distribution"]["totalMessageFees"] == 110 assert request_params["fees"]["messageAllocations"][0]["budget"] == 110 assert ( request_params["fees"]["messageAllocations"][0]["callKey"] == "0x" + "00" * 32 ) - assert estimate["observed"]["recommendedExecutionBudgetPerRound"] == 602_117 + assert estimate["observed"]["recommendedExecutionBudgetPerRound"] == 100_000_000 assert estimate["observed"]["messageFeeBudget"] == 110 assert estimate["observed"]["messageFeeConsumed"] == 50 assert estimate["simulation"]["feeAccounting"]["message_fee_budget"] == "110" assert estimate["simulation"]["feeReport"]["totalEstimatedFee"] == "501664" - assert estimate["distribution"]["executionBudgetPerRound"] == 700_000 + assert estimate["distribution"]["executionBudgetPerRound"] == 100_000_000 assert estimate["distribution"]["totalMessageFees"] == 110 assert estimate["messageAllocations"][0]["budget"] == 110 - assert estimate["feeValue"] == 711_110 + assert estimate["feeValue"] == 100_011_110 def test_estimate_transaction_fees_from_simulation_preserves_mode2_allocations(): From bc04db8999b86979273408511c90c9a89620ff49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Nem=C5=A1e?= Date: Wed, 10 Jun 2026 13:07:26 +0100 Subject: [PATCH 03/12] feat(fees)!: estimation correctness, wait-for-decided semantics, v0.6 parity with genlayer-js (#90) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(fees)!: estimation correctness, wait-for-decided semantics, v0.6 parity with genlayer-js Mirrors genlayer-js feat/v06-fee-estimation-rework (same designer rulings): - effective receipt gas price = max(quoteGasPrice(), eth_gasPrice); zero price on an enabled policy raises instead of producing a zero cap; effective execution budget floor = max(on-chain view, price x 306,192 gas local recompute) - simulation-derived budgets no longer clobbered by the 100M default (floor vs observed x headroom); default constant documented as provisional w/ TODO(data) - wait_until='decided'|'finalized' replaces status (legacy param mapped with one-time DeprecationWarning); new is_successful(tx) helper - enum completeness: status 14 LEADER_REVEALING (fixes a KeyError crash mid-poll), VoteType 3 TIMEOUT / 4 NONDET_DISAGREE, v0.6 ResultType remap - DEPLOY_CALL_KEY = bytes32(1) deploy sentinel; fee revert selector naming in send errors (InsufficientFees/MaxPriceExceeded/ExecutionBudgetExceeded/ BudgetTooLow/RollupBudgetBelowFloor/FeeValueMustBeNonZero) py-specific parity fixes: - asimov chain config gains FeeManager/RoundsStorage/Appeals addresses (parity with js testnetAsimov) - Studio floor-fallback formula unified with js (message-reveal leg included) - appeal_transaction/top_up_and_submit_appeal auto-resolve the minimum bond when value is omitted (previously defaulted to 0 = guaranteed on-chain revert); top_up_fees now requires value * ci: run tests on pushes to dev branches tests.yml had no push trigger at all — merges to v0.19-dev got zero post-merge CI. --- .github/workflows/tests.yml | 3 + genlayer_py/chains/testnet_asimov.py | 105 ++++++- genlayer_py/client/genlayer_client.py | 12 +- genlayer_py/contracts/actions.py | 164 +++++++--- genlayer_py/transactions/__init__.py | 11 + genlayer_py/transactions/actions.py | 118 +++++++- genlayer_py/transactions/fees.py | 24 +- genlayer_py/types/transactions.py | 24 +- tests/unit/contracts/test_contract_actions.py | 279 +++++++++++++++++- .../test_wait_for_transaction_receipt.py | 74 ++++- 10 files changed, 737 insertions(+), 77 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5d389c4..216cc65 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,9 @@ name: Tests on: + push: + branches: + - 'v*-dev' workflow_call: pull_request: types: [opened, reopened, synchronize, ready_for_review] diff --git a/genlayer_py/chains/testnet_asimov.py b/genlayer_py/chains/testnet_asimov.py index 794f294..8984a80 100644 --- a/genlayer_py/chains/testnet_asimov.py +++ b/genlayer_py/chains/testnet_asimov.py @@ -1,4 +1,4 @@ -from genlayer_py.types import GenLayerChain, NativeCurrency +from genlayer_py.types import GenLayerChain, NativeCurrency, SimpleContractInfo from genlayer_py.consensus.abi import CONSENSUS_MAIN_ABI, CONSENSUS_DATA_ABI @@ -17,6 +17,103 @@ "bytecode": "", } +FEE_MANAGER_CONTRACT: SimpleContractInfo = { + "address": "0x21737AA4bea8FF12E202BF1BAB23751A95617533", + "abi": [ + { + "type": "function", + "name": "calculateMinAppealBond", + "stateMutability": "view", + "inputs": [ + {"name": "_txId", "type": "bytes32"}, + {"name": "_round", "type": "uint256"}, + {"name": "_status", "type": "uint8"}, + ], + "outputs": [{"name": "totalFeesToPay", "type": "uint256"}], + }, + ], +} + +ROUNDS_STORAGE_CONTRACT: SimpleContractInfo = { + "address": "0x1F595c0D549DE0812F127508ea1039636CFA62Cc", + "abi": [ + { + "type": "function", + "name": "getRoundNumber", + "stateMutability": "view", + "inputs": [{"name": "txId", "type": "bytes32"}], + "outputs": [{"name": "", "type": "uint256"}], + }, + { + "type": "function", + "name": "getRoundData", + "stateMutability": "view", + "inputs": [ + {"name": "txId", "type": "bytes32"}, + {"name": "round", "type": "uint256"}, + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "components": [ + {"name": "round", "type": "uint256"}, + {"name": "leaderIndex", "type": "uint256"}, + {"name": "votesCommitted", "type": "uint256"}, + {"name": "votesRevealed", "type": "uint256"}, + {"name": "appealBond", "type": "uint256"}, + {"name": "rotationsLeft", "type": "uint256"}, + {"name": "result", "type": "uint8"}, + {"name": "roundValidators", "type": "address[]"}, + {"name": "validatorVotes", "type": "uint8[]"}, + {"name": "validatorVotesHash", "type": "bytes32[]"}, + {"name": "validatorResultHash", "type": "bytes32[]"}, + ], + } + ], + }, + { + "type": "function", + "name": "getLastRoundData", + "stateMutability": "view", + "inputs": [{"name": "txId", "type": "bytes32"}], + "outputs": [ + {"name": "round", "type": "uint256"}, + { + "name": "roundData", + "type": "tuple", + "components": [ + {"name": "round", "type": "uint256"}, + {"name": "leaderIndex", "type": "uint256"}, + {"name": "votesCommitted", "type": "uint256"}, + {"name": "votesRevealed", "type": "uint256"}, + {"name": "appealBond", "type": "uint256"}, + {"name": "rotationsLeft", "type": "uint256"}, + {"name": "result", "type": "uint8"}, + {"name": "roundValidators", "type": "address[]"}, + {"name": "validatorVotes", "type": "uint8[]"}, + {"name": "validatorVotesHash", "type": "bytes32[]"}, + {"name": "validatorResultHash", "type": "bytes32[]"}, + ], + }, + ], + }, + ], +} + +APPEALS_CONTRACT: SimpleContractInfo = { + "address": "0x0F739Dd8f5322b9547c7d19a9621BC2ac8DF4089", + "abi": [ + { + "type": "function", + "name": "canAppeal", + "stateMutability": "view", + "inputs": [{"name": "_txId", "type": "bytes32"}], + "outputs": [{"name": "", "type": "bool"}], + }, + ], +} + testnet_asimov: GenLayerChain = GenLayerChain( id=4221, @@ -34,9 +131,9 @@ testnet=True, consensus_main_contract=CONSENSUS_MAIN_CONTRACT, consensus_data_contract=CONSENSUS_DATA_CONTRACT, - fee_manager_contract=None, - rounds_storage_contract=None, - appeals_contract=None, + fee_manager_contract=FEE_MANAGER_CONTRACT, + rounds_storage_contract=ROUNDS_STORAGE_CONTRACT, + appeals_contract=APPEALS_CONTRACT, staking_contract=None, default_number_of_initial_validators=5, default_consensus_max_rotations=3, diff --git a/genlayer_py/client/genlayer_client.py b/genlayer_py/client/genlayer_client.py index aae6275..8445c83 100644 --- a/genlayer_py/client/genlayer_client.py +++ b/genlayer_py/client/genlayer_client.py @@ -4,7 +4,7 @@ from eth_typing import Address, ChecksumAddress, HexStr from eth_account.signers.local import LocalAccount from hexbytes import HexBytes -from typing import AnyStr +from typing import AnyStr, Literal from genlayer_py.types import ( GenLayerChain, TransactionStatus, @@ -302,8 +302,8 @@ def top_up_fees( self, transaction_id: HexStr, distribution: FeesDistributionInput, + value: int, account: Optional[LocalAccount] = None, - value: int = 0, ) -> HexStr: """Deposits additional fee budget for an existing consensus transaction.""" return top_up_fees( @@ -319,7 +319,7 @@ def top_up_and_submit_appeal( transaction_id: HexStr, distribution: FeesDistributionInput, account: Optional[LocalAccount] = None, - value: int = 0, + value: Optional[int] = None, ) -> HexStr: """Deposits appeal fee budget and submits an appeal in one consensus call.""" return top_up_and_submit_appeal( @@ -334,7 +334,8 @@ def top_up_and_submit_appeal( def wait_for_transaction_receipt( self, transaction_hash: _Hash32, - status: TransactionStatus = TransactionStatus.ACCEPTED, + status: Optional[TransactionStatus] = None, + wait_until: Optional[Literal["decided", "finalized"]] = None, interval: int = transaction_config.wait_interval, retries: int = transaction_config.retries, full_transaction: bool = False, @@ -344,6 +345,7 @@ def wait_for_transaction_receipt( self=self, transaction_hash=transaction_hash, status=status, + wait_until=wait_until, interval=interval, retries=retries, full_transaction=full_transaction, @@ -375,7 +377,7 @@ def appeal_transaction( self, transaction_id: HexStr, account: Optional[LocalAccount] = None, - value: int = 0, + value: Optional[int] = None, ): """Appeals a consensus transaction to trigger a new round of validation. Returns the original transaction_id (appeals operate on the same tx).""" diff --git a/genlayer_py/contracts/actions.py b/genlayer_py/contracts/actions.py index 3f27908..b40f9c6 100644 --- a/genlayer_py/contracts/actions.py +++ b/genlayer_py/contracts/actions.py @@ -24,6 +24,13 @@ FeesDistributionInput, FeesDistribution, FEES_DISTRIBUTION_ABI_TYPE, + DEFAULT_BOOTLOADER_OVERHEAD, + DEFAULT_CALLDATA_GAS_PER_BYTE, + DEFAULT_FIXED_PROPOSE_RECEIPT_GAS, + DEFAULT_GAS_PER_CHANGED_SLOT, + DEFAULT_INTRINSIC_GAS, + DEFAULT_RECEIPT_SLOTS_CHANGED, + MIN_RECEIPT_BYTES, NormalizedTransactionFees, SimulationFeeEstimateOptions, TransactionFeeEstimate, @@ -214,7 +221,7 @@ def appeal_transaction( self: GenLayerClient, transaction_id: HexStr, account: Optional[LocalAccount] = None, - value: int = 0, + value: Optional[int] = None, ) -> HexStr: """Appeals a consensus transaction. Returns the original transaction_id. @@ -226,6 +233,7 @@ def appeal_transaction( raise GenLayerError("No account set.") if self.chain.consensus_main_contract is None: raise GenLayerError("Consensus main contract not configured.") + resolved_value = _resolve_appeal_value(self, transaction_id, value) encoded_data = _encode_submit_appeal_data(self=self, transaction_id=transaction_id) @@ -233,7 +241,7 @@ def appeal_transaction( self=self, encoded_data=encoded_data, sender_account=sender_account, - value=value, + value=resolved_value, operation_name="Appeal", ) @@ -244,8 +252,8 @@ def top_up_fees( self: GenLayerClient, transaction_id: HexStr, distribution: FeesDistributionInput, + value: int, account: Optional[LocalAccount] = None, - value: int = 0, ) -> HexStr: """Deposits additional fee budget for an existing consensus transaction. @@ -273,13 +281,14 @@ def top_up_and_submit_appeal( transaction_id: HexStr, distribution: FeesDistributionInput, account: Optional[LocalAccount] = None, - value: int = 0, + value: Optional[int] = None, ) -> HexStr: """Deposits appeal fee budget and submits an appeal in one consensus call. Returns the original GenLayer transaction id, matching appeal_transaction. """ sender_account = account if account is not None else self.local_account + resolved_value = _resolve_appeal_value(self, transaction_id, value) encoded_data = _encode_fee_management_data( self=self, function_name="topUpAndSubmitAppeal", @@ -290,7 +299,7 @@ def top_up_and_submit_appeal( self=self, encoded_data=encoded_data, sender_account=sender_account, - value=value, + value=resolved_value, operation_name="Top up and submit appeal", ) return transaction_id @@ -377,6 +386,20 @@ def get_min_appeal_bond( return fee_contract.functions.calculateMinAppealBond(tx_bytes, round_number, tx_status).call() +def _resolve_appeal_value( + self: GenLayerClient, + transaction_id: HexStr, + value: Optional[int], +) -> int: + if value is not None: + return value + if self.chain.fee_manager_contract is None or self.chain.rounds_storage_contract is None: + raise GenLayerError( + "Cannot auto-resolve appeal bond: fee_manager_contract/rounds_storage_contract not configured for this chain." + ) + return get_min_appeal_bond(self, transaction_id) + + def _to_bytes32(self: GenLayerClient, hex_str: HexStr) -> bytes: """Convert a hex string to bytes32.""" if hex_str.startswith("0x"): @@ -597,20 +620,37 @@ def get_current_fee_policy(self: GenLayerClient) -> FeePolicyQuote: ) gen_per_time_unit = fee_manager_contract.functions.GENPerTimeUnit().call() storage_unit_price = fee_manager_contract.functions.storageUnitPrice().call() - receipt_gas_price = fee_manager_contract.functions.quoteGasPrice().call() + quoted_receipt_gas_price = fee_manager_contract.functions.quoteGasPrice().call() execution_budget_floor = ( fee_manager_contract.functions.messageFeeParamsBudgetFloor().call() ) + enabled = ( + gen_per_time_unit > 0 + or storage_unit_price > 0 + or quoted_receipt_gas_price > 0 + ) + network_receipt_gas_price = self.w3.eth.gas_price if enabled else 0 + receipt_gas_price = max(quoted_receipt_gas_price, network_receipt_gas_price) + if enabled and receipt_gas_price == 0: + raise GenLayerError( + "receipt gas price quoted as zero; refusing to build a zero price cap" + ) + local_execution_budget_floor = receipt_gas_price * ( + DEFAULT_FIXED_PROPOSE_RECEIPT_GAS + + DEFAULT_INTRINSIC_GAS + + DEFAULT_BOOTLOADER_OVERHEAD + + (MIN_RECEIPT_BYTES * DEFAULT_CALLDATA_GAS_PER_BYTE) + + (DEFAULT_RECEIPT_SLOTS_CHANGED * DEFAULT_GAS_PER_CHANGED_SLOT) + ) return { - "enabled": ( - gen_per_time_unit > 0 - or storage_unit_price > 0 - or receipt_gas_price > 0 - ), + "enabled": enabled, "genPerTimeUnit": gen_per_time_unit, "storageUnitPrice": storage_unit_price, "receiptGasPrice": receipt_gas_price, - "executionBudgetFloor": execution_budget_floor, + "executionBudgetFloor": max( + execution_budget_floor, + local_execution_budget_floor, + ), } try: @@ -920,6 +960,46 @@ def _prepare_transaction( return transaction +KNOWN_REVERT_SELECTOR_NAMES = { + "0x8d53e553": "InsufficientFees", + "0xb4132db3": "MaxPriceExceeded", + "0x57df8523": "ExecutionBudgetExceeded", + "0x305e533c": "BudgetTooLow", + "0xa70732ee": "RollupBudgetBelowFloor", + "0x632be5a1": "FeeValueMustBeNonZero", +} + + +def _format_rpc_error(error: Exception) -> str: + parts = [str(error)] + for attr in ("message", "data"): + value = getattr(error, attr, None) + if isinstance(value, str) and value.strip(): + parts.append(value) + args = getattr(error, "args", ()) + for arg in args: + if isinstance(arg, dict): + for key in ("message", "data", "details", "shortMessage"): + value = arg.get(key) + if isinstance(value, str) and value.strip(): + parts.append(value) + elif isinstance(arg, str) and arg.strip(): + parts.append(arg) + + text = " ".join(dict.fromkeys(parts)) + selector_name = next( + ( + name + for selector, name in KNOWN_REVERT_SELECTOR_NAMES.items() + if selector in text + ), + None, + ) + if selector_name and selector_name not in text: + return f"{text} ({selector_name})" + return text + + def _send_consensus_call( self: GenLayerClient, encoded_data: HexStr, @@ -934,18 +1014,21 @@ def _send_consensus_call( if self.chain.consensus_main_contract is None: raise GenLayerError("Consensus main contract not configured.") - transaction = _prepare_transaction( - self=self, - sender=sender_account.address, - recipient=self.chain.consensus_main_contract["address"], - data=encoded_data, - value=value, - ) - signed_transaction = sender_account.sign_transaction(transaction) - serialized_transaction = self.w3.to_hex(signed_transaction.raw_transaction) - tx_hash = self.provider.make_request( - method="eth_sendRawTransaction", params=[serialized_transaction] - )["result"] + try: + transaction = _prepare_transaction( + self=self, + sender=sender_account.address, + recipient=self.chain.consensus_main_contract["address"], + data=encoded_data, + value=value, + ) + signed_transaction = sender_account.sign_transaction(transaction) + serialized_transaction = self.w3.to_hex(signed_transaction.raw_transaction) + tx_hash = self.provider.make_request( + method="eth_sendRawTransaction", params=[serialized_transaction] + )["result"] + except Exception as exc: + raise GenLayerError(f"{operation_name} failed: {_format_rpc_error(exc)}") from exc if self.chain.id == localnet.id: return tx_hash @@ -974,21 +1057,24 @@ def _send_transaction( f"Consensus main contract address not found in chain config for \"{self.chain.name}\".", ) - transaction = _prepare_transaction( - self=self, - sender=sender_account.address, - recipient=self.chain.consensus_main_contract["address"], - data=encoded_data, - value=value, - ) - signed_transaction = sender_account.sign_transaction(transaction) - serialized_transaction = self.w3.to_hex(signed_transaction.raw_transaction) - params = [serialized_transaction] - if sim_config is not None: - params.append(sim_config) - tx_hash = self.provider.make_request( - method="eth_sendRawTransaction", params=params - )["result"] + try: + transaction = _prepare_transaction( + self=self, + sender=sender_account.address, + recipient=self.chain.consensus_main_contract["address"], + data=encoded_data, + value=value, + ) + signed_transaction = sender_account.sign_transaction(transaction) + serialized_transaction = self.w3.to_hex(signed_transaction.raw_transaction) + params = [serialized_transaction] + if sim_config is not None: + params.append(sim_config) + tx_hash = self.provider.make_request( + method="eth_sendRawTransaction", params=params + )["result"] + except Exception as exc: + raise GenLayerError(f"Transaction failed: {_format_rpc_error(exc)}") from exc tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) if tx_receipt.status != 1: diff --git a/genlayer_py/transactions/__init__.py b/genlayer_py/transactions/__init__.py index 3674413..ee9b531 100644 --- a/genlayer_py/transactions/__init__.py +++ b/genlayer_py/transactions/__init__.py @@ -2,6 +2,7 @@ CALL_KEY_DEPLOY, CALL_KEY_UNNAMED, CALL_KEY_WILDCARD, + DEPLOY_CALL_KEY, MESSAGE_ALLOCATION_ROOT_PARENT_INDEX, DEFAULT_FEES_DISTRIBUTION, MessageType, @@ -9,16 +10,24 @@ calculate_local_round_fees, create_fees_distribution, derive_external_message_call_key, + deploy_call_key, derive_internal_message_call_key, encode_external_message_fee_params, encode_internal_message_fee_params, extract_studio_fee_policy, ) + +def is_successful(transaction): + from .actions import is_successful as _is_successful + + return _is_successful(transaction) + __all__ = [ "CALL_KEY_DEPLOY", "CALL_KEY_UNNAMED", "CALL_KEY_WILDCARD", + "DEPLOY_CALL_KEY", "MESSAGE_ALLOCATION_ROOT_PARENT_INDEX", "DEFAULT_FEES_DISTRIBUTION", "MessageType", @@ -26,8 +35,10 @@ "calculate_local_round_fees", "create_fees_distribution", "derive_external_message_call_key", + "deploy_call_key", "derive_internal_message_call_key", "encode_external_message_fee_params", "encode_internal_message_fee_params", "extract_studio_fee_policy", + "is_successful", ] diff --git a/genlayer_py/transactions/actions.py b/genlayer_py/transactions/actions.py index 3da21f1..e4e3825 100644 --- a/genlayer_py/transactions/actions.py +++ b/genlayer_py/transactions/actions.py @@ -2,7 +2,8 @@ from genlayer_py.logging import logger import json -from typing import Any, Dict, List +import warnings +from typing import Any, Dict, List, Literal, Optional from web3 import Web3 from web3.types import _Hash32 from eth_typing import HexStr @@ -11,6 +12,8 @@ from genlayer_py.config import transaction_config from genlayer_py.types import ( TransactionStatus, + ExecutionResult, + EXECUTION_RESULT_NUMBER_TO_NAME, TRANSACTION_STATUS_NAME_TO_NUMBER, TRANSACTION_STATUS_NUMBER_TO_NAME, is_decided_state, @@ -66,40 +69,129 @@ from genlayer_py.client import GenLayerClient +_did_warn_wait_for_transaction_receipt_status = False + + +def _warn_deprecated_receipt_status() -> None: + global _did_warn_wait_for_transaction_receipt_status + if _did_warn_wait_for_transaction_receipt_status: + return + _did_warn_wait_for_transaction_receipt_status = True + warnings.warn( + "wait_for_transaction_receipt(status=...) is deprecated; use " + "wait_until='decided' or wait_until='finalized' instead.", + DeprecationWarning, + stacklevel=3, + ) + + +def _resolve_wait_target( + status: Optional[TransactionStatus], + wait_until: Optional[Literal["decided", "finalized"]], +) -> dict: + if wait_until is not None: + if wait_until not in ("decided", "finalized"): + raise ValueError("wait_until must be 'decided' or 'finalized'.") + return {"wait_until": wait_until, "legacy_status": None, "label": wait_until} + if status is None: + return {"wait_until": "decided", "legacy_status": None, "label": "decided"} + + _warn_deprecated_receipt_status() + if status == TransactionStatus.ACCEPTED: + return {"wait_until": "decided", "legacy_status": None, "label": "decided"} + if status == TransactionStatus.FINALIZED: + return {"wait_until": "finalized", "legacy_status": None, "label": "finalized"} + return {"wait_until": None, "legacy_status": status, "label": status.value} + + +def _has_reached_wait_target(transaction_status: str, target: dict) -> bool: + if target["wait_until"] == "decided": + return is_decided_state(transaction_status) + if target["wait_until"] == "finalized": + return transaction_status == TRANSACTION_STATUS_NAME_TO_NUMBER[ + TransactionStatus.FINALIZED + ] + legacy_status = target["legacy_status"] + return ( + legacy_status is not None + and transaction_status == TRANSACTION_STATUS_NAME_TO_NUMBER[legacy_status] + ) + + +def _normalize_status_name(value: Any) -> Optional[TransactionStatus]: + if isinstance(value, TransactionStatus): + return value + if isinstance(value, str): + if value in TransactionStatus._value2member_map_: + return TransactionStatus(value) + return TRANSACTION_STATUS_NUMBER_TO_NAME.get(value) + if value is None: + return None + return TRANSACTION_STATUS_NUMBER_TO_NAME.get(str(value)) + + +def _normalize_execution_result_name(value: Any) -> Optional[ExecutionResult]: + if isinstance(value, ExecutionResult): + return value + if isinstance(value, str) and value in ExecutionResult._value2member_map_: + return ExecutionResult(value) + if value is None: + return None + return EXECUTION_RESULT_NUMBER_TO_NAME.get(str(value)) + + +def is_successful(transaction: GenLayerTransaction) -> bool: + status_name = _normalize_status_name( + transaction.get("status_name", transaction.get("status")) + ) + execution_result_name = _normalize_execution_result_name( + transaction.get( + "tx_execution_result_name", + transaction.get("tx_execution_result"), + ) + ) + return ( + status_name in (TransactionStatus.ACCEPTED, TransactionStatus.FINALIZED) + and execution_result_name == ExecutionResult.FINISHED_WITH_RETURN + ) + + def wait_for_transaction_receipt( self: GenLayerClient, transaction_hash: _Hash32, - status: TransactionStatus = TransactionStatus.ACCEPTED, + status: Optional[TransactionStatus] = None, + wait_until: Optional[Literal["decided", "finalized"]] = None, interval: int = transaction_config.wait_interval, retries: int = transaction_config.retries, full_transaction: bool = False, ) -> GenLayerTransaction: attempts = 0 + target = _resolve_wait_target(status, wait_until) + transaction = None + last_status = None while attempts < retries: transaction = self.get_transaction(transaction_hash=transaction_hash) if transaction is None: raise GenLayerError(f"Transaction {transaction_hash} not found") transaction_status = str(transaction["status"]) - last_status = TRANSACTION_STATUS_NUMBER_TO_NAME[transaction_status] - finalized_status = TRANSACTION_STATUS_NAME_TO_NUMBER[ - TransactionStatus.FINALIZED - ] - requested_status = TRANSACTION_STATUS_NAME_TO_NUMBER[status] + last_status = TRANSACTION_STATUS_NUMBER_TO_NAME.get(transaction_status) - if transaction_status == requested_status or ( - status == TransactionStatus.ACCEPTED - and is_decided_state(transaction_status) - ): + if _has_reached_wait_target(transaction_status, target): if not full_transaction: return _simplify_transaction_receipt(transaction) return transaction time.sleep(interval / 1000) attempts += 1 + last_status_label = ( + last_status.value + if isinstance(last_status, TransactionStatus) + else str(transaction["status"]) if transaction else "" + ) raise GenLayerError( - f"Transaction {transaction_hash} did not reach desired status '{status.value}' after {retries} attempts " + f"Transaction {transaction_hash} did not reach desired status '{target['label']}' after {retries} attempts " f"(polling every {interval}ms for a total of {retries * interval / 1000:.1f}s). " - f"Last observed status: '{last_status.value}'. " + f"Last observed status: '{last_status_label}'. " f"This may indicate the transaction is still processing, or the network is experiencing delays. " f"Consider increasing 'retries' or 'interval' parameters.\n" f"Transaction object simplified: {json.dumps(_simplify_transaction_receipt(transaction), indent=2, default=str)}" diff --git a/genlayer_py/transactions/fees.py b/genlayer_py/transactions/fees.py index 9916866..d67b639 100644 --- a/genlayer_py/transactions/fees.py +++ b/genlayer_py/transactions/fees.py @@ -156,7 +156,8 @@ class NormalizedTransactionFees(TypedDict): MESSAGE_ALLOCATION_ROOT_PARENT_INDEX = (1 << 256) - 1 CALL_KEY_WILDCARD = b"\x00" * 32 CALL_KEY_UNNAMED = HexStr("0x" + ("0" * 64)) -CALL_KEY_DEPLOY = CALL_KEY_UNNAMED +DEPLOY_CALL_KEY = HexStr("0x" + ("0" * 62) + "01") +CALL_KEY_DEPLOY = DEPLOY_CALL_KEY FEES_DISTRIBUTION_ABI_TYPE = ( "(uint256,uint256,uint256,uint256,uint256,uint256,uint256[],uint256,uint256,uint256)" @@ -194,6 +195,8 @@ class NormalizedTransactionFees(TypedDict): DEFAULT_LEADER_TIMEUNITS_ALLOCATION = 100 DEFAULT_VALIDATOR_TIMEUNITS_ALLOCATION = 200 DEFAULT_TRANSACTION_EXECUTION_BUDGET_PER_ROUND = 500_000 +# Provisional heuristic sized ~20x observed dev-env consumption (~5M gas-equivalent). +# TODO(data): replace with telemetry-derived default (p99 x margin) once fee consumption telemetry is collected. DEFAULT_TRANSACTION_EXECUTION_GAS = 100_000_000 DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM = 10_000 MIN_RECEIPT_BYTES = 512 @@ -201,8 +204,11 @@ class NormalizedTransactionFees(TypedDict): DEFAULT_INTRINSIC_GAS = 21_000 DEFAULT_BOOTLOADER_OVERHEAD = 60_000 DEFAULT_FIXED_PROPOSE_RECEIPT_GAS = 210_000 +DEFAULT_FIXED_MESSAGE_REVEAL_GAS = 100_000 DEFAULT_GAS_PER_CHANGED_SLOT = 1_000 DEFAULT_CALLDATA_GAS_PER_BYTE = 16 +DEFAULT_MESSAGE_REVEAL_LENGTH_SLOTS = 32 +DEFAULT_NONDET_OUTPUT_LENGTH_BYTES = 32 ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" VALIDATORS_PER_ROUND = [ @@ -308,6 +314,10 @@ def derive_external_message_call_key( return _bytes_to_padded_call_key(calldata[:4]) +def deploy_call_key() -> HexStr: + return DEPLOY_CALL_KEY + + def _normalize_message_type(value: Union[MessageType, int, str]) -> int: if isinstance(value, MessageType): return int(value) @@ -658,6 +668,10 @@ def extract_studio_fee_policy(config: Any) -> FeePolicyQuote: policy.get("fixedProposeReceiptGas", DEFAULT_FIXED_PROPOSE_RECEIPT_GAS), "policy.fixedProposeReceiptGas", ) + fixed_message_reveal_gas = _int_from_unknown( + policy.get("fixedMessageRevealGas", DEFAULT_FIXED_MESSAGE_REVEAL_GAS), + "policy.fixedMessageRevealGas", + ) explicit_budget_floor = policy.get( "messageFeeParamsBudgetFloor", config.get("messageFeeParamsBudgetFloor"), @@ -673,8 +687,12 @@ def extract_studio_fee_policy(config: Any) -> FeePolicyQuote: fixed_propose_receipt_gas + intrinsic_gas + bootloader_overhead - + (MIN_RECEIPT_BYTES * calldata_gas_per_byte) + (DEFAULT_RECEIPT_SLOTS_CHANGED * gas_per_changed_slot) + + fixed_message_reveal_gas + + intrinsic_gas + + bootloader_overhead + + (DEFAULT_MESSAGE_REVEAL_LENGTH_SLOTS * gas_per_changed_slot) + + (DEFAULT_NONDET_OUTPUT_LENGTH_BYTES * calldata_gas_per_byte) ) ) @@ -860,7 +878,7 @@ def observed_simulation_fee_usage( observed_execution_budget = execution_fee_consumed + execution_fee_report_total recommended_execution_budget = ( max( - _default_execution_budget_per_round(policy), + policy["executionBudgetFloor"], _with_cap_headroom(observed_execution_budget, execution_headroom_bps), ) if observed_execution_budget > 0 diff --git a/genlayer_py/types/transactions.py b/genlayer_py/types/transactions.py index f73a85a..89a25fe 100644 --- a/genlayer_py/types/transactions.py +++ b/genlayer_py/types/transactions.py @@ -27,6 +27,7 @@ class TransactionStatus(str, Enum): READY_TO_FINALIZE = "READY_TO_FINALIZE" VALIDATORS_TIMEOUT = "VALIDATORS_TIMEOUT" LEADER_TIMEOUT = "LEADER_TIMEOUT" + LEADER_REVEALING = "LEADER_REVEALING" TRANSACTION_STATUS_NUMBER_TO_NAME = { @@ -44,6 +45,7 @@ class TransactionStatus(str, Enum): "11": TransactionStatus.READY_TO_FINALIZE, "12": TransactionStatus.VALIDATORS_TIMEOUT, "13": TransactionStatus.LEADER_TIMEOUT, + "14": TransactionStatus.LEADER_REVEALING, } TRANSACTION_STATUS_NAME_TO_NUMBER = { @@ -61,6 +63,7 @@ class TransactionStatus(str, Enum): TransactionStatus.READY_TO_FINALIZE: "11", TransactionStatus.VALIDATORS_TIMEOUT: "12", TransactionStatus.LEADER_TIMEOUT: "13", + TransactionStatus.LEADER_REVEALING: "14", } DECIDED_STATES = [ @@ -86,29 +89,26 @@ class TransactionResult(str, Enum): NO_MAJORITY = "NO_MAJORITY" MAJORITY_AGREE = "MAJORITY_AGREE" MAJORITY_DISAGREE = "MAJORITY_DISAGREE" + MAJORITY_TIMEOUT = "MAJORITY_TIMEOUT" TRANSACTION_RESULT_NUMBER_TO_NAME = { "0": TransactionResult.IDLE, - "1": TransactionResult.AGREE, - "2": TransactionResult.DISAGREE, - "3": TransactionResult.TIMEOUT, + "1": TransactionResult.MAJORITY_AGREE, + "2": TransactionResult.MAJORITY_DISAGREE, + "3": TransactionResult.MAJORITY_TIMEOUT, "4": TransactionResult.DETERMINISTIC_VIOLATION, "5": TransactionResult.NO_MAJORITY, - "6": TransactionResult.MAJORITY_AGREE, - "7": TransactionResult.MAJORITY_DISAGREE, } TRANSACTION_RESULT_NAME_TO_NUMBER = { TransactionResult.IDLE: "0", - TransactionResult.AGREE: "1", - TransactionResult.DISAGREE: "2", - TransactionResult.TIMEOUT: "3", + TransactionResult.MAJORITY_AGREE: "1", + TransactionResult.MAJORITY_DISAGREE: "2", + TransactionResult.MAJORITY_TIMEOUT: "3", TransactionResult.DETERMINISTIC_VIOLATION: "4", TransactionResult.NO_MAJORITY: "5", - TransactionResult.MAJORITY_AGREE: "6", - TransactionResult.MAJORITY_DISAGREE: "7", } @@ -117,12 +117,16 @@ class ExecutionResult(str, Enum): NOT_VOTED = "NOT_VOTED" FINISHED_WITH_RETURN = "FINISHED_WITH_RETURN" FINISHED_WITH_ERROR = "FINISHED_WITH_ERROR" + TIMEOUT = "TIMEOUT" + NONDET_DISAGREE = "NONDET_DISAGREE" EXECUTION_RESULT_NUMBER_TO_NAME = { "0": ExecutionResult.NOT_VOTED, "1": ExecutionResult.FINISHED_WITH_RETURN, "2": ExecutionResult.FINISHED_WITH_ERROR, + "3": ExecutionResult.TIMEOUT, + "4": ExecutionResult.NONDET_DISAGREE, } diff --git a/tests/unit/contracts/test_contract_actions.py b/tests/unit/contracts/test_contract_actions.py index 7887bed..5a9e298 100644 --- a/tests/unit/contracts/test_contract_actions.py +++ b/tests/unit/contracts/test_contract_actions.py @@ -2,17 +2,21 @@ from unittest.mock import Mock import eth_utils +import pytest from eth_abi import decode as abi_decode from web3 import Web3 import genlayer_py.contracts.actions as contract_actions from genlayer_py.chains import localnet +from genlayer_py.chains.testnet_asimov import testnet_asimov +from genlayer_py.exceptions import GenLayerError from genlayer_py.transactions.fees import ( ADD_TRANSACTION_WITH_FEES_ARGUMENT_TYPES, ADD_TRANSACTION_WITH_FEES_SELECTOR, CALL_KEY_DEPLOY, CALL_KEY_UNNAMED, CALL_KEY_WILDCARD, + DEPLOY_CALL_KEY, FEES_DISTRIBUTION_ABI_TYPE, MESSAGE_ALLOCATION_ROOT_PARENT_INDEX, MessageType, @@ -21,6 +25,7 @@ create_fees_distribution, derive_external_message_call_key, derive_internal_message_call_key, + deploy_call_key, encode_external_message_fee_params, encode_internal_message_fee_params, extract_studio_fee_policy, @@ -29,6 +34,7 @@ DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM = 10_000 +LOCAL_EXECUTION_BUDGET_FLOOR_GAS = 306_192 ADD_TRANSACTION_ABI_V5 = [ @@ -155,7 +161,9 @@ def test_derive_internal_message_call_key_hashes_exact_32_byte_method_name(): def test_derive_message_call_key_constants_for_deploy_and_unnamed(): - assert CALL_KEY_DEPLOY == "0x" + "00" * 32 + assert DEPLOY_CALL_KEY == "0x" + "00" * 31 + "01" + assert CALL_KEY_DEPLOY == DEPLOY_CALL_KEY + assert deploy_call_key() == DEPLOY_CALL_KEY assert CALL_KEY_UNNAMED == "0x" + "00" * 32 assert derive_internal_message_call_key("") == CALL_KEY_UNNAMED @@ -275,6 +283,87 @@ def fake_send_consensus_call(**kwargs): assert captured["encoded_data"].startswith(f"0x{selector}") +def test_appeal_transaction_auto_resolves_min_bond(monkeypatch): + client = _make_client(ADD_TRANSACTION_ABI_WITH_FEES) + client.chain.fee_manager_contract = {"address": "0x4444444444444444444444444444444444444444"} + client.chain.rounds_storage_contract = {"address": "0x5555555555555555555555555555555555555555"} + captured = {} + + monkeypatch.setattr( + contract_actions, + "get_min_appeal_bond", + Mock(return_value=4_321), + ) + monkeypatch.setattr( + contract_actions, + "_encode_submit_appeal_data", + Mock(return_value="0x1234"), + ) + + def fake_send_consensus_call(**kwargs): + captured.update(kwargs) + return "0xevmtx" + + monkeypatch.setattr( + contract_actions, + "_send_consensus_call", + fake_send_consensus_call, + ) + + result = contract_actions.appeal_transaction( + self=client, + transaction_id=TX_ID, + ) + + assert result == TX_ID + contract_actions.get_min_appeal_bond.assert_called_once_with(client, TX_ID) + assert captured["value"] == 4_321 + assert captured["operation_name"] == "Appeal" + + +def test_top_up_and_submit_appeal_auto_resolves_min_bond(monkeypatch): + client = _make_client(ADD_TRANSACTION_ABI_WITH_FEES) + client.chain.fee_manager_contract = {"address": "0x4444444444444444444444444444444444444444"} + client.chain.rounds_storage_contract = {"address": "0x5555555555555555555555555555555555555555"} + captured = {} + + monkeypatch.setattr( + contract_actions, + "get_min_appeal_bond", + Mock(return_value=9_999), + ) + + def fake_send_consensus_call(**kwargs): + captured.update(kwargs) + return "0xevmtx" + + monkeypatch.setattr( + contract_actions, + "_send_consensus_call", + fake_send_consensus_call, + ) + + result = contract_actions.top_up_and_submit_appeal( + self=client, + transaction_id=TX_ID, + distribution={"appealRounds": 1, "rotations": [0, 1]}, + ) + + assert result == TX_ID + assert captured["value"] == 9_999 + + +def test_appeal_auto_bond_requires_fee_contracts(): + client = _make_client(ADD_TRANSACTION_ABI_WITH_FEES) + client.chain.fee_manager_contract = None + + with pytest.raises(GenLayerError, match="Cannot auto-resolve appeal bond"): + contract_actions.appeal_transaction( + self=client, + transaction_id=TX_ID, + ) + + def test_send_consensus_call_returns_localnet_rpc_hash_without_waiting(monkeypatch): wait_for_transaction_receipt = Mock() sign_transaction = Mock( @@ -315,6 +404,14 @@ def test_send_consensus_call_returns_localnet_rpc_hash_without_waiting(monkeypat wait_for_transaction_receipt.assert_not_called() +def test_revert_selector_formatter_names_fee_errors(): + error = Exception({"data": "0x632be5a1"}) + + assert contract_actions._format_rpc_error(error).endswith( + "(FeeValueMustBeNonZero)" + ) + + def _make_client(add_transaction_abi): chain = SimpleNamespace( id=61999, @@ -333,6 +430,113 @@ def _make_client(add_transaction_abi): ) +class _FakeContractFunction: + def __init__(self, value): + self.value = value + + def call(self): + return self.value + + +class _FakeFeeManagerFunctions: + def __init__(self, values): + self.values = values + + def GENPerTimeUnit(self): + return _FakeContractFunction(self.values["gen"]) + + def storageUnitPrice(self): + return _FakeContractFunction(self.values["storage"]) + + def quoteGasPrice(self): + return _FakeContractFunction(self.values["quote"]) + + def messageFeeParamsBudgetFloor(self): + return _FakeContractFunction(self.values["floor"]) + + +class _FakeEth: + def __init__(self, values, gas_price): + self.values = values + self._gas_price = gas_price + self.gas_price_accesses = 0 + + def contract(self, address=None, abi=None): + return SimpleNamespace(functions=_FakeFeeManagerFunctions(self.values)) + + @property + def gas_price(self): + self.gas_price_accesses += 1 + if isinstance(self._gas_price, Exception): + raise self._gas_price + return self._gas_price + + +def _make_fee_policy_client(values, gas_price=0): + eth = _FakeEth(values, gas_price) + return SimpleNamespace( + chain=SimpleNamespace( + fee_manager_contract={"address": "0x4444444444444444444444444444444444444444"}, + ), + w3=SimpleNamespace( + eth=eth, + to_checksum_address=Mock(side_effect=lambda address: address), + ), + ) + + +def test_get_current_fee_policy_uses_effective_receipt_gas_price_and_local_floor(): + client = _make_fee_policy_client( + {"gen": 0, "storage": 0, "quote": 1, "floor": 0}, + gas_price=2, + ) + + policy = contract_actions.get_current_fee_policy(client) + + assert policy["enabled"] is True + assert policy["receiptGasPrice"] == 2 + assert policy["executionBudgetFloor"] == 2 * LOCAL_EXECUTION_BUDGET_FLOOR_GAS + assert client.w3.eth.gas_price_accesses == 1 + + +def test_get_current_fee_policy_refuses_enabled_zero_receipt_price(): + client = _make_fee_policy_client( + {"gen": 1, "storage": 0, "quote": 0, "floor": 0}, + gas_price=0, + ) + + with pytest.raises( + GenLayerError, + match="receipt gas price quoted as zero; refusing to build a zero price cap", + ): + contract_actions.get_current_fee_policy(client) + + +def test_get_current_fee_policy_does_not_read_network_gas_price_when_disabled(): + client = _make_fee_policy_client( + {"gen": 0, "storage": 0, "quote": 0, "floor": 0}, + gas_price=AssertionError("eth_gasPrice should not be called"), + ) + + policy = contract_actions.get_current_fee_policy(client) + + assert policy["enabled"] is False + assert policy["receiptGasPrice"] == 0 + assert client.w3.eth.gas_price_accesses == 0 + + +def test_asimov_chain_has_fee_contracts(): + assert testnet_asimov.fee_manager_contract["address"] == ( + "0x21737AA4bea8FF12E202BF1BAB23751A95617533" + ) + assert testnet_asimov.rounds_storage_contract["address"] == ( + "0x1F595c0D549DE0812F127508ea1039636CFA62Cc" + ) + assert testnet_asimov.appeals_contract["address"] == ( + "0x0F739Dd8f5322b9547c7d19a9621BC2ac8DF4089" + ) + + def test_simulate_write_contract_passes_fee_policy_and_value_to_sim_call(): make_request = Mock( return_value={ @@ -687,6 +891,31 @@ def test_estimate_transaction_fees_uses_studio_fee_config(): assert estimate["feeValue"] == 3_000_011_000 +def test_extract_studio_fee_policy_fallback_includes_message_reveal_leg(): + policy = extract_studio_fee_policy( + { + "enabled": True, + "policy": { + "genPerTimeUnit": "10", + "storageUnitPrice": "20", + "receiptGasPrice": "30", + }, + } + ) + + assert policy["executionBudgetFloor"] == 30 * ( + 210_000 + + 21_000 + + 60_000 + + 7 * 1_000 + + 100_000 + + 21_000 + + 60_000 + + 32 * 1_000 + + 32 * 16 + ) + + def test_estimate_transaction_fees_derives_message_bucket_from_allocations(): client = SimpleNamespace( chain=SimpleNamespace( @@ -818,6 +1047,52 @@ def test_estimate_transaction_fees_from_simulation_builds_trusted_preset(): assert estimate["feeValue"] == 613_123 +def test_simulation_execution_budget_uses_floor_without_default_gas_clobber(): + client = SimpleNamespace( + chain=SimpleNamespace( + fee_manager_contract=None, + default_number_of_initial_validators=5, + ), + provider=SimpleNamespace( + make_request=Mock( + return_value={ + "result": { + "enabled": True, + "policy": { + "genPerTimeUnit": "10", + "storageUnitPrice": "0", + "receiptGasPrice": "1", + "messageFeeParamsBudgetFloor": str( + LOCAL_EXECUTION_BUDGET_FLOOR_GAS + ), + }, + } + } + ) + ), + ) + + estimate = contract_actions.estimate_transaction_fees_from_simulation( + self=client, + options={ + "simulation": { + "feeAccounting": { + "execution_fee_consumed": "10", + "execution_fee_report": {"totalEstimatedFee": "0"}, + } + }, + "executionHeadroomBps": 10_000, + }, + ) + + assert estimate["observed"]["recommendedExecutionBudgetPerRound"] == ( + LOCAL_EXECUTION_BUDGET_FLOOR_GAS + ) + assert estimate["distribution"]["executionBudgetPerRound"] == ( + LOCAL_EXECUTION_BUDGET_FLOOR_GAS + ) + + def test_estimate_transaction_fees_for_write_uses_studio_estimate_rpc(): fee_params = encode_internal_message_fee_params( { @@ -928,7 +1203,7 @@ def make_request(method, params): request_params["fees"]["messageAllocations"][0]["callKey"] == "0x" + "00" * 32 ) - assert estimate["observed"]["recommendedExecutionBudgetPerRound"] == 100_000_000 + assert estimate["observed"]["recommendedExecutionBudgetPerRound"] == 602_117 assert estimate["observed"]["messageFeeBudget"] == 110 assert estimate["observed"]["messageFeeConsumed"] == 50 assert estimate["simulation"]["feeAccounting"]["message_fee_budget"] == "110" diff --git a/tests/unit/transactions/test_wait_for_transaction_receipt.py b/tests/unit/transactions/test_wait_for_transaction_receipt.py index 2a06893..6a3a7f0 100644 --- a/tests/unit/transactions/test_wait_for_transaction_receipt.py +++ b/tests/unit/transactions/test_wait_for_transaction_receipt.py @@ -3,8 +3,15 @@ from genlayer_py.transactions.actions import ( wait_for_transaction_receipt, _simplify_transaction_receipt, + is_successful, +) +from genlayer_py.types import ( + ExecutionResult, + TransactionStatus, + DECIDED_STATES, + TRANSACTION_STATUS_NUMBER_TO_NAME, + is_decided_state, ) -from genlayer_py.types import TransactionStatus, DECIDED_STATES, is_decided_state from genlayer_py.exceptions import GenLayerError @@ -162,6 +169,43 @@ def test_wait_for_accepted_with_all_decided_states(self, mock_client): assert result == mock_transaction + def test_wait_until_decided_resolves_on_undetermined(self, mock_client): + mock_transaction = { + "hash": "0x4b8037744adab7ea8335b4f839979d20031d83a8ccdf706e0ae61312930335f6", + "status": "6", + "status_name": "UNDETERMINED", + } + mock_client.get_transaction.return_value = mock_transaction + + result = wait_for_transaction_receipt( + self=mock_client, + transaction_hash="0x4b8037744adab7ea8335b4f839979d20031d83a8ccdf706e0ae61312930335f6", + wait_until="decided", + full_transaction=True, + ) + + assert result == mock_transaction + + def test_status_14_does_not_crash_wait_loop(self, mock_client): + mock_transaction = { + "hash": "0x4b8037744adab7ea8335b4f839979d20031d83a8ccdf706e0ae61312930335f6", + "status": "14", + "status_name": "LEADER_REVEALING", + } + mock_client.get_transaction.return_value = mock_transaction + + with pytest.raises(GenLayerError, match="LEADER_REVEALING"): + wait_for_transaction_receipt( + self=mock_client, + transaction_hash="0x4b8037744adab7ea8335b4f839979d20031d83a8ccdf706e0ae61312930335f6", + wait_until="finalized", + retries=1, + interval=1, + full_transaction=True, + ) + + assert TRANSACTION_STATUS_NUMBER_TO_NAME["14"] == TransactionStatus.LEADER_REVEALING + def test_wait_for_specific_status_not_affected(self, mock_client): """Test that waiting for specific non-ACCEPTED statuses is not affected by decided states logic""" mock_transaction = { @@ -227,6 +271,34 @@ def test_is_decided_state_with_invalid_status(self): assert is_decided_state(status) == False, f"Invalid status {status} should not be decided" +class TestIsSuccessful: + def test_truth_table(self): + assert is_successful( + { + "status": "5", + "tx_execution_result": "1", + } + ) + assert not is_successful( + { + "status": "6", + "tx_execution_result": "1", + } + ) + assert not is_successful( + { + "status": TransactionStatus.ACCEPTED, + "tx_execution_result": "2", + } + ) + assert is_successful( + { + "status_name": TransactionStatus.FINALIZED, + "tx_execution_result_name": ExecutionResult.FINISHED_WITH_RETURN, + } + ) + + class TestSimplifyTransactionReceipt: """Test suite for _simplify_transaction_receipt function""" From dd25ef7f43e99a14b8fe42a64e01374845ad4d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Nem=C5=A1e?= Date: Wed, 10 Jun 2026 15:46:12 +0100 Subject: [PATCH 04/12] ci: keep main forwarded to active dev branch (#92) --- .github/scripts/validate-branch-policy.sh | 107 ++++++++++++++++++++++ .github/workflows/branch-policy.yml | 24 +++++ .github/workflows/fast-forward-main.yaml | 57 ++++++++++++ .github/workflows/retarget-main-prs.yaml | 53 +++++++++++ support/ci/ACTIVE_DEV_BRANCH | 1 + 5 files changed, 242 insertions(+) create mode 100755 .github/scripts/validate-branch-policy.sh create mode 100644 .github/workflows/branch-policy.yml create mode 100644 .github/workflows/fast-forward-main.yaml create mode 100644 .github/workflows/retarget-main-prs.yaml create mode 100644 support/ci/ACTIVE_DEV_BRANCH diff --git a/.github/scripts/validate-branch-policy.sh b/.github/scripts/validate-branch-policy.sh new file mode 100755 index 0000000..0fb7ee2 --- /dev/null +++ b/.github/scripts/validate-branch-policy.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +failed=0 + +error() { + echo "::error::$*" + failed=1 +} + +warning() { + echo "::warning::$*" +} + +active_branch_file="support/ci/ACTIVE_DEV_BRANCH" +if [[ ! -f "${active_branch_file}" ]]; then + error "${active_branch_file} is required." + active_branch="" +else + active_branch="$(tr -d '[:space:]' < "${active_branch_file}")" +fi + +if [[ -z "${active_branch}" ]]; then + error "${active_branch_file} must not be empty." +elif [[ "${active_branch}" == "main" ]]; then + error "${active_branch_file} must point to a dev branch, not main." +elif [[ "${active_branch}" != *-dev ]]; then + warning "${active_branch_file} should normally point to a -dev branch; got ${active_branch}." +fi + +release_branch="${active_branch%-dev}" +default_branch="${GITHUB_DEFAULT_BRANCH:-}" +event_name="${GITHUB_EVENT_NAME:-local}" +base_ref="${GITHUB_BASE_REF:-}" +head_ref="${GITHUB_HEAD_REF:-}" +ref_name="${GITHUB_REF_NAME:-}" +actor="${GITHUB_ACTOR:-}" + +if [[ -n "${default_branch}" && "${default_branch}" != "main" ]]; then + warning "Repository default branch should be main after branch-policy rollout; currently ${default_branch}." +fi + +if [[ -n "${base_ref}" && "${base_ref}" == "main" ]]; then + warning "PR targets main; retarget-main-prs should move it to ${active_branch}." +fi + +if [[ -n "${base_ref}" && -n "${active_branch}" ]]; then + if [[ "${base_ref}" == "${release_branch}" && "${head_ref}" != "${active_branch}" && "${ALLOW_DIRECT_RELEASE_PR:-false}" != "true" ]]; then + error "PRs into ${release_branch} must come from ${active_branch}. Merge feature work into ${active_branch}, then promote ${active_branch} -> ${release_branch}." + fi +fi + +if [[ "${event_name}" == "push" && "${ref_name}" == "main" ]]; then + case "${actor}" in + github-actions[bot]|ci-core-e2e-runner[bot]) + ;; + *) + error "main should only move by automation from ${active_branch}; direct push actor was ${actor:-unknown}." + ;; + esac +fi + +if [[ ! -f ".github/workflows/fast-forward-main.yaml" ]]; then + error ".github/workflows/fast-forward-main.yaml is required." +fi + +if [[ ! -f ".github/workflows/retarget-main-prs.yaml" ]]; then + error ".github/workflows/retarget-main-prs.yaml is required." +fi + +if [[ -f ".github/workflows/release-from-main.yml" ]]; then + error ".github/workflows/release-from-main.yml is forbidden. Releases must be tag/version-branch driven." +fi + +if [[ -f "release.config.js" ]]; then + error "release.config.js is forbidden in versioned tooling branches; semantic-release-on-main must not be restored." +fi + +if [[ -f ".github/workflows/release-from-tag.yml" ]]; then + if ! grep -Fq 'v*.*.*' .github/workflows/release-from-tag.yml; then + error "release-from-tag.yml must trigger only from version tags matching v*.*.*." + fi + if ! grep -Fq 'refs/remotes/origin/${version_branch}' .github/workflows/release-from-tag.yml || \ + ! grep -Fq 'tag_commit' .github/workflows/release-from-tag.yml || \ + ! grep -Fq 'branch_head' .github/workflows/release-from-tag.yml; then + error "release-from-tag.yml must verify the tag commit is the current matching version branch head." + fi +fi + +if [[ -f ".github/workflows/manual-docker-release.yml" ]]; then + if ! grep -Fq 'expected_branch=' .github/workflows/manual-docker-release.yml; then + error "manual-docker-release.yml must derive and enforce the expected version branch from the tag." + fi + if ! grep -Fq './.github/workflows/release-from-tag.yml' .github/workflows/manual-docker-release.yml; then + error "manual-docker-release.yml must delegate image promotion to release-from-tag.yml." + fi +fi + +if [[ "${failed}" -ne 0 ]]; then + exit 1 +fi + +if [[ -n "${base_ref}" ]]; then + echo "Branch policy ok for PR ${head_ref} -> ${base_ref}; active dev branch is ${active_branch}." +else + echo "Branch policy ok for ${event_name} on ${ref_name:-detached ref}; active dev branch is ${active_branch}." +fi diff --git a/.github/workflows/branch-policy.yml b/.github/workflows/branch-policy.yml new file mode 100644 index 0000000..7759870 --- /dev/null +++ b/.github/workflows/branch-policy.yml @@ -0,0 +1,24 @@ +name: Branch Policy + +on: + pull_request: + types: [opened, synchronize, reopened, edited, ready_for_review] + push: + branches: + - "**" + workflow_dispatch: + +permissions: + contents: read + +jobs: + branch-policy: + name: Validate branch policy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate branch policy + env: + GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: ./.github/scripts/validate-branch-policy.sh diff --git a/.github/workflows/fast-forward-main.yaml b/.github/workflows/fast-forward-main.yaml new file mode 100644 index 0000000..688e997 --- /dev/null +++ b/.github/workflows/fast-forward-main.yaml @@ -0,0 +1,57 @@ +name: Fast-forward main + +# main is the static/default branch for GitHub UX and tools that assume a +# stable default branch. It is not the integration target. On each push to the +# configured active dev branch, fast-forward main to that commit. + +on: + push: + branches: ["**"] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: fast-forward-main-${{ github.repository }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + fast-forward: + if: github.ref_type == 'branch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fast-forward main to active dev branch + run: | + set -euo pipefail + + active_branch="$(tr -d '[:space:]' < support/ci/ACTIVE_DEV_BRANCH)" + if [[ -z "${active_branch}" || "${active_branch}" == "main" ]]; then + echo "::error::support/ci/ACTIVE_DEV_BRANCH must name a non-main dev branch" + exit 1 + fi + + if [[ "${GITHUB_REF_NAME}" != "${active_branch}" ]]; then + echo "Push was to ${GITHUB_REF_NAME}; active dev branch is ${active_branch}. Nothing to do." + exit 0 + fi + + if git ls-remote --exit-code --heads origin main >/dev/null 2>&1; then + git fetch origin main + if ! git merge-base --is-ancestor origin/main HEAD; then + echo "::error::main has diverged from ${active_branch}; refusing non-fast-forward update" + exit 1 + fi + else + echo "main does not exist yet; creating it at ${GITHUB_SHA}." + fi + + git push origin "HEAD:refs/heads/main" diff --git a/.github/workflows/retarget-main-prs.yaml b/.github/workflows/retarget-main-prs.yaml new file mode 100644 index 0000000..37a066f --- /dev/null +++ b/.github/workflows/retarget-main-prs.yaml @@ -0,0 +1,53 @@ +name: Retarget main PRs + +# main is a static/default alias of the active dev branch. Contributions should +# target the active dev branch directly; PRs opened against main are retargeted +# automatically so required checks and release-train rules run in the right +# branch context. +# +# pull_request_target is used for the write-scoped token. This workflow never +# checks out or executes PR head code; it reads only trusted base-branch files. + +on: + pull_request_target: + types: [opened, reopened, synchronize, edited, ready_for_review] + +permissions: + contents: read + pull-requests: write + issues: write + +defaults: + run: + shell: bash + +jobs: + retarget: + if: github.event.pull_request.base.ref == 'main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.ref }} + + - name: Retarget PR to active dev branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + + active_branch="$(tr -d '[:space:]' < support/ci/ACTIVE_DEV_BRANCH)" + if [[ -z "${active_branch}" || "${active_branch}" == "main" ]]; then + echo "::error::support/ci/ACTIVE_DEV_BRANCH must name a non-main dev branch" + exit 1 + fi + + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --base "${active_branch}" + + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body "$(cat < Date: Thu, 11 Jun 2026 17:09:05 +0100 Subject: [PATCH 05/12] docs: add branching guide (#94) --- CONTRIBUTING.md | 20 +++++----------- docs/BRANCHING.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 docs/BRANCHING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17b6afe..682319a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,19 +30,11 @@ Have ideas for new features or use cases? We're eager to hear them! But first: ## Branch model -This repo uses a branch-per-major release model. There is no `main`. - -- **`v0.18`** — current stable major (semver-zero, so 0.18 IS the major; 0.19 would be a major bump that gets its own branch). PRs for bug fixes / non-breaking features target this branch. -- **`v-dev`** — when next-major (i.e. next-minor on 0.x) work is in progress, this branch is open for breaking changes. PRs introducing them target this branch, not `v0.18`. -- **Older majors** stay on the repo for back-ports and security patches. Default branch on github.com is whichever major is current stable. - -When you fork or clone, the default branch is `v0.18` today. If you have a `main` branch from a previous checkout, delete it locally: - -```sh -git checkout v0.18 -git branch -D main -git remote prune origin -``` +See [docs/BRANCHING.md](docs/BRANCHING.md) for the current release-train model. +In short: independently releasable work may target the stable branch directly; +multi-feature or cross-repo train work uses the active `*-dev` integration +branch and is promoted to the matching stable branch when ready. `main` is only +the default/static GitHub branch. ## Releases @@ -170,7 +162,7 @@ The project uses automated semantic versioning based on commit messages: | `feat!:`, `fix!:`, or `BREAKING CHANGE:` | **Major** version bump | 1.0.0 → 2.0.0 | | `docs:`, `style:`, `refactor:`, `test:`, `chore:`, `build:`, `ci:` | **No** version bump | Version stays the same | -**Important**: Never manually edit version numbers in `pyproject.toml` or other files. The release automation will handle all version updates automatically when PRs are merged to the main branch. +**Important**: Never manually edit version numbers in `pyproject.toml` or other files. Releases are cut from the stable branch using the release automation described above. ## Logging Configuration diff --git a/docs/BRANCHING.md b/docs/BRANCHING.md new file mode 100644 index 0000000..0125486 --- /dev/null +++ b/docs/BRANCHING.md @@ -0,0 +1,58 @@ +# Branching and Release Trains + +This repo follows the GenLayer release-train model. + +## Current Train + +- Current stable branch: `v0.18` +- Active integration branch: `v0.19-dev` +- Next stable target: `v0.19` +- `main`: default/static branch alias for the active integration branch + +## Stable Branches + +Stable branches are long-lived release lines. For semver-zero packages, each +minor line is treated as the release line, for example `v0.18` or `v0.19`. + +PRs may target a stable branch directly when the merged result should be +releasable immediately. This is appropriate for bug fixes, small non-breaking +features, isolated release fixes, or a breaking change that is intentionally +shipping as the next version by itself. + +Stable branches must remain releasable. PRs into stable branches are expected to +pass the required cross-repo `E2E Tests` gate before merge. + +## Integration Branches + +Integration branches are optional. Use one when multiple changes need to +accumulate before release, especially for cross-repo work, dependent features, +breaking changes that must ship together, or a train that needs advisory E2E +while still expected to be red. + +Integration branches are named after the target stable branch plus `-dev`, for +example `v0.19-dev`. Feature PRs for that train target the integration branch. + +PRs into integration branches may run `E2E Tests` as advisory checks. They are +not the release gate. + +## Promotion and Release + +When an integration train is ready, open a promotion PR from the integration +branch to the matching stable branch, for example `v0.19-dev` to `v0.19`. + +That promotion PR is the release-readiness gate and must pass required +cross-repo `E2E Tests`. The actual package release is cut from the stable branch +using a version tag after the stable branch is ready. + +## `main` + +`main` exists for GitHub UX and tools that require a stable default branch. It is +not a release branch and it is not the integration target. + +This repo keeps `main` forwarded to the active integration branch using +automation. PRs opened against `main` are automatically retargeted to the branch +listed in `support/ci/ACTIVE_DEV_BRANCH`. + +When changing the active integration branch, update +`support/ci/ACTIVE_DEV_BRANCH`, the repo docs, and the corresponding +`genlayer-e2e` release-train matrix in the same change set. From 6bfa86a160a25d789630944e4ed05f0214fc164d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Nem=C5=A1e?= Date: Thu, 11 Jun 2026 22:45:14 +0100 Subject: [PATCH 06/12] fix(fees)!: move wildcard callKey sentinel to keccak256(empty), deploy key = bytes32(0) (#93) The wildcard sentinel previously shared bytes32(0) with GenVM's natural empty-method-name key (deploy, emit_transfer), making deploy-specific Mode-2 allocations impossible. Per cross-team decision, the wildcard moves to the untagged keccak256 of empty bytes (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470), which is provably outside the derived-key space. DEPLOY_CALL_KEY drops the bytes32(1) sentinel (GenVM ask withdrawn) and now equals CALL_KEY_UNNAMED = bytes32(0), matching what GenVM actually emits. Must land together with the consensus CALL_KEY_WILDCARD constant and the node Mode-2 tree decode change in v0.6. --- genlayer_py/transactions/fees.py | 10 ++++++++-- tests/unit/contracts/test_contract_actions.py | 12 +++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/genlayer_py/transactions/fees.py b/genlayer_py/transactions/fees.py index d67b639..ba1a9e3 100644 --- a/genlayer_py/transactions/fees.py +++ b/genlayer_py/transactions/fees.py @@ -154,9 +154,15 @@ class NormalizedTransactionFees(TypedDict): MESSAGE_ALLOCATION_ROOT_PARENT_INDEX = (1 << 256) - 1 -CALL_KEY_WILDCARD = b"\x00" * 32 +# Wildcard sentinel = keccak256 of empty bytes, untagged. Reserved: it can never be a +# derived key — short names (<32B) are left-aligned with a zero tail byte, long names +# get the low bit forced to 1, and this hash has neither. +CALL_KEY_WILDCARD = bytes.fromhex( + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" +) +# Empty method name derives bytes32(0); GenVM emits it for deploy and emit_transfer. CALL_KEY_UNNAMED = HexStr("0x" + ("0" * 64)) -DEPLOY_CALL_KEY = HexStr("0x" + ("0" * 62) + "01") +DEPLOY_CALL_KEY = CALL_KEY_UNNAMED CALL_KEY_DEPLOY = DEPLOY_CALL_KEY FEES_DISTRIBUTION_ABI_TYPE = ( diff --git a/tests/unit/contracts/test_contract_actions.py b/tests/unit/contracts/test_contract_actions.py index 5a9e298..c0c6de9 100644 --- a/tests/unit/contracts/test_contract_actions.py +++ b/tests/unit/contracts/test_contract_actions.py @@ -161,7 +161,13 @@ def test_derive_internal_message_call_key_hashes_exact_32_byte_method_name(): def test_derive_message_call_key_constants_for_deploy_and_unnamed(): - assert DEPLOY_CALL_KEY == "0x" + "00" * 31 + "01" + # Wildcard is the untagged hash of empty bytes — outside the derived-key space. + assert CALL_KEY_WILDCARD == eth_utils.keccak(b"") + assert CALL_KEY_WILDCARD == bytes.fromhex( + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ) + # Empty name derives bytes32(0): the natural key for deploy and emit_transfer. + assert DEPLOY_CALL_KEY == "0x" + "00" * 32 assert CALL_KEY_DEPLOY == DEPLOY_CALL_KEY assert deploy_call_key() == DEPLOY_CALL_KEY assert CALL_KEY_UNNAMED == "0x" + "00" * 32 @@ -594,7 +600,7 @@ def test_simulate_write_contract_passes_fee_policy_and_value_to_sim_call(): assert request_params["fees"]["messageAllocations"][0]["budget"] == 5 assert ( request_params["fees"]["messageAllocations"][0]["callKey"] - == "0x" + "00" * 32 + == "0x" + CALL_KEY_WILDCARD.hex() ) @@ -1201,7 +1207,7 @@ def make_request(method, params): assert request_params["fees"]["messageAllocations"][0]["budget"] == 110 assert ( request_params["fees"]["messageAllocations"][0]["callKey"] - == "0x" + "00" * 32 + == "0x" + CALL_KEY_WILDCARD.hex() ) assert estimate["observed"]["recommendedExecutionBudgetPerRound"] == 602_117 assert estimate["observed"]["messageFeeBudget"] == 110 From ec7cab9f33cd0b5e40003d1dd21a4be6fef2611d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Nem=C5=A1e?= Date: Tue, 16 Jun 2026 10:02:43 +0100 Subject: [PATCH 07/12] fix: bump package version to v0.19 (#95) Admin merged after required repo CI passed; optional E2E app check is queued/non-blocking for dev-branch PRs. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c8002e3..7024068 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "genlayer-py" -version = "0.18.0" +version = "0.19.0" description = "GenLayer Python SDK" authors = [ { name = "GenLayer" } From 30c262c48ec5f089223c584b381101074381e397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Nem=C5=A1e?= Date: Tue, 7 Jul 2026 21:19:16 +0100 Subject: [PATCH 08/12] feat(vesting): vesting staking actions + ABI (#97) * feat(vesting): add vesting staking actions * feat(vesting): validator-leg actions (join/deposit/exit/claim, operator transfer, identity, wallet reads) Mirrors the genlayer-js validator actions for the CON-607 Vesting.sol surface: vesting_validator_join/deposit/exit/claim, operator-transfer initiate/complete/cancel, set_identity (extra_cid str/hex handling as in js), and get_validator_wallets/validator_wallet_count/ validator_deposited/is_validator_wallet reads. --- genlayer_py/vesting/__init__.py | 51 ++ genlayer_py/vesting/abi/__init__.py | 9 + genlayer_py/vesting/abi/vesting_abi.json | 925 +++++++++++++++++++++ genlayer_py/vesting/actions.py | 479 +++++++++++ pyproject.toml | 2 +- tests/unit/vesting/__init__.py | 0 tests/unit/vesting/test_vesting_actions.py | 559 +++++++++++++ 7 files changed, 2024 insertions(+), 1 deletion(-) create mode 100644 genlayer_py/vesting/__init__.py create mode 100644 genlayer_py/vesting/abi/__init__.py create mode 100644 genlayer_py/vesting/abi/vesting_abi.json create mode 100644 genlayer_py/vesting/actions.py create mode 100644 tests/unit/vesting/__init__.py create mode 100644 tests/unit/vesting/test_vesting_actions.py diff --git a/genlayer_py/vesting/__init__.py b/genlayer_py/vesting/__init__.py new file mode 100644 index 0000000..ad7d320 --- /dev/null +++ b/genlayer_py/vesting/__init__.py @@ -0,0 +1,51 @@ +from genlayer_py.vesting.actions import ( + vesting_delegator_join, + vesting_delegator_exit, + vesting_delegator_claim, + vesting_validator_join, + vesting_validator_deposit, + vesting_validator_exit, + vesting_validator_claim, + vesting_validator_initiate_operator_transfer, + vesting_validator_complete_operator_transfer, + vesting_validator_cancel_operator_transfer, + vesting_validator_set_identity, + vesting_withdraw, + vested_amount, + unvested_amount, + withdrawable_amount, + get_vesting_schedule, + get_vesting_state, + get_vesting_stake_info, + get_validator_wallets, + validator_wallet_count, + validator_deposited, + is_validator_wallet, + get_vesting_contract, +) + +__all__ = [ + "vesting_delegator_join", + "vesting_delegator_exit", + "vesting_delegator_claim", + "vesting_validator_join", + "vesting_validator_deposit", + "vesting_validator_exit", + "vesting_validator_claim", + "vesting_validator_initiate_operator_transfer", + "vesting_validator_complete_operator_transfer", + "vesting_validator_cancel_operator_transfer", + "vesting_validator_set_identity", + "vesting_withdraw", + "vested_amount", + "unvested_amount", + "withdrawable_amount", + "get_vesting_schedule", + "get_vesting_state", + "get_vesting_stake_info", + "get_validator_wallets", + "validator_wallet_count", + "validator_deposited", + "is_validator_wallet", + "get_vesting_contract", +] diff --git a/genlayer_py/vesting/abi/__init__.py b/genlayer_py/vesting/abi/__init__.py new file mode 100644 index 0000000..34c0257 --- /dev/null +++ b/genlayer_py/vesting/abi/__init__.py @@ -0,0 +1,9 @@ +import json +import importlib.resources + +with importlib.resources.as_file( + importlib.resources.files("genlayer_py.vesting.abi").joinpath("vesting_abi.json") +) as path, open(path, "r", encoding="utf-8") as f: + VESTING_ABI = json.load(f) + +__all__ = ["VESTING_ABI"] diff --git a/genlayer_py/vesting/abi/vesting_abi.json b/genlayer_py/vesting/abi/vesting_abi.json new file mode 100644 index 0000000..4ea485e --- /dev/null +++ b/genlayer_py/vesting/abi/vesting_abi.json @@ -0,0 +1,925 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "DelegatorJoined", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "DelegatorExited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "returned", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "rewardOrLoss", + "type": "int256" + } + ], + "name": "DelegatorClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ValidatorJoined", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ValidatorDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "ValidatorExited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "returned", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "rewardOrLoss", + "type": "int256" + } + ], + "name": "ValidatorClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "vestingContract", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "enum IVesting.Category", + "name": "category", + "type": "uint8" + } + ], + "name": "VestingCreated", + "type": "event" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "category", + "outputs": [ + { + "internalType": "enum IVesting.Category", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "beneficiary", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "creator", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "revoker", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "startDate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cliffDuration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "periodDuration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "numberOfPeriods", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cliffUnlockBps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "needsManualUnlock", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "manualUnlocked", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "revoked", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vestingStopped", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalWithdrawn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vestedAtRevocation", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalAmountAtRevocation", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "revokedAt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vestingStoppedAt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vestedAtStop", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "accumulatedRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "accumulatedLosses", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vestedAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unvestedAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawableAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + } + ], + "name": "depositedPerValidator", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + } + ], + "name": "pendingExitDeposited", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "validatorWallets", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "isValidatorWallet", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "validatorDeposited", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getValidatorWallets", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "validatorWalletCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "vestingWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "vestingDelegatorJoin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "vestingDelegatorExit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + } + ], + "name": "vestingDelegatorClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "vestingValidatorJoin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "vestingValidatorDeposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "vestingValidatorExit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "vestingValidatorClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "internalType": "address", + "name": "newOperator", + "type": "address" + } + ], + "name": "vestingValidatorInitiateOperatorTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "vestingValidatorCompleteOperatorTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "vestingValidatorCancelOperatorTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "internalType": "string", + "name": "moniker", + "type": "string" + }, + { + "internalType": "string", + "name": "logoUri", + "type": "string" + }, + { + "internalType": "string", + "name": "website", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "string", + "name": "email", + "type": "string" + }, + { + "internalType": "string", + "name": "twitter", + "type": "string" + }, + { + "internalType": "string", + "name": "telegram", + "type": "string" + }, + { + "internalType": "string", + "name": "github", + "type": "string" + }, + { + "internalType": "bytes", + "name": "extraCid", + "type": "bytes" + } + ], + "name": "vestingValidatorSetIdentity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_beneficiary", + "type": "address" + } + ], + "name": "getVesting", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/genlayer_py/vesting/actions.py b/genlayer_py/vesting/actions.py new file mode 100644 index 0000000..26bcfc8 --- /dev/null +++ b/genlayer_py/vesting/actions.py @@ -0,0 +1,479 @@ +"""Vesting actions for GenLayerClient. + +Mirrors the genlayer_py.staking actions module. Beneficiary write +methods operate on a per-beneficiary Vesting contract address. Factory +discovery methods operate on a VestingFactory contract address. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Optional, Union + +from eth_account.signers.local import LocalAccount +from eth_typing import Address, ChecksumAddress +from hexbytes import HexBytes + +from genlayer_py.exceptions import GenLayerError +from genlayer_py.vesting.abi import VESTING_ABI + +if TYPE_CHECKING: + from genlayer_py.client import GenLayerClient + + +AddressLike = Union[Address, ChecksumAddress, str] +ExtraCidLike = Union[str, bytes, bytearray, HexBytes] + + +def _vesting_address( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> ChecksumAddress: + return self.w3.to_checksum_address(vesting_contract_address) + + +def _vesting(self: "GenLayerClient", vesting_contract_address: AddressLike): + return self.w3.eth.contract( + address=_vesting_address(self, vesting_contract_address), abi=VESTING_ABI + ) + + +def _vesting_factory(self: "GenLayerClient", vesting_factory_address: AddressLike): + return self.w3.eth.contract( + address=self.w3.to_checksum_address(vesting_factory_address), + abi=VESTING_ABI, + ) + + +def _extra_cid(extra_cid: Optional[ExtraCidLike]) -> bytes: + if not extra_cid: + return b"" + if isinstance(extra_cid, str): + if extra_cid.startswith("0x"): + return bytes(HexBytes(extra_cid)) + return extra_cid.encode() + return bytes(extra_cid) + + +def _sender( + self: "GenLayerClient", account: Optional[LocalAccount] +) -> LocalAccount: + acct = account or self.local_account + if acct is None: + raise GenLayerError("No account provided and client has no local_account") + return acct + + +def _send( + self: "GenLayerClient", + account: LocalAccount, + tx: dict, +) -> HexBytes: + """Sign and broadcast a prepared transaction dict.""" + signed = account.sign_transaction(tx) + return self.w3.eth.send_raw_transaction(signed.raw_transaction) + + +def _build( + self: "GenLayerClient", + account: LocalAccount, + to: ChecksumAddress, + data: bytes, + value: int = 0, + gas: Optional[int] = None, +) -> dict: + tx = { + "from": account.address, + "to": to, + "data": data, + "value": value, + "nonce": self.w3.eth.get_transaction_count(account.address), + "chainId": self.chain.id, + } + # Lean on the node's eth_estimateGas unless caller overrode it. + tx["gas"] = gas if gas is not None else self.w3.eth.estimate_gas(tx) * 2 + tx["gasPrice"] = self.w3.eth.gas_price + return tx + + +# --- read methods ----------------------------------------------------- + + +def vested_amount( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> int: + return _vesting(self, vesting_contract_address).functions.vestedAmount().call() + + +def unvested_amount( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> int: + return _vesting(self, vesting_contract_address).functions.unvestedAmount().call() + + +def withdrawable_amount( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> int: + return ( + _vesting(self, vesting_contract_address) + .functions.withdrawableAmount() + .call() + ) + + +def get_vesting_schedule( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> dict: + contract = _vesting(self, vesting_contract_address) + return { + "name": contract.functions.name().call(), + "category": contract.functions.category().call(), + "beneficiary": contract.functions.beneficiary().call(), + "creator": contract.functions.creator().call(), + "revoker": contract.functions.revoker().call(), + "factory": contract.functions.factory().call(), + "total_amount": contract.functions.totalAmount().call(), + "start_date": contract.functions.startDate().call(), + "cliff_duration": contract.functions.cliffDuration().call(), + "period_duration": contract.functions.periodDuration().call(), + "number_of_periods": contract.functions.numberOfPeriods().call(), + "cliff_unlock_bps": contract.functions.cliffUnlockBps().call(), + "needs_manual_unlock": contract.functions.needsManualUnlock().call(), + } + + +def get_vesting_state( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> dict: + contract = _vesting(self, vesting_contract_address) + return { + "manual_unlocked": contract.functions.manualUnlocked().call(), + "revoked": contract.functions.revoked().call(), + "vesting_stopped": contract.functions.vestingStopped().call(), + "total_withdrawn": contract.functions.totalWithdrawn().call(), + "vested_at_revocation": contract.functions.vestedAtRevocation().call(), + "total_amount_at_revocation": contract.functions.totalAmountAtRevocation() + .call(), + "revoked_at": contract.functions.revokedAt().call(), + "vesting_stopped_at": contract.functions.vestingStoppedAt().call(), + "vested_at_stop": contract.functions.vestedAtStop().call(), + "accumulated_rewards": contract.functions.accumulatedRewards().call(), + "accumulated_losses": contract.functions.accumulatedLosses().call(), + "vested_amount": contract.functions.vestedAmount().call(), + "unvested_amount": contract.functions.unvestedAmount().call(), + "withdrawable_amount": contract.functions.withdrawableAmount().call(), + } + + +def get_vesting_stake_info( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + validator: AddressLike, +) -> dict: + contract = _vesting(self, vesting_contract_address) + validator_address = self.w3.to_checksum_address(validator) + return { + "deposited": contract.functions.depositedPerValidator( + validator_address + ).call(), + "pending_exit_deposited": contract.functions.pendingExitDeposited( + validator_address + ).call(), + } + + +def get_validator_wallets( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> List[ChecksumAddress]: + return ( + _vesting(self, vesting_contract_address) + .functions.getValidatorWallets() + .call() + ) + + +def validator_wallet_count( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> int: + return ( + _vesting(self, vesting_contract_address) + .functions.validatorWalletCount() + .call() + ) + + +def validator_deposited( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, +) -> int: + return ( + _vesting(self, vesting_contract_address) + .functions.validatorDeposited(self.w3.to_checksum_address(wallet)) + .call() + ) + + +def is_validator_wallet( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, +) -> bool: + return ( + _vesting(self, vesting_contract_address) + .functions.isValidatorWallet(self.w3.to_checksum_address(wallet)) + .call() + ) + + +def get_vesting_contract( + self: "GenLayerClient", + vesting_factory_address: AddressLike, + beneficiary: AddressLike, +) -> ChecksumAddress: + contract = _vesting_factory(self, vesting_factory_address) + vesting_contract_address = contract.functions.getVesting( + self.w3.to_checksum_address(beneficiary) + ).call() + return self.w3.to_checksum_address(vesting_contract_address) + + +# --- write methods ---------------------------------------------------- + + +def vesting_delegator_join( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + validator: AddressLike, + amount: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Delegates `amount` GEN from a Vesting contract to a validator.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingDelegatorJoin", + args=[self.w3.to_checksum_address(validator), amount], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_delegator_exit( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + validator: AddressLike, + shares: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Burns `shares` of a Vesting contract's delegation position.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingDelegatorExit", + args=[self.w3.to_checksum_address(validator), shares], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_delegator_claim( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + validator: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Claims exited staking funds back into the Vesting contract.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingDelegatorClaim", args=[self.w3.to_checksum_address(validator)] + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_join( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + operator: AddressLike, + amount: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Creates a validator wallet using GEN from a Vesting contract.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorJoin", + args=[self.w3.to_checksum_address(operator), amount], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_deposit( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + amount: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Adds Vesting-held GEN to one of the Vesting validator wallets.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorDeposit", + args=[self.w3.to_checksum_address(wallet), amount], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_exit( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + shares: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Burns `shares` from a Vesting-owned validator wallet.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorExit", + args=[self.w3.to_checksum_address(wallet), shares], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_claim( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Claims exited validator self-stake back into the Vesting contract.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorClaim", args=[self.w3.to_checksum_address(wallet)] + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_initiate_operator_transfer( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + new_operator: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Begins operator transfer for a Vesting-owned validator wallet.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorInitiateOperatorTransfer", + args=[ + self.w3.to_checksum_address(wallet), + self.w3.to_checksum_address(new_operator), + ], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_complete_operator_transfer( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Completes operator transfer for a Vesting-owned validator wallet.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorCompleteOperatorTransfer", + args=[self.w3.to_checksum_address(wallet)], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_cancel_operator_transfer( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Cancels operator transfer for a Vesting-owned validator wallet.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorCancelOperatorTransfer", + args=[self.w3.to_checksum_address(wallet)], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_set_identity( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + moniker: str, + logo_uri: str, + website: str, + description: str, + email: str, + twitter: str, + telegram: str, + github: str, + extra_cid: Optional[ExtraCidLike] = None, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Sets identity metadata on a Vesting-owned validator wallet.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorSetIdentity", + args=[ + self.w3.to_checksum_address(wallet), + moniker, + logo_uri, + website, + description, + email, + twitter, + telegram, + github, + _extra_cid(extra_cid), + ], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_withdraw( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + amount: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Withdraws vested tokens from a Vesting contract to its beneficiary.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi("vestingWithdraw", args=[amount]) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) diff --git a/pyproject.toml b/pyproject.toml index 7024068..8b401c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ where = ["."] exclude = ["tests*", "scripts*"] [tool.setuptools.package-data] -"genlayer_py" = ["consensus/abi/*.json", "staking/abi/*.json"] +"genlayer_py" = ["consensus/abi/*.json", "staking/abi/*.json", "vesting/abi/*.json"] [dependency-groups] dev = [ diff --git a/tests/unit/vesting/__init__.py b/tests/unit/vesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/vesting/test_vesting_actions.py b/tests/unit/vesting/test_vesting_actions.py new file mode 100644 index 0000000..68be07e --- /dev/null +++ b/tests/unit/vesting/test_vesting_actions.py @@ -0,0 +1,559 @@ +"""Unit tests for the vesting action module. + +The tests here are structural, matching the staking action tests: write +helpers are checked for ABI selectors and target addresses, while read +helpers use mocked contract calls instead of a live node. +""" + +from types import SimpleNamespace +from unittest.mock import Mock + +from eth_utils import keccak +import pytest +from web3 import Web3 + +import genlayer_py.vesting.actions as vesting_actions +from genlayer_py.vesting.abi import VESTING_ABI + + +VESTING_ADDR = "0x1111111111111111111111111111111111111111" +FACTORY_ADDR = "0x2222222222222222222222222222222222222222" +SENDER_ADDR = "0x3333333333333333333333333333333333333333" +VALIDATOR_ADDR = "0x4444444444444444444444444444444444444444" +BENEFICIARY_ADDR = "0x5555555555555555555555555555555555555555" +CREATOR_ADDR = "0x6666666666666666666666666666666666666666" +REVOKER_ADDR = "0x7777777777777777777777777777777777777777" +WALLET_ADDR = "0x8888888888888888888888888888888888888888" +NEW_OPERATOR_ADDR = "0x9999999999999999999999999999999999999999" + +SEL_VESTING_DELEGATOR_JOIN = keccak( + text="vestingDelegatorJoin(address,uint256)" +)[:4].hex() +SEL_VESTING_DELEGATOR_EXIT = keccak( + text="vestingDelegatorExit(address,uint256)" +)[:4].hex() +SEL_VESTING_DELEGATOR_CLAIM = keccak(text="vestingDelegatorClaim(address)")[ + :4 +].hex() +SEL_VESTING_VALIDATOR_JOIN = keccak( + text="vestingValidatorJoin(address,uint256)" +)[:4].hex() +SEL_VESTING_VALIDATOR_DEPOSIT = keccak( + text="vestingValidatorDeposit(address,uint256)" +)[:4].hex() +SEL_VESTING_VALIDATOR_EXIT = keccak( + text="vestingValidatorExit(address,uint256)" +)[:4].hex() +SEL_VESTING_VALIDATOR_CLAIM = keccak(text="vestingValidatorClaim(address)")[ + :4 +].hex() +SEL_VESTING_VALIDATOR_INITIATE_OPERATOR_TRANSFER = keccak( + text="vestingValidatorInitiateOperatorTransfer(address,address)" +)[:4].hex() +SEL_VESTING_VALIDATOR_COMPLETE_OPERATOR_TRANSFER = keccak( + text="vestingValidatorCompleteOperatorTransfer(address)" +)[:4].hex() +SEL_VESTING_VALIDATOR_CANCEL_OPERATOR_TRANSFER = keccak( + text="vestingValidatorCancelOperatorTransfer(address)" +)[:4].hex() +SEL_VESTING_VALIDATOR_SET_IDENTITY = keccak( + text=( + "vestingValidatorSetIdentity(" + "address,string,string,string,string,string,string,string,string,bytes)" + ) +)[:4].hex() +SEL_VESTING_WITHDRAW = keccak(text="vestingWithdraw(uint256)")[:4].hex() + + +def _make_client(): + """SimpleNamespace stand-in for GenLayerClient. + + Uses a real Web3().eth for contract encoding, but patches the eth + methods that would otherwise hit a live node. + """ + signed = SimpleNamespace(raw_transaction=b"\x00") + w3 = Web3() + w3.eth.get_transaction_count = Mock(return_value=1) + w3.eth.estimate_gas = Mock(return_value=100_000) + w3.eth.send_raw_transaction = Mock(return_value=b"\xde\xad" * 16) + type(w3.eth).gas_price = 1_000_000_000 # type: ignore[assignment] + + local_account = SimpleNamespace( + address=SENDER_ADDR, + sign_transaction=Mock(return_value=signed), + ) + chain = SimpleNamespace(id=61999) + return SimpleNamespace(chain=chain, local_account=local_account, w3=w3) + + +def _last_tx(client): + """Return the tx dict the client's sign_transaction was called with.""" + return client.local_account.sign_transaction.call_args.args[0] + + +def _call_result(value): + return SimpleNamespace(call=Mock(return_value=value)) + + +def _decode_vesting_tx(client, data): + contract = client.w3.eth.contract( + address=client.w3.to_checksum_address(VESTING_ADDR), abi=VESTING_ABI + ) + return contract.decode_function_input(data) + + +def test_vesting_delegator_join_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_delegator_join( + self=client, + vesting_contract_address=VESTING_ADDR, + validator=VALIDATOR_ADDR, + amount=10, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["value"] == 0 + assert tx["data"][2:10] == SEL_VESTING_DELEGATOR_JOIN + + +def test_vesting_delegator_exit_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_delegator_exit( + self=client, + vesting_contract_address=VESTING_ADDR, + validator=VALIDATOR_ADDR, + shares=42, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_DELEGATOR_EXIT + + +def test_vesting_delegator_claim_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_delegator_claim( + self=client, + vesting_contract_address=VESTING_ADDR, + validator=VALIDATOR_ADDR, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_DELEGATOR_CLAIM + + +def test_vesting_withdraw_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_withdraw( + self=client, + vesting_contract_address=VESTING_ADDR, + amount=7, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_WITHDRAW + + +def test_vesting_validator_join_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_join( + self=client, + vesting_contract_address=VESTING_ADDR, + operator=VALIDATOR_ADDR, + amount=10, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["value"] == 0 + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_JOIN + + +def test_vesting_validator_deposit_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_deposit( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + amount=25, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["value"] == 0 + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_DEPOSIT + + +def test_vesting_validator_exit_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_exit( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + shares=42, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_EXIT + + +def test_vesting_validator_claim_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_claim( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_CLAIM + + +def test_vesting_validator_initiate_operator_transfer_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_initiate_operator_transfer( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + new_operator=NEW_OPERATOR_ADDR, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_INITIATE_OPERATOR_TRANSFER + + +def test_vesting_validator_complete_operator_transfer_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_complete_operator_transfer( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_COMPLETE_OPERATOR_TRANSFER + + +def test_vesting_validator_cancel_operator_transfer_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_cancel_operator_transfer( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_CANCEL_OPERATOR_TRANSFER + + +def test_vesting_validator_set_identity_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_set_identity( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + moniker="validator", + logo_uri="logo", + website="site", + description="desc", + email="email", + twitter="tw", + telegram="tg", + github="gh", + extra_cid="cid", + ) + tx = _last_tx(client) + _, args = _decode_vesting_tx(client, tx["data"]) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_SET_IDENTITY + assert args["extraCid"] == b"cid" + + +def test_vesting_validator_set_identity_preserves_hex_extra_cid(): + client = _make_client() + vesting_actions.vesting_validator_set_identity( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + moniker="validator", + logo_uri="logo", + website="site", + description="desc", + email="email", + twitter="tw", + telegram="tg", + github="gh", + extra_cid="0x1234", + ) + tx = _last_tx(client) + _, args = _decode_vesting_tx(client, tx["data"]) + assert args["extraCid"] == b"\x12\x34" + + +def test_write_requires_account(): + client = _make_client() + client.local_account = None + with pytest.raises(Exception, match="No account provided"): + vesting_actions.vesting_withdraw( + self=client, + vesting_contract_address=VESTING_ADDR, + amount=7, + ) + + +def test_vested_unvested_and_withdrawable_amount_reads(): + client = _make_client() + functions = SimpleNamespace( + vestedAmount=Mock(return_value=_call_result(11)), + unvestedAmount=Mock(return_value=_call_result(22)), + withdrawableAmount=Mock(return_value=_call_result(33)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + assert ( + vesting_actions.vested_amount( + self=client, vesting_contract_address=VESTING_ADDR + ) + == 11 + ) + assert ( + vesting_actions.unvested_amount( + self=client, vesting_contract_address=VESTING_ADDR + ) + == 22 + ) + assert ( + vesting_actions.withdrawable_amount( + self=client, vesting_contract_address=VESTING_ADDR + ) + == 33 + ) + + +def test_get_vesting_schedule_reads_schedule_fields(): + client = _make_client() + functions = SimpleNamespace( + name=Mock(return_value=_call_result("Founder")), + category=Mock(return_value=_call_result(1)), + beneficiary=Mock(return_value=_call_result(BENEFICIARY_ADDR)), + creator=Mock(return_value=_call_result(CREATOR_ADDR)), + revoker=Mock(return_value=_call_result(REVOKER_ADDR)), + factory=Mock(return_value=_call_result(FACTORY_ADDR)), + totalAmount=Mock(return_value=_call_result(1000)), + startDate=Mock(return_value=_call_result(100)), + cliffDuration=Mock(return_value=_call_result(200)), + periodDuration=Mock(return_value=_call_result(300)), + numberOfPeriods=Mock(return_value=_call_result(4)), + cliffUnlockBps=Mock(return_value=_call_result(500)), + needsManualUnlock=Mock(return_value=_call_result(False)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + schedule = vesting_actions.get_vesting_schedule( + self=client, vesting_contract_address=VESTING_ADDR + ) + + assert schedule == { + "name": "Founder", + "category": 1, + "beneficiary": BENEFICIARY_ADDR, + "creator": CREATOR_ADDR, + "revoker": REVOKER_ADDR, + "factory": FACTORY_ADDR, + "total_amount": 1000, + "start_date": 100, + "cliff_duration": 200, + "period_duration": 300, + "number_of_periods": 4, + "cliff_unlock_bps": 500, + "needs_manual_unlock": False, + } + + +def test_get_vesting_state_reads_state_fields(): + client = _make_client() + functions = SimpleNamespace( + manualUnlocked=Mock(return_value=_call_result(True)), + revoked=Mock(return_value=_call_result(False)), + vestingStopped=Mock(return_value=_call_result(False)), + totalWithdrawn=Mock(return_value=_call_result(1)), + vestedAtRevocation=Mock(return_value=_call_result(2)), + totalAmountAtRevocation=Mock(return_value=_call_result(3)), + revokedAt=Mock(return_value=_call_result(4)), + vestingStoppedAt=Mock(return_value=_call_result(5)), + vestedAtStop=Mock(return_value=_call_result(6)), + accumulatedRewards=Mock(return_value=_call_result(7)), + accumulatedLosses=Mock(return_value=_call_result(8)), + vestedAmount=Mock(return_value=_call_result(9)), + unvestedAmount=Mock(return_value=_call_result(10)), + withdrawableAmount=Mock(return_value=_call_result(11)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + state = vesting_actions.get_vesting_state( + self=client, vesting_contract_address=VESTING_ADDR + ) + + assert state == { + "manual_unlocked": True, + "revoked": False, + "vesting_stopped": False, + "total_withdrawn": 1, + "vested_at_revocation": 2, + "total_amount_at_revocation": 3, + "revoked_at": 4, + "vesting_stopped_at": 5, + "vested_at_stop": 6, + "accumulated_rewards": 7, + "accumulated_losses": 8, + "vested_amount": 9, + "unvested_amount": 10, + "withdrawable_amount": 11, + } + + +def test_get_vesting_stake_info_reads_validator_state(): + client = _make_client() + functions = SimpleNamespace( + depositedPerValidator=Mock(return_value=_call_result(123)), + pendingExitDeposited=Mock(return_value=_call_result(45)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + stake_info = vesting_actions.get_vesting_stake_info( + self=client, + vesting_contract_address=VESTING_ADDR, + validator=VALIDATOR_ADDR, + ) + + assert stake_info == {"deposited": 123, "pending_exit_deposited": 45} + checksum_validator = client.w3.to_checksum_address(VALIDATOR_ADDR) + functions.depositedPerValidator.assert_called_once_with(checksum_validator) + functions.pendingExitDeposited.assert_called_once_with(checksum_validator) + + +def test_get_validator_wallets_reads_wallet_list(): + client = _make_client() + functions = SimpleNamespace( + getValidatorWallets=Mock(return_value=_call_result([WALLET_ADDR])), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + wallets = vesting_actions.get_validator_wallets( + self=client, vesting_contract_address=VESTING_ADDR + ) + + assert wallets == [WALLET_ADDR] + + +def test_validator_wallet_count_reads_wallet_count(): + client = _make_client() + functions = SimpleNamespace( + validatorWalletCount=Mock(return_value=_call_result(3)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + assert ( + vesting_actions.validator_wallet_count( + self=client, vesting_contract_address=VESTING_ADDR + ) + == 3 + ) + + +def test_validator_deposited_reads_wallet_deposit(): + client = _make_client() + functions = SimpleNamespace( + validatorDeposited=Mock(return_value=_call_result(123)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + deposited = vesting_actions.validator_deposited( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + ) + + assert deposited == 123 + functions.validatorDeposited.assert_called_once_with( + client.w3.to_checksum_address(WALLET_ADDR) + ) + + +def test_is_validator_wallet_reads_membership(): + client = _make_client() + functions = SimpleNamespace( + isValidatorWallet=Mock(return_value=_call_result(True)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + is_wallet = vesting_actions.is_validator_wallet( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + ) + + assert is_wallet is True + functions.isValidatorWallet.assert_called_once_with( + client.w3.to_checksum_address(WALLET_ADDR) + ) + + +def test_get_vesting_contract_reads_factory_mapping(): + client = _make_client() + functions = SimpleNamespace( + getVesting=Mock(return_value=_call_result(VESTING_ADDR)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + vesting_contract = vesting_actions.get_vesting_contract( + self=client, + vesting_factory_address=FACTORY_ADDR, + beneficiary=BENEFICIARY_ADDR, + ) + + assert vesting_contract == client.w3.to_checksum_address(VESTING_ADDR) + client.w3.eth.contract.assert_called_once_with( + address=client.w3.to_checksum_address(FACTORY_ADDR), abi=VESTING_ABI + ) + functions.getVesting.assert_called_once_with( + client.w3.to_checksum_address(BENEFICIARY_ADDR) + ) + + +def test_abi_includes_expected_functions_and_events(): + """Guard against the bundled ABI JSON drifting or truncating.""" + names = {e["name"] for e in VESTING_ABI if e.get("type") == "function"} + events = {e["name"] for e in VESTING_ABI if e.get("type") == "event"} + assert { + "vestingDelegatorJoin", + "vestingDelegatorExit", + "vestingDelegatorClaim", + "vestingValidatorJoin", + "vestingValidatorDeposit", + "vestingValidatorExit", + "vestingValidatorClaim", + "vestingValidatorInitiateOperatorTransfer", + "vestingValidatorCompleteOperatorTransfer", + "vestingValidatorCancelOperatorTransfer", + "vestingValidatorSetIdentity", + "vestingWithdraw", + "vestedAmount", + "unvestedAmount", + "withdrawableAmount", + "depositedPerValidator", + "pendingExitDeposited", + "getValidatorWallets", + "validatorWalletCount", + "validatorDeposited", + "isValidatorWallet", + "getVesting", + }.issubset(names) + assert { + "TokensWithdrawn", + "DelegatorJoined", + "DelegatorExited", + "DelegatorClaimed", + "ValidatorJoined", + "ValidatorDeposited", + "ValidatorExited", + "ValidatorClaimed", + "VestingCreated", + }.issubset(events) From ef36623ac7e36fb36701449f66bb61f6518d9d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Nem=C5=A1e?= Date: Thu, 23 Jul 2026 11:05:44 +0100 Subject: [PATCH 09/12] feat(calldata)!: migrate method-call key "method" -> "" (v0.6 genvm ABI) (#96) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(calldata)!: migrate method-call key "method" -> "" (v0.6 genvm ABI) The GenVM v0.6 calldata ABI renames the method-call key from "method" to "" (empty string). Because the calldata encoder sorts map keys ascending, "" sorts first, so the method name becomes the binary prefix of the encoded calldata. genvm-manager auto-remaps "method" -> "" but logs an error on every call; SDKs must migrate to stop the error and match the canonical wire format. - make_calldata_object (genlayer_py/contracts/utils.py) is the sole producer of the method key; it now emits ret[""] = method. This flows through every method-call path (write/read/sim/estimate + encode_tx_data_call). - The calldata encoder/decoder are key-agnostic and unchanged; the decoder round-trips whatever key is present, so decoding auto-adapts. - Updated consensus/smoke round-trip assertions to read call_data[""] and added test_method_key_is_empty_string_binary_prefix, which asserts the "" key is present, "method" is absent, and the method name precedes the "args" key bytes in the raw calldata (proving "" sorts first). This test fails under the old key. - Regenerated the raw / base64 / readable calldata fixtures to the new wire format ({"": "ask_for_coin", ...}) across the write sample-data files and conftest. Deploy (constructor) fixtures are unaffected — they carry no method key. No back-compat toggle: the codebase has no calldata versioning mechanism and none is introduced. BREAKING CHANGE: method-call calldata now keys the method name under "" (empty string) instead of "method", per the v0.6 GenVM calldata ABI. Calldata built by this SDK is incompatible with pre-v0.6 GenVM, and decoded calldata exposes the method name under the "" key. * test(e2e): migrate contract fixtures to genvm v0.3 SDK API The tests/e2e/contracts fixtures used the v0.2.x SDK idiom and fail under GenVM v0.6 with NameError (from genlayer import * no longer binds gl). Migrate all 15 to v0.3: - from genlayer import * -> import genlayer as gl + from genlayer.types import * - gl.Contract -> gl.contract.Contract; gl.ContractAt -> gl.contract.get_at; gl.deploy_contract -> gl.contract.deploy - gl.DynArray/TreeMap -> gl.storage.*; @allow_storage -> @gl.storage.allow - gl.eq_principle_* -> gl.eq_principle.*; gl.exec_prompt -> gl.nondet.exec_prompt; gl.get_webpage -> gl.nondet.web.render - drop callable u256(x) wrapping (aliases are Annotated[int], not callable) multi_tenant_storage.py additionally dropped the removed emit(gas=...) kwarg (v0.3 emit takes value/on/use_balance/fee_params; a normal internal message's fee comes from the sender's prefunded pool, no per-emit gas limit). The py-genlayer:test Depends placeholder is unchanged. --------- Co-authored-by: kp2pml30 --- genlayer_py/contracts/utils.py | 2 +- .../contracts/football_prediction_market.py | 11 +- tests/e2e/contracts/intelligent_oracle.py | 23 +- .../contracts/intelligent_oracle_factory.py | 9 +- tests/e2e/contracts/llm_erc20.py | 11 +- tests/e2e/contracts/log_indexer.py | 11 +- tests/e2e/contracts/log_indexer_testnet.py | 13 +- .../contracts/multi_file_contract/__init__.py | 9 +- .../contracts/multi_file_contract/other.py | 5 +- tests/e2e/contracts/multi_read_erc20.py | 9 +- tests/e2e/contracts/multi_tenant_storage.py | 15 +- tests/e2e/contracts/read_erc20.py | 7 +- tests/e2e/contracts/simple_time_contract.py | 5 +- tests/e2e/contracts/storage.py | 5 +- tests/e2e/contracts/user_storage.py | 7 +- tests/e2e/contracts/wizard_of_coin.py | 9 +- tests/unit/conftest.py | 2 +- tests/unit/consensus/test_consensus_main.py | 31 ++- .../full_write_transaction_data.py | 230 ++---------------- .../sample_data/raw_write_transaction_data.py | 26 +- .../simplified_write_transaction_data.py | 6 +- tests/unit/smoke/test_testnet_smoke.py | 2 +- 22 files changed, 148 insertions(+), 300 deletions(-) diff --git a/genlayer_py/contracts/utils.py b/genlayer_py/contracts/utils.py index 7e79432..2f02317 100644 --- a/genlayer_py/contracts/utils.py +++ b/genlayer_py/contracts/utils.py @@ -9,7 +9,7 @@ def make_calldata_object( ) -> CalldataEncodable: ret: Dict[str, CalldataEncodable] = {} if method is not None: - ret["method"] = method + ret[""] = method if args is not None and len(args) > 0: ret["args"] = args if kwargs is not None and isinstance(kwargs, dict) and kwargs: diff --git a/tests/e2e/contracts/football_prediction_market.py b/tests/e2e/contracts/football_prediction_market.py index 758c9ef..8e7e2af 100644 --- a/tests/e2e/contracts/football_prediction_market.py +++ b/tests/e2e/contracts/football_prediction_market.py @@ -1,12 +1,13 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * import json import typing -class PredictionMarket(gl.Contract): +class PredictionMarket(gl.contract.Contract): has_resolved: bool team1: str team2: str @@ -50,7 +51,7 @@ def resolve(self) -> typing.Any: team2 = self.team2 def get_match_result() -> str: - web_data = gl.get_webpage(market_resolution_url, mode="text") + web_data = gl.nondet.web.render(market_resolution_url, mode="text") print(web_data) task = f""" @@ -75,11 +76,11 @@ def get_match_result() -> str: your output must be only JSON without any formatting prefix or suffix. This result should be perfectly parsable by a JSON parser without errors. """ - result = gl.exec_prompt(task).replace("```json", "").replace("```", "") + result = gl.nondet.exec_prompt(task).replace("```json", "").replace("```", "") print(result) return json.dumps(json.loads(result), sort_keys=True) - result_json = json.loads(gl.eq_principle_strict_eq(get_match_result)) + result_json = json.loads(gl.eq_principle.strict_eq(get_match_result)) if result_json["winner"] > -1: self.has_resolved = True diff --git a/tests/e2e/contracts/intelligent_oracle.py b/tests/e2e/contracts/intelligent_oracle.py index ef49eb0..febbc20 100644 --- a/tests/e2e/contracts/intelligent_oracle.py +++ b/tests/e2e/contracts/intelligent_oracle.py @@ -4,7 +4,8 @@ from enum import Enum from datetime import datetime, timezone from urllib.parse import urlparse -from genlayer import * +import genlayer as gl +from genlayer.types import * class Status(Enum): @@ -13,15 +14,15 @@ class Status(Enum): ERROR = "Error" -class IntelligentOracle(gl.Contract): +class IntelligentOracle(gl.contract.Contract): # Declare persistent storage fields prediction_market_id: str title: str description: str - potential_outcomes: DynArray[str] - rules: DynArray[str] - data_source_domains: DynArray[str] - resolution_urls: DynArray[str] + potential_outcomes: gl.storage.DynArray[str] + rules: gl.storage.DynArray[str] + data_source_domains: gl.storage.DynArray[str] + resolution_urls: gl.storage.DynArray[str] earliest_resolution_date: str # Store as ISO format string status: str # Store as string since Enum isn't supported analysis: str # Store analysis results @@ -139,7 +140,7 @@ def resolve(self, evidence_url: str = "") -> None: for resource_url in resources_to_check: def evaluate_single_source() -> str: - resource_web_data = gl.get_webpage(resource_url, mode="text") + resource_web_data = gl.nondet.web.render(resource_url, mode="text") print(resource_web_data) task = f""" @@ -223,11 +224,11 @@ def evaluate_single_source() -> str: - **Clarity:** Make sure your reasoning is easy to understand. - **Validity:** Ensure the JSON output is properly formatted and free of errors. Do not include trailing commas. """ - result = gl.exec_prompt(task) + result = gl.nondet.exec_prompt(task) print(result) return result - result = gl.eq_principle_prompt_comparative( + result = gl.eq_principle.prompt_comparative( evaluate_single_source, principle="`outcome` field must be exactly the same. All other fields must be similar", ) @@ -305,11 +306,11 @@ def evaluate_all_sources() -> str: """ - result = gl.exec_prompt(task) + result = gl.nondet.exec_prompt(task) print(result) return result - result = gl.eq_principle_prompt_comparative( + result = gl.eq_principle.prompt_comparative( evaluate_all_sources, principle="`outcome` field must be exactly the same. All other fields must be similar", ) diff --git a/tests/e2e/contracts/intelligent_oracle_factory.py b/tests/e2e/contracts/intelligent_oracle_factory.py index 9cf5e3c..db54513 100644 --- a/tests/e2e/contracts/intelligent_oracle_factory.py +++ b/tests/e2e/contracts/intelligent_oracle_factory.py @@ -1,11 +1,12 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class Registry(gl.Contract): +class Registry(gl.contract.Contract): # Declare persistent storage fields - contract_addresses: DynArray[str] + contract_addresses: gl.storage.DynArray[str] intelligent_oracle_code: str def __init__(self, intelligent_oracle_code: str): @@ -24,7 +25,7 @@ def create_new_prediction_market( earliest_resolution_date: str, ) -> None: registered_contracts = len(self.contract_addresses) - contract_address = gl.deploy_contract( + contract_address = gl.contract.deploy( code=self.intelligent_oracle_code.encode("utf-8"), args=[ prediction_market_id, diff --git a/tests/e2e/contracts/llm_erc20.py b/tests/e2e/contracts/llm_erc20.py index dd28981..6dcdc1b 100644 --- a/tests/e2e/contracts/llm_erc20.py +++ b/tests/e2e/contracts/llm_erc20.py @@ -2,14 +2,15 @@ import json -from genlayer import * +import genlayer as gl +from genlayer.types import * -class LlmErc20(gl.Contract): - balances: TreeMap[Address, u256] +class LlmErc20(gl.contract.Contract): + balances: gl.storage.TreeMap[Address, u256] def __init__(self, total_supply: int) -> None: - self.balances[gl.message.sender_address] = u256(total_supply) + self.balances[gl.message.sender_address] = total_supply @gl.public.write def transfer(self, amount: int, to_address: str) -> None: @@ -47,7 +48,7 @@ def transfer(self, amount: int, to_address: str) -> None: The total sum of all balances should remain the same before and after the transaction""" final_result = ( - gl.eq_principle_prompt_non_comparative( + gl.eq_principle.prompt_non_comparative( lambda: input, task=task, criteria=criteria, diff --git a/tests/e2e/contracts/log_indexer.py b/tests/e2e/contracts/log_indexer.py index 4d5dd56..040c736 100644 --- a/tests/e2e/contracts/log_indexer.py +++ b/tests/e2e/contracts/log_indexer.py @@ -6,14 +6,15 @@ # } import numpy as np -from genlayer import * +import genlayer as gl +from genlayer.types import * import genlayer_embeddings as gle from dataclasses import dataclass import typing -@allow_storage +@gl.storage.allow @dataclass class StoreValue: log_id: u256 @@ -21,7 +22,7 @@ class StoreValue: # contract class -class LogIndexer(gl.Contract): +class LogIndexer(gl.contract.Contract): vector_store: gle.VecDB[np.float32, typing.Literal[384], StoreValue] def __init__(self): @@ -52,14 +53,14 @@ def get_closest_vector(self, text: str) -> dict | None: @gl.public.write def add_log(self, log: str, log_id: int) -> None: emb = self.get_embedding(log) - self.vector_store.insert(emb, StoreValue(text=log, log_id=u256(log_id))) + self.vector_store.insert(emb, StoreValue(text=log, log_id=log_id)) @gl.public.write def update_log(self, log_id: int, log: str) -> None: emb = self.get_embedding(log) for elem in self.vector_store.knn(emb, 2): if elem.value.text == log: - elem.value.log_id = u256(log_id) + elem.value.log_id = log_id @gl.public.write def remove_log(self, id: int) -> None: diff --git a/tests/e2e/contracts/log_indexer_testnet.py b/tests/e2e/contracts/log_indexer_testnet.py index 85f396a..e0d6878 100644 --- a/tests/e2e/contracts/log_indexer_testnet.py +++ b/tests/e2e/contracts/log_indexer_testnet.py @@ -6,13 +6,14 @@ # } import numpy as np -from genlayer import * +import genlayer as gl +from genlayer.types import * import genlayermodelwrappers from dataclasses import dataclass import typing -@allow_storage +@gl.storage.allow @dataclass class StoreValue: log_id: u256 @@ -20,8 +21,8 @@ class StoreValue: # contract class -class LogIndexer(gl.Contract): - vector_store: VecDB[np.float32, typing.Literal[384], StoreValue] +class LogIndexer(gl.contract.Contract): + vector_store: genlayermodelwrappers.VecDB[np.float32, typing.Literal[384], StoreValue] def __init__(self): pass @@ -51,14 +52,14 @@ def get_closest_vector(self, text: str) -> dict | None: @gl.public.write def add_log(self, log: str, log_id: int) -> None: emb = self.get_embedding(log) - self.vector_store.insert(emb, StoreValue(text=log, log_id=u256(log_id))) + self.vector_store.insert(emb, StoreValue(text=log, log_id=log_id)) @gl.public.write def update_log(self, log_id: int, log: str) -> None: emb = self.get_embedding(log) for elem in self.vector_store.knn(emb, 2): if elem.value.text == log: - elem.value.log_id = u256(log_id) + elem.value.log_id = log_id @gl.public.write def remove_log(self, id: int) -> None: diff --git a/tests/e2e/contracts/multi_file_contract/__init__.py b/tests/e2e/contracts/multi_file_contract/__init__.py index a5258f6..f218200 100644 --- a/tests/e2e/contracts/multi_file_contract/__init__.py +++ b/tests/e2e/contracts/multi_file_contract/__init__.py @@ -1,13 +1,14 @@ -from genlayer import * +import genlayer as gl +from genlayer.types import * -class MultiFileContract(gl.Contract): +class MultiFileContract(gl.contract.Contract): other_addr: Address def __init__(self): with open("/contract/other.py", "rt") as f: text = f.read() - self.other_addr = gl.deploy_contract( + self.other_addr = gl.contract.deploy( code=text.encode("utf-8"), args=["123"], salt_nonce=1 ) @@ -17,4 +18,4 @@ def wait(self) -> None: @gl.public.view def test(self) -> str: - return gl.ContractAt(self.other_addr).view().test() + return gl.contract.get_at(self.other_addr).view().test() diff --git a/tests/e2e/contracts/multi_file_contract/other.py b/tests/e2e/contracts/multi_file_contract/other.py index 330de34..c8dc6c7 100644 --- a/tests/e2e/contracts/multi_file_contract/other.py +++ b/tests/e2e/contracts/multi_file_contract/other.py @@ -1,9 +1,10 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class Other(gl.Contract): +class Other(gl.contract.Contract): data: str def __init__(self, data: str): diff --git a/tests/e2e/contracts/multi_read_erc20.py b/tests/e2e/contracts/multi_read_erc20.py index 169d293..22d5b32 100644 --- a/tests/e2e/contracts/multi_read_erc20.py +++ b/tests/e2e/contracts/multi_read_erc20.py @@ -1,10 +1,11 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class multi_read_erc20(gl.Contract): - balances: TreeMap[Address, TreeMap[Address, u256]] +class multi_read_erc20(gl.contract.Contract): + balances: gl.storage.TreeMap[Address, gl.storage.TreeMap[Address, u256]] def __init__(self): pass @@ -14,7 +15,7 @@ def update_token_balances( self, account_address: str, token_contracts: list[str] ) -> None: for token_contract in token_contracts: - contract = gl.ContractAt(Address(token_contract)) + contract = gl.contract.get_at(Address(token_contract)) balance = contract.view().get_balance_of(account_address) self.balances.get_or_insert_default(Address(account_address))[ Address(token_contract) diff --git a/tests/e2e/contracts/multi_tenant_storage.py b/tests/e2e/contracts/multi_tenant_storage.py index 5e5af47..8659437 100644 --- a/tests/e2e/contracts/multi_tenant_storage.py +++ b/tests/e2e/contracts/multi_tenant_storage.py @@ -1,9 +1,10 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class MultiTentantStorage(gl.Contract): +class MultiTentantStorage(gl.contract.Contract): """ Same functionality as UserStorage, but implemented with multiple storage contracts. Each user is assigned to a storage contract, and all storage contracts are managed by this same contract. @@ -11,9 +12,9 @@ class MultiTentantStorage(gl.Contract): This is done to test contract calls between different contracts. """ - all_storage_contracts: DynArray[Address] - available_storage_contracts: DynArray[Address] - mappings: TreeMap[ + all_storage_contracts: gl.storage.DynArray[Address] + available_storage_contracts: gl.storage.DynArray[Address] + mappings: gl.storage.TreeMap[ Address, Address ] # mapping of user address to storage contract address @@ -29,7 +30,7 @@ def get_available_contracts(self) -> list[str]: @gl.public.view def get_all_storages(self) -> dict[str, str]: return { - storage_contract.as_hex: gl.ContractAt(storage_contract) + storage_contract.as_hex: gl.contract.get_at(storage_contract) .view() .get_storage() for storage_contract in self.all_storage_contracts @@ -45,4 +46,4 @@ def update_storage(self, new_storage: str) -> None: self.available_storage_contracts.pop() contract_to_use = self.mappings[gl.message.sender_address] - gl.ContractAt(contract_to_use).emit(gas=100000).update_storage(new_storage) + gl.contract.get_at(contract_to_use).emit().update_storage(new_storage) diff --git a/tests/e2e/contracts/read_erc20.py b/tests/e2e/contracts/read_erc20.py index 9f2d549..3883c5f 100644 --- a/tests/e2e/contracts/read_erc20.py +++ b/tests/e2e/contracts/read_erc20.py @@ -1,9 +1,10 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class read_erc20(gl.Contract): +class read_erc20(gl.contract.Contract): token_contract: Address def __init__(self, token_contract: str): @@ -11,4 +12,4 @@ def __init__(self, token_contract: str): @gl.public.view def get_balance_of(self, account_address: str) -> int: - return gl.ContractAt(self.token_contract).view().get_balance_of(account_address) + return gl.contract.get_at(self.token_contract).view().get_balance_of(account_address) diff --git a/tests/e2e/contracts/simple_time_contract.py b/tests/e2e/contracts/simple_time_contract.py index 05bfbc0..974e801 100644 --- a/tests/e2e/contracts/simple_time_contract.py +++ b/tests/e2e/contracts/simple_time_contract.py @@ -6,10 +6,11 @@ # } from datetime import datetime, timezone -from genlayer import * +import genlayer as gl +from genlayer.types import * -class SimpleTimeContract(gl.Contract): +class SimpleTimeContract(gl.contract.Contract): """ A simple contract that demonstrates time-based function availability. """ diff --git a/tests/e2e/contracts/storage.py b/tests/e2e/contracts/storage.py index 6cc44a9..58ff12a 100644 --- a/tests/e2e/contracts/storage.py +++ b/tests/e2e/contracts/storage.py @@ -1,10 +1,11 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * # contract class -class Storage(gl.Contract): +class Storage(gl.contract.Contract): storage: str # constructor diff --git a/tests/e2e/contracts/user_storage.py b/tests/e2e/contracts/user_storage.py index 3ca9fd8..3432396 100644 --- a/tests/e2e/contracts/user_storage.py +++ b/tests/e2e/contracts/user_storage.py @@ -1,10 +1,11 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class UserStorage(gl.Contract): - storage: TreeMap[Address, str] +class UserStorage(gl.contract.Contract): + storage: gl.storage.TreeMap[Address, str] # constructor def __init__(self): diff --git a/tests/e2e/contracts/wizard_of_coin.py b/tests/e2e/contracts/wizard_of_coin.py index 6194818..bff7e3f 100644 --- a/tests/e2e/contracts/wizard_of_coin.py +++ b/tests/e2e/contracts/wizard_of_coin.py @@ -1,10 +1,11 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * import json -class WizardOfCoin(gl.Contract): +class WizardOfCoin(gl.contract.Contract): have_coin: bool def __init__(self, have_coin: bool): @@ -39,12 +40,12 @@ def ask_for_coin(self, request: str) -> None: """ def get_wizard_answer(): - result = gl.exec_prompt(prompt) + result = gl.nondet.exec_prompt(prompt) result = result.replace("```json", "").replace("```", "") print(result) return result - result = gl.eq_principle_prompt_comparative( + result = gl.eq_principle.prompt_comparative( get_wizard_answer, "The value of give_coin has to match" ) parsed_result = json.loads(result) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index e182e24..ff06a60 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -101,7 +101,7 @@ def sample_transaction_data(): "created_at": "2025-07-22T19:58:39.866436+00:00", "data": { "calldata": { - "readable": '{"args":[500,"0x4C76986555e8C63bD0D9CFAFbf8e68C338556b6b"],"method":"transfer"}' + "readable": '{"":"transfer","args":[500,"0x4C76986555e8C63bD0D9CFAFbf8e68C338556b6b"]}' } }, "contract_snapshot": { diff --git a/tests/unit/consensus/test_consensus_main.py b/tests/unit/consensus/test_consensus_main.py index dd37be8..ccaf909 100644 --- a/tests/unit/consensus/test_consensus_main.py +++ b/tests/unit/consensus/test_consensus_main.py @@ -3,6 +3,7 @@ encode_tx_data_call, encode_tx_data_deploy, ) +from genlayer_py.abi import calldata from genlayer_py.consensus.consensus_main import decode_add_transaction_data from genlayer_py.transactions import MessageType from web3 import Web3 @@ -39,7 +40,7 @@ def test_codec_add_transaction_data_1(): ) assert decoded_data["num_of_initial_validators"] == 10 assert decoded_data["max_rotations"] == 10 - assert decoded_data["tx_data"]["decoded"]["call_data"]["method"] == "addTransaction" + assert decoded_data["tx_data"]["decoded"]["call_data"][""] == "addTransaction" assert decoded_data["tx_data"]["decoded"]["leader_only"] is False assert decoded_data["tx_data"]["decoded"]["type"] == "call" @@ -73,7 +74,7 @@ def test_codec_add_transaction_data_2(): ) assert decoded_data["num_of_initial_validators"] == 5 assert decoded_data["max_rotations"] == 1 - assert decoded_data["tx_data"]["decoded"]["call_data"]["method"] == "set_store" + assert decoded_data["tx_data"]["decoded"]["call_data"][""] == "set_store" assert decoded_data["tx_data"]["decoded"]["leader_only"] is False assert decoded_data["tx_data"]["decoded"]["type"] == "call" @@ -158,4 +159,28 @@ def test_codec_fee_aware_add_transaction_data_round_trip(): assert decoded_data["fees_distribution"]["totalMessageFees"] == 11 assert decoded_data["message_allocations"][0]["messageType"] == MessageType.Internal assert decoded_data["message_allocations"][0]["onAcceptance"] is False - assert decoded_data["tx_data"]["decoded"]["call_data"]["method"] == "set_store" + assert decoded_data["tx_data"]["decoded"]["call_data"][""] == "set_store" + + +def test_method_key_is_empty_string_binary_prefix(): + tx_data = encode_tx_data_call( + function_name="my_method", + leader_only=False, + args=["value"], + kwargs={}, + ) + encoded_data = encode_add_transaction_data( + sender_address="0x3d338dea364bdac3c4c3036a38766870b98c4320", + recipient_address="0x7f3ebb777cd2ae9c266d6cea2c7a3ed81c30ddc2", + num_of_initial_validators=5, + max_rotations=1, + tx_data=tx_data.hex() if isinstance(tx_data, bytes) else tx_data, + ) + + decoded_data = decode_add_transaction_data(encoded_data) + call_data = decoded_data["tx_data"]["decoded"]["call_data"] + assert "" in call_data and call_data[""] == "my_method" + assert 'method' not in call_data + + raw = calldata.encode(call_data) + assert b"my_method" in raw and raw.index(b"my_method") < raw.index(b"args") diff --git a/tests/unit/sample_data/full_write_transaction_data.py b/tests/unit/sample_data/full_write_transaction_data.py index 3b6cca5..4c1ce8e 100644 --- a/tests/unit/sample_data/full_write_transaction_data.py +++ b/tests/unit/sample_data/full_write_transaction_data.py @@ -11,73 +11,9 @@ "leader_receipt": [ { "calldata": { - "base64": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", - "raw": [ - 22, - 4, - 97, - 114, - 103, - 115, - 13, - 148, - 2, - 67, - 97, - 110, - 32, - 121, - 111, - 117, - 32, - 112, - 108, - 101, - 97, - 115, - 101, - 32, - 103, - 105, - 118, - 101, - 32, - 109, - 101, - 32, - 121, - 111, - 117, - 114, - 32, - 99, - 111, - 105, - 110, - 32, - 63, - 6, - 109, - 101, - 116, - 104, - 111, - 100, - 100, - 97, - 115, - 107, - 95, - 102, - 111, - 114, - 95, - 99, - 111, - 105, - 110, - ], - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}', + "base64": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", + "raw": [22, 0, 100, 97, 115, 107, 95, 102, 111, 114, 95, 99, 111, 105, 110, 4, 97, 114, 103, 115, 13, 148, 2, 67, 97, 110, 32, 121, 111, 117, 32, 112, 108, 101, 97, 115, 101, 32, 103, 105, 118, 101, 32, 109, 101, 32, 121, 111, 117, 114, 32, 99, 111, 105, 110, 32, 63], + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}', }, "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", @@ -236,73 +172,9 @@ }, { "calldata": { - "base64": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", - "raw": [ - 22, - 4, - 97, - 114, - 103, - 115, - 13, - 148, - 2, - 67, - 97, - 110, - 32, - 121, - 111, - 117, - 32, - 112, - 108, - 101, - 97, - 115, - 101, - 32, - 103, - 105, - 118, - 101, - 32, - 109, - 101, - 32, - 121, - 111, - 117, - 114, - 32, - 99, - 111, - 105, - 110, - 32, - 63, - 6, - 109, - 101, - 116, - 104, - 111, - 100, - 100, - 97, - 115, - 107, - 95, - 102, - 111, - 114, - 95, - 99, - 111, - 105, - 110, - ], - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}', + "base64": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", + "raw": [22, 0, 100, 97, 115, 107, 95, 102, 111, 114, 95, 99, 111, 105, 110, 4, 97, 114, 103, 115, 13, 148, 2, 67, 97, 110, 32, 121, 111, 117, 32, 112, 108, 101, 97, 115, 101, 32, 103, 105, 118, 101, 32, 109, 101, 32, 121, 111, 117, 114, 32, 99, 111, 105, 110, 32, 63], + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}', }, "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", @@ -342,7 +214,7 @@ ], "validators": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -375,7 +247,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -408,7 +280,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -441,7 +313,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -488,7 +360,7 @@ "consensus_round": "Accepted", "leader_result": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -523,7 +395,7 @@ "vote": None, }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -566,7 +438,7 @@ ], "validator_results": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -599,7 +471,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -632,7 +504,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -665,7 +537,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -725,73 +597,9 @@ "current_timestamp": "1753284800", "data": { "calldata": { - "base64": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", - "raw": [ - 22, - 4, - 97, - 114, - 103, - 115, - 13, - 148, - 2, - 67, - 97, - 110, - 32, - 121, - 111, - 117, - 32, - 112, - 108, - 101, - 97, - 115, - 101, - 32, - 103, - 105, - 118, - 101, - 32, - 109, - 101, - 32, - 121, - 111, - 117, - 114, - 32, - 99, - 111, - 105, - 110, - 32, - 63, - 6, - 109, - 101, - 116, - 104, - 111, - 100, - 100, - 97, - 115, - 107, - 95, - 102, - 111, - 114, - 95, - 99, - 111, - 105, - 110, - ], - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}', + "base64": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", + "raw": [22, 0, 100, 97, 115, 107, 95, 102, 111, 114, 95, 99, 111, 105, 110, 4, 97, 114, 103, 115, 13, 148, 2, 67, 97, 110, 32, 121, 111, 117, 32, 112, 108, 101, 97, 115, 101, 32, 103, 105, 118, 101, 32, 109, 101, 32, 121, 111, 117, 114, 32, 99, 111, 105, 110, 32, 63], + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}', } }, "eq_blocks_outputs": "0xcdc6c28000c0c080c5c480c28000", diff --git a/tests/unit/sample_data/raw_write_transaction_data.py b/tests/unit/sample_data/raw_write_transaction_data.py index d27a843..e985a28 100644 --- a/tests/unit/sample_data/raw_write_transaction_data.py +++ b/tests/unit/sample_data/raw_write_transaction_data.py @@ -10,7 +10,7 @@ "consensus_data": { "leader_receipt": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -45,7 +45,7 @@ "vote": None, }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -80,7 +80,7 @@ ], "validators": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -113,7 +113,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -146,7 +146,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -179,7 +179,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -226,7 +226,7 @@ "consensus_round": "Accepted", "leader_result": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -261,7 +261,7 @@ "vote": None, }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -304,7 +304,7 @@ ], "validator_results": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -337,7 +337,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -370,7 +370,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -403,7 +403,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -462,7 +462,7 @@ "created_timestamp": "1753284283", "current_timestamp": "1753284706", "data": { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu" + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/" }, "eq_blocks_outputs": "0xcdc6c28000c0c080c5c480c28000", "from_address": "0xd650f318A0C1F940a3b6dFeA695747fA9804D685", diff --git a/tests/unit/sample_data/simplified_write_transaction_data.py b/tests/unit/sample_data/simplified_write_transaction_data.py index 1cbd100..71c61fa 100644 --- a/tests/unit/sample_data/simplified_write_transaction_data.py +++ b/tests/unit/sample_data/simplified_write_transaction_data.py @@ -32,7 +32,7 @@ "stake": 1, }, "calldata": { - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}' + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}' }, "eq_outputs": { "0": { @@ -66,7 +66,7 @@ "stake": 1, }, "calldata": { - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}' + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}' }, "eq_outputs": {}, "result": {"status": "return", "payload": {"readable": "null"}}, @@ -169,7 +169,7 @@ "created_at": "2025-07-23T15:24:43.501990+00:00", "data": { "calldata": { - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}' + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}' } }, "from_address": "0xd650f318A0C1F940a3b6dFeA695747fA9804D685", diff --git a/tests/unit/smoke/test_testnet_smoke.py b/tests/unit/smoke/test_testnet_smoke.py index 09dce89..5f87aef 100644 --- a/tests/unit/smoke/test_testnet_smoke.py +++ b/tests/unit/smoke/test_testnet_smoke.py @@ -135,7 +135,7 @@ def test_round_trip_call(self): assert decoded["recipient_address"].lower() == recipient.lower() assert decoded["num_of_initial_validators"] == num_validators assert decoded["max_rotations"] == max_rotations - assert decoded["tx_data"]["decoded"]["call_data"]["method"] == "my_method" + assert decoded["tx_data"]["decoded"]["call_data"][""] == "my_method" assert decoded["tx_data"]["decoded"]["leader_only"] is True assert decoded["tx_data"]["decoded"]["type"] == "call" From 3106bc87955d612b5bb43fa67e225b659b657c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Nem=C5=A1e?= Date: Sun, 26 Jul 2026 13:55:05 +0100 Subject: [PATCH 10/12] fix: resolve v0.19-dev bug-hunt findings (#99) * test: add v0.19 bug hunt regressions * fix: resolve v0.19 bug hunt regressions --- genlayer_py/abi/calldata/decoder.py | 27 ++++++---- genlayer_py/client/client.py | 3 +- genlayer_py/contracts/actions.py | 3 +- genlayer_py/transactions/actions.py | 13 +++++ tests/unit/test_bug_hunt_v019.py | 76 +++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 tests/unit/test_bug_hunt_v019.py diff --git a/genlayer_py/abi/calldata/decoder.py b/genlayer_py/abi/calldata/decoder.py index c993ac8..0772217 100644 --- a/genlayer_py/abi/calldata/decoder.py +++ b/genlayer_py/abi/calldata/decoder.py @@ -8,11 +8,24 @@ def decode(mem0: Buffer) -> CalldataEncodable: mem: memoryview = memoryview(mem0) + def take(length: int, label: str) -> memoryview: + nonlocal mem + if len(mem) < length: + raise GenLayerError( + f"truncated calldata while reading {label}: " + f"expected {length} bytes, found {len(mem)}" + ) + result = mem[:length] + mem = mem[length:] + return result + def read_uleb128() -> int: nonlocal mem ret = 0 off = 0 while True: + if len(mem) == 0: + raise GenLayerError("unexpected end of calldata while reading ULEB128") m = mem[0] ret = ret | ((m & 0x7F) << off) off += 7 @@ -33,9 +46,7 @@ def impl() -> CalldataEncodable: if code == consts.SPECIAL_TRUE: return True if code == consts.SPECIAL_ADDR: - ret_addr = mem[: CalldataAddress.SIZE] - mem = mem[CalldataAddress.SIZE :] - return CalldataAddress(ret_addr) + return CalldataAddress(take(CalldataAddress.SIZE, "address")) raise GenLayerError(f"Unknown special {bin(code)} {hex(code)}") code = code >> 3 if typ == consts.TYPE_PINT: @@ -43,12 +54,9 @@ def impl() -> CalldataEncodable: elif typ == consts.TYPE_NINT: return -code - 1 elif typ == consts.TYPE_BYTES: - ret_bytes = mem[:code] - mem = mem[code:] - return ret_bytes + return take(code, "bytes") elif typ == consts.TYPE_STR: - ret_str = mem[:code] - mem = mem[code:] + ret_str = take(code, "string") return str(ret_str, encoding="utf-8") elif typ == consts.TYPE_ARR: ret_arr = [] @@ -60,8 +68,7 @@ def impl() -> CalldataEncodable: prev = None for _i in range(code): le = read_uleb128() - key = str(mem[:le], encoding="utf-8") - mem = mem[le:] + key = str(take(le, "map key"), encoding="utf-8") if prev is not None: assert prev < key prev = key diff --git a/genlayer_py/client/client.py b/genlayer_py/client/client.py index 3d6d833..92d3e1c 100644 --- a/genlayer_py/client/client.py +++ b/genlayer_py/client/client.py @@ -1,3 +1,4 @@ +from copy import deepcopy from typing import Optional from genlayer_py.types import GenLayerChain from genlayer_py.chains import localnet @@ -10,7 +11,7 @@ def create_client( endpoint: Optional[str] = None, account: Optional[LocalAccount] = None, ) -> GenLayerClient: - chain_config = chain or localnet + chain_config = deepcopy(chain or localnet) if endpoint is not None: chain_config.rpc_urls["default"]["http"] = [endpoint] client = GenLayerClient(chain_config, account) diff --git a/genlayer_py/contracts/actions.py b/genlayer_py/contracts/actions.py index b40f9c6..1679443 100644 --- a/genlayer_py/contracts/actions.py +++ b/genlayer_py/contracts/actions.py @@ -93,7 +93,8 @@ def read_contract( ) -> CalldataEncodable: if account is None and self.local_account is None: raise GenLayerError("No account provided and no account is connected") - sender_address = self.local_account.address + sender = account if account is not None else self.local_account + sender_address = sender.address data = [ calldata.encode( make_calldata_object(method=function_name, args=args, kwargs=kwargs) diff --git a/genlayer_py/transactions/actions.py b/genlayer_py/transactions/actions.py index e4e3825..826ee91 100644 --- a/genlayer_py/transactions/actions.py +++ b/genlayer_py/transactions/actions.py @@ -150,6 +150,19 @@ def is_successful(transaction: GenLayerTransaction) -> bool: transaction.get("tx_execution_result"), ) ) + + if execution_result_name is None: + consensus_data = transaction.get("consensus_data") + if isinstance(consensus_data, dict): + leader_receipt = consensus_data.get("leader_receipt") + if isinstance(leader_receipt, list): + leader_receipt = leader_receipt[0] if leader_receipt else None + if ( + isinstance(leader_receipt, dict) + and leader_receipt.get("execution_result") == "SUCCESS" + ): + execution_result_name = ExecutionResult.FINISHED_WITH_RETURN + return ( status_name in (TransactionStatus.ACCEPTED, TransactionStatus.FINALIZED) and execution_result_name == ExecutionResult.FINISHED_WITH_RETURN diff --git a/tests/unit/test_bug_hunt_v019.py b/tests/unit/test_bug_hunt_v019.py new file mode 100644 index 0000000..b0e3485 --- /dev/null +++ b/tests/unit/test_bug_hunt_v019.py @@ -0,0 +1,76 @@ +"""Regression tests for high-confidence defects found on v0.19-dev. + +These tests are intentionally red until the corresponding production defects +are fixed. +""" + +from copy import deepcopy +from types import SimpleNamespace +from unittest.mock import Mock, patch + +import pytest + +from genlayer_py.abi import calldata +from genlayer_py.chains import localnet +from genlayer_py.client.client import GenLayerClient, create_client +from genlayer_py.contracts.actions import read_contract +from genlayer_py.exceptions import GenLayerError +from genlayer_py.transactions.actions import is_successful + + +def test_create_client_endpoint_does_not_mutate_caller_chain_config(): + chain = deepcopy(localnet) + original_endpoints = list(chain.rpc_urls["default"]["http"]) + + with patch.object(GenLayerClient, "initialize_consensus_smart_contract"): + client = create_client(chain=chain, endpoint="http://override.invalid:8545") + + assert client.chain.rpc_urls["default"]["http"] == [ + "http://override.invalid:8545" + ] + assert chain.rpc_urls["default"]["http"] == original_endpoints + + +def test_read_contract_uses_explicit_account_when_client_has_no_default(): + account = SimpleNamespace( + address="0x1111111111111111111111111111111111111111" + ) + provider = Mock() + provider.make_request.return_value = {"result": ""} + client = SimpleNamespace(local_account=None, provider=provider) + + assert ( + read_contract( + self=client, + address="0x2222222222222222222222222222222222222222", + function_name="balance", + account=account, + raw_return=True, + ) + == "0x" + ) + request = provider.make_request.call_args.kwargs["params"][0] + assert request["from"] == account.address + + +def test_calldata_decoder_rejects_truncated_length_prefixed_bytes(): + declares_two_bytes_but_contains_one = bytes([(2 << 3) | 3, 0xAA]) + + with pytest.raises(GenLayerError, match="truncated|unexpected end|invalid"): + calldata.decode(declares_two_bytes_but_contains_one) + + +def test_is_successful_recognizes_localnet_consensus_receipt(): + transaction = { + "status_name": "FINALIZED", + "consensus_data": { + "leader_receipt": [ + { + "mode": "leader", + "execution_result": "SUCCESS", + } + ] + }, + } + + assert is_successful(transaction) From 3583471d5c4a8c18da54850961dee541fee1e5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Nem=C5=A1e?= Date: Sun, 26 Jul 2026 18:57:01 +0100 Subject: [PATCH 11/12] fix: include pending transactions in default nonce lookup (#100) --- genlayer_py/accounts/actions.py | 7 ++++++- tests/unit/account/test_accounts.py | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/genlayer_py/accounts/actions.py b/genlayer_py/accounts/actions.py index 55b7091..e2d8c57 100644 --- a/genlayer_py/accounts/actions.py +++ b/genlayer_py/accounts/actions.py @@ -38,4 +38,9 @@ def get_current_nonce( if address is None and self.account is None: raise GenLayerError("No address provided and no account is connected") address_to_use = address or self.account.address - return self.get_transaction_count(address_to_use, block_identifier) + # Include locally pending transactions by default so consecutive + # submissions do not reuse the same nonce before the first is mined. + resolved_block_identifier = ( + "pending" if block_identifier is None else block_identifier + ) + return self.get_transaction_count(address_to_use, resolved_block_identifier) diff --git a/tests/unit/account/test_accounts.py b/tests/unit/account/test_accounts.py index 4e0a22c..b3ec88c 100644 --- a/tests/unit/account/test_accounts.py +++ b/tests/unit/account/test_accounts.py @@ -1,7 +1,10 @@ from eth_account import Account from eth_account.signers.local import LocalAccount +from types import SimpleNamespace +from unittest.mock import Mock from genlayer_py.accounts.account import generate_private_key, create_account +from genlayer_py.accounts.actions import get_current_nonce def test_generate_private_key(): @@ -54,3 +57,25 @@ def test_create_account_with_none_private_key(): assert ( account1.address != account2.address ) # Different addresses since different random keys + + +def test_get_current_nonce_includes_pending_transactions_by_default(): + address = "0x0000000000000000000000000000000000000001" + client = SimpleNamespace( + account=SimpleNamespace(address=address), + get_transaction_count=Mock(return_value=7), + ) + + assert get_current_nonce(client) == 7 + client.get_transaction_count.assert_called_once_with(address, "pending") + + +def test_get_current_nonce_preserves_explicit_block_identifier(): + address = "0x0000000000000000000000000000000000000001" + client = SimpleNamespace( + account=None, + get_transaction_count=Mock(return_value=3), + ) + + assert get_current_nonce(client, address, "latest") == 3 + client.get_transaction_count.assert_called_once_with(address, "latest") From 2a689c0465e8267a5e79641dd754850175d55c6e Mon Sep 17 00:00:00 2001 From: KirilA Date: Tue, 18 Aug 2026 15:13:34 +0100 Subject: [PATCH 12/12] feat(staking): support the two-step operator rotation (#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CON-715 removed ValidatorWalletBlueprint.setOperator in favour of initiateOperatorTransfer + completeOperatorTransfer. Against a consensus deployment carrying that change, set_operator fails with ABIFunctionNotFound: the function 'setOperator' was not found in this contract's abi. Adds the two calls plus cancel_operator_transfer and the get_pending_operator view, and keeps set_operator for deployments that still expose it. This SDK had no proof-of-possession support at all, so the encoding is new here: genlayer_py.staking.operator_registration mirrors genlayer-js's createOperatorRegistration — keccak over abi.encode(domain, chainId, registrar, owner, pubKey[0], pubKey[1]), signed EIP-191 by the operator key. Both SDKs sign proofs the same contract verifies, so the test pins the exact vector genlayer-js asserts (domain hash, message hash and signature bytes) rather than only checking internal consistency. The registrar differs by flow and is the easy thing to get wrong: the factory verifies a validator join, the wallet verifies a rotation. get_operator_transfer_context builds the wallet-bound context and reads owner() from the wallet rather than assuming the caller is the owner, and initiate_operator_transfer verifies the proof locally so a factory-bound one fails with a clear message instead of an opaque revert. A test pins that a join proof does not verify for a rotation. Not addressed here: validator_join still encodes the retired validatorJoin() / validatorJoin(address) overloads, which CON-666 replaced with validatorJoin(uint256[2],bytes). That is the same migration and now has the helper it needs, but it changes a public signature, so it wants its own change. Verified: pytest tests/unit — 141 passing. --- genlayer_py/client/genlayer_client.py | 37 +++++ genlayer_py/staking/__init__.py | 10 ++ .../staking/abi/validator_wallet_abi.json | 50 +++++++ genlayer_py/staking/actions.py | 95 +++++++++++- genlayer_py/staking/operator_registration.py | 135 ++++++++++++++++++ tests/unit/test_operator_registration.py | 101 +++++++++++++ 6 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 genlayer_py/staking/operator_registration.py create mode 100644 tests/unit/test_operator_registration.py diff --git a/genlayer_py/client/genlayer_client.py b/genlayer_py/client/genlayer_client.py index 8445c83..a453dc3 100644 --- a/genlayer_py/client/genlayer_client.py +++ b/genlayer_py/client/genlayer_client.py @@ -52,6 +52,11 @@ validator_claim, validator_prime, set_operator, + get_operator_transfer_context, + initiate_operator_transfer, + complete_operator_transfer, + cancel_operator_transfer, + get_pending_operator, set_identity, delegator_join, delegator_exit, @@ -490,6 +495,38 @@ def set_operator( self=self, validator=validator, operator=operator, account=account ) + def get_operator_transfer_context(self, validator): + """Wallet-bound context for building a rotation proof.""" + return get_operator_transfer_context(self=self, validator=validator) + + def initiate_operator_transfer( + self, validator, registration, account: Optional[LocalAccount] = None + ) -> HexBytes: + """Starts the two-step operator rotation (CON-715).""" + return initiate_operator_transfer( + self=self, validator=validator, registration=registration, account=account + ) + + def complete_operator_transfer( + self, validator, account: Optional[LocalAccount] = None + ) -> HexBytes: + """Finalises a pending operator rotation.""" + return complete_operator_transfer( + self=self, validator=validator, account=account + ) + + def cancel_operator_transfer( + self, validator, account: Optional[LocalAccount] = None + ) -> HexBytes: + """Abandons a pending operator rotation.""" + return cancel_operator_transfer( + self=self, validator=validator, account=account + ) + + def get_pending_operator(self, validator) -> dict: + """Pending operator and when its transfer was initiated.""" + return get_pending_operator(self=self, validator=validator) + def set_identity( self, validator, moniker: str, account: Optional[LocalAccount] = None ) -> HexBytes: diff --git a/genlayer_py/staking/__init__.py b/genlayer_py/staking/__init__.py index 8c8fa92..d449eb4 100644 --- a/genlayer_py/staking/__init__.py +++ b/genlayer_py/staking/__init__.py @@ -5,6 +5,11 @@ validator_claim, validator_prime, set_operator, + get_operator_transfer_context, + initiate_operator_transfer, + complete_operator_transfer, + cancel_operator_transfer, + get_pending_operator, set_identity, delegator_join, delegator_exit, @@ -27,6 +32,11 @@ "validator_claim", "validator_prime", "set_operator", + "get_operator_transfer_context", + "initiate_operator_transfer", + "complete_operator_transfer", + "cancel_operator_transfer", + "get_pending_operator", "set_identity", "delegator_join", "delegator_exit", diff --git a/genlayer_py/staking/abi/validator_wallet_abi.json b/genlayer_py/staking/abi/validator_wallet_abi.json index cfd125a..df1cd5c 100644 --- a/genlayer_py/staking/abi/validator_wallet_abi.json +++ b/genlayer_py/staking/abi/validator_wallet_abi.json @@ -998,5 +998,55 @@ { "stateMutability": "payable", "type": "receive" + }, + { + "name": "initiateOperatorTransfer", + "type": "function", + "stateMutability": "nonpayable", + "inputs": [ + { + "name": "_newOperatorPubKey", + "type": "uint256[2]", + "internalType": "uint256[2]" + }, + { + "name": "_possessionProof", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [] + }, + { + "name": "completeOperatorTransfer", + "type": "function", + "stateMutability": "nonpayable", + "inputs": [], + "outputs": [] + }, + { + "name": "cancelOperatorTransfer", + "type": "function", + "stateMutability": "nonpayable", + "inputs": [], + "outputs": [] + }, + { + "name": "getPendingOperator", + "type": "function", + "stateMutability": "view", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ] } ] diff --git a/genlayer_py/staking/actions.py b/genlayer_py/staking/actions.py index 065b465..c3a9a64 100644 --- a/genlayer_py/staking/actions.py +++ b/genlayer_py/staking/actions.py @@ -17,6 +17,11 @@ from genlayer_py.exceptions import GenLayerError from genlayer_py.staking.abi import STAKING_ABI, VALIDATOR_WALLET_ABI +from genlayer_py.staking.operator_registration import ( + OperatorRegistrationContext, + OperatorRegistrationProof, + verify_operator_registration, +) if TYPE_CHECKING: from genlayer_py.client import GenLayerClient @@ -237,7 +242,12 @@ def set_operator( account: Optional[LocalAccount] = None, ) -> HexBytes: """Rotates the operator for an existing ValidatorWallet. Only the - wallet owner (the EOA that called validator_join) may do this.""" + wallet owner (the EOA that called validator_join) may do this. + + CON-715 removed this single call in favour of the two-step transfer + below; against a deployment carrying that change it reverts with no + decodable reason, because the selector no longer exists. Prefer + initiate_operator_transfer + complete_operator_transfer.""" sender = _sender(self, account) wallet = _wallet(self, validator) data = wallet.encode_abi( @@ -247,6 +257,89 @@ def set_operator( return _send(self, sender, tx) +def get_operator_transfer_context( + self: "GenLayerClient", validator: AddressLike +) -> OperatorRegistrationContext: + """Context for a rotation proof. + + Rotation is verified by the wallet rather than the factory, so the + registrar is the wallet's own address. The owner is read from the wallet + instead of assumed to be the caller: the proof is bound to whatever + owner() returns, and a mismatch is easier to diagnose here than as an + onlyOwner revert.""" + wallet = _wallet(self, validator) + return OperatorRegistrationContext( + registrar=self.w3.to_checksum_address(validator), + owner=self.w3.to_checksum_address(wallet.functions.owner().call()), + chain_id=self.w3.eth.chain_id, + ) + + +def initiate_operator_transfer( + self: "GenLayerClient", + validator: AddressLike, + registration: OperatorRegistrationProof, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Starts the two-step operator rotation. Owner only. + + `registration` must be built against get_operator_transfer_context — a + join proof is bound to the factory and will not verify here.""" + context = get_operator_transfer_context(self, validator) + if not verify_operator_registration(registration, context): + raise GenLayerError( + "Operator registration proof does not match the wallet, owner, chain, " + "or public key. Rotation proofs must use the validator wallet as " + "their registrar." + ) + + sender = _sender(self, account) + wallet = _wallet(self, validator) + data = wallet.encode_abi( + "initiateOperatorTransfer", + args=[list(registration.operator_pub_key), registration.possession_proof], + ) + tx = _build(self, sender, self.w3.to_checksum_address(validator), data) + return _send(self, sender, tx) + + +def complete_operator_transfer( + self: "GenLayerClient", + validator: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Finalises a pending rotation. Callable by the wallet owner or the + pending operator, once the factory's operatorTransferDelay has elapsed.""" + sender = _sender(self, account) + wallet = _wallet(self, validator) + data = wallet.encode_abi("completeOperatorTransfer", args=[]) + tx = _build(self, sender, self.w3.to_checksum_address(validator), data) + return _send(self, sender, tx) + + +def cancel_operator_transfer( + self: "GenLayerClient", + validator: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Abandons a pending rotation, leaving the current operator in place.""" + sender = _sender(self, account) + wallet = _wallet(self, validator) + data = wallet.encode_abi("cancelOperatorTransfer", args=[]) + tx = _build(self, sender, self.w3.to_checksum_address(validator), data) + return _send(self, sender, tx) + + +def get_pending_operator(self: "GenLayerClient", validator: AddressLike) -> dict: + """Pending operator and when its transfer was initiated (0 when none).""" + wallet = _wallet(self, validator) + operator, initiated_at = wallet.functions.getPendingOperator().call() + return { + "operator": self.w3.to_checksum_address(operator), + "initiated_at": int(initiated_at), + } + + def set_identity( self: "GenLayerClient", validator: AddressLike, diff --git a/genlayer_py/staking/operator_registration.py b/genlayer_py/staking/operator_registration.py new file mode 100644 index 0000000..b182ad5 --- /dev/null +++ b/genlayer_py/staking/operator_registration.py @@ -0,0 +1,135 @@ +"""Proof of possession for operator keys. + +Consensus requires an operator to prove control of its key before that key is +bound to a validator wallet. The proof is an EIP-191 signature, by the operator +key, over a domain-separated hash of (chainId, registrar, owner, pubKey). + +`registrar` is whichever contract verifies the proof, and it differs by flow: +the ValidatorWalletFactory for a validator join, the wallet itself for an +operator rotation (ValidatorWalletBlueprint.initiateOperatorTransfer calls +PubKeyUtils.validateWithPossession(pubKey, address(this), owner(), proof)). +Passing the wrong one produces a proof that simply fails to verify. + +The encoding mirrors genlayer-js's createOperatorRegistration exactly — the +shared vector in tests/unit/test_operator_registration.py pins the two +implementations together. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Tuple + +from eth_abi.abi import encode as abi_encode +from eth_account import Account +from eth_account.messages import encode_defunct +from eth_typing import ChecksumAddress +from eth_utils.address import to_checksum_address +from eth_utils.crypto import keccak + +OPERATOR_REGISTRATION_DOMAIN = keccak( + text="GenLayer/operatorPubKey/proof-of-possession/v1" +) + +OperatorPublicKey = Tuple[int, int] + + +@dataclass(frozen=True) +class OperatorRegistrationContext: + """Who verifies the proof, on whose behalf, and on which chain.""" + + registrar: ChecksumAddress + owner: ChecksumAddress + chain_id: int + + +@dataclass(frozen=True) +class OperatorRegistrationProof: + operator: ChecksumAddress + operator_pub_key: OperatorPublicKey + possession_proof: bytes + + +def operator_public_key_from_private_key(private_key: str) -> OperatorPublicKey: + """Splits the uncompressed secp256k1 public key into the contract's + uint256[2] tuple.""" + account = Account.from_key(private_key) + public_key = account._key_obj.public_key.to_bytes() + return ( + int.from_bytes(public_key[0:32], "big"), + int.from_bytes(public_key[32:64], "big"), + ) + + +def operator_address_from_public_key(pub_key: OperatorPublicKey) -> ChecksumAddress: + raw = pub_key[0].to_bytes(32, "big") + pub_key[1].to_bytes(32, "big") + return to_checksum_address(keccak(raw)[-20:]) + + +def operator_possession_message( + pub_key: OperatorPublicKey, context: OperatorRegistrationContext +) -> bytes: + return keccak( + abi_encode( + ["bytes32", "uint256", "address", "address", "uint256", "uint256"], + [ + OPERATOR_REGISTRATION_DOMAIN, + context.chain_id, + context.registrar, + context.owner, + pub_key[0], + pub_key[1], + ], + ) + ) + + +def create_operator_registration( + private_key: str, context: OperatorRegistrationContext +) -> OperatorRegistrationProof: + """Builds the proof package the proof-bearing calls consume. + + The private key is used only to sign and is never retained in the result. + """ + account = Account.from_key(private_key) + pub_key = operator_public_key_from_private_key(private_key) + operator = operator_address_from_public_key(pub_key) + + if operator != to_checksum_address(account.address): + raise ValueError( + "Operator private key and public key derive different identities." + ) + + signed = Account.sign_message( + encode_defunct(operator_possession_message(pub_key, context)), + private_key=private_key, + ) + + return OperatorRegistrationProof( + operator=operator, + operator_pub_key=pub_key, + possession_proof=bytes(signed.signature), + ) + + +def verify_operator_registration( + registration: OperatorRegistrationProof, + context: OperatorRegistrationContext, +) -> bool: + """Checks key identity and the exact registrar/owner/chain binding.""" + if operator_address_from_public_key(registration.operator_pub_key) != to_checksum_address( + registration.operator + ): + return False + + message = encode_defunct( + operator_possession_message(registration.operator_pub_key, context) + ) + try: + recovered = Account.recover_message( + message, signature=registration.possession_proof + ) + except Exception: + return False + + return to_checksum_address(recovered) == to_checksum_address(registration.operator) diff --git a/tests/unit/test_operator_registration.py b/tests/unit/test_operator_registration.py new file mode 100644 index 0000000..3e24c76 --- /dev/null +++ b/tests/unit/test_operator_registration.py @@ -0,0 +1,101 @@ +"""Cross-language vector for the operator proof of possession. + +The expected values below are the ones genlayer-js asserts in +tests/operator-registration.test.ts. Both SDKs sign proofs the same contract +verifies, so a divergence here is a real interoperability break rather than a +cosmetic difference — pin the exact bytes, not just internal consistency. +""" + +from genlayer_py.staking.operator_registration import ( + OPERATOR_REGISTRATION_DOMAIN, + OperatorRegistrationContext, + create_operator_registration, + operator_possession_message, + verify_operator_registration, +) + +OPERATOR_KEY = "0x0000000000000000000000000000000000000000000000000000000000000002" +OTHER_OPERATOR_KEY = "0x0000000000000000000000000000000000000000000000000000000000000003" +CONTEXT = OperatorRegistrationContext( + registrar="0x1111111111111111111111111111111111111111", + owner="0x2222222222222222222222222222222222222222", + chain_id=61999, +) + + +def test_matches_the_genlayer_js_vector(): + registration = create_operator_registration(OPERATOR_KEY, CONTEXT) + + assert ( + "0x" + OPERATOR_REGISTRATION_DOMAIN.hex() + == "0x56a1f863be2956668ca2fd6b4010d6fde7a54f2b5a02d6c624a2bad7e5fd5ada" + ) + assert registration.operator == "0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF" + assert registration.operator_pub_key == ( + 89565891926547004231252920425935692360644145829622209833684329913297188986597, + 12158399299693830322967808612713398636155367887041628176798871954788371653930, + ) + assert ( + "0x" + operator_possession_message(registration.operator_pub_key, CONTEXT).hex() + == "0x7823e1bdaf3a8cea679a7bafaf8ddc39c379ac690f35696328650c3a712f36e0" + ) + assert ( + "0x" + registration.possession_proof.hex() + == "0x30cedc70f8ab478fbc1a13a3f36e7f6a10eed631f59db4c451e38fe6d94dc640" + "586d7a3202471043dbad68a3850655d39114aaca647df5734b171f8db7e88f161c" + ) + assert verify_operator_registration(registration, CONTEXT) + + +def test_rejects_wrong_key_and_cross_domain_proofs(): + registration = create_operator_registration(OPERATOR_KEY, CONTEXT) + wrong_key = create_operator_registration(OTHER_OPERATOR_KEY, CONTEXT) + + assert not verify_operator_registration( + OperatorRegistrationProofLike(registration, wrong_key.possession_proof), CONTEXT + ) + for field, value in ( + ("registrar", "0x3333333333333333333333333333333333333333"), + ("owner", "0x4444444444444444444444444444444444444444"), + ("chain_id", CONTEXT.chain_id + 1), + ): + mutated = OperatorRegistrationContext( + registrar=value if field == "registrar" else CONTEXT.registrar, + owner=value if field == "owner" else CONTEXT.owner, + chain_id=value if field == "chain_id" else CONTEXT.chain_id, + ) + assert not verify_operator_registration(registration, mutated) + + +def test_binds_rotation_proofs_to_the_wallet_not_the_factory(): + """Join proofs are verified by the factory, rotation proofs by the wallet. + + Reusing one for the other is the easy mistake, and it fails silently — the + proof simply does not verify — so pin it. + """ + factory = "0x1111111111111111111111111111111111111111" + wallet = "0x5555555555555555555555555555555555555555" + owner = "0x2222222222222222222222222222222222222222" + + join = create_operator_registration( + OPERATOR_KEY, + OperatorRegistrationContext(registrar=factory, owner=owner, chain_id=61999), + ) + rotation_context = OperatorRegistrationContext( + registrar=wallet, owner=owner, chain_id=61999 + ) + rotation = create_operator_registration(OPERATOR_KEY, rotation_context) + + assert verify_operator_registration(rotation, rotation_context) + assert not verify_operator_registration(join, rotation_context) + assert rotation.possession_proof != join.possession_proof + assert rotation.operator == join.operator + + +class OperatorRegistrationProofLike: + """Swaps in a foreign signature while keeping the advertised identity.""" + + def __init__(self, registration, possession_proof): + self.operator = registration.operator + self.operator_pub_key = registration.operator_pub_key + self.possession_proof = possession_proof