From c387bd92a5cbbb7b423a6f5d921e22a26b7d512a Mon Sep 17 00:00:00 2001 From: Kristina Nikolaeva <112946046+kristinaNikolaevaa@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:12:31 +0100 Subject: [PATCH] Ndev 3380 compasability for iterative trx (#456) --- contracts/common/StorageSoliditySource.sol | 2 +- contracts/precompiled/CallSolanaCaller.sol | 175 ++++++- .../basic/evm/test_solana_interoperability.py | 490 +++++++++++++++++- .../basic/indexer/test_instruction_parsing.py | 6 +- integration/tests/conftest.py | 6 +- integration/tests/neon_evm/conftest.py | 5 + .../tests/neon_evm/test_interoperability.py | 96 +++- .../tests/neon_evm/utils/call_solana.py | 4 +- utils/evm_loader.py | 1 - 9 files changed, 726 insertions(+), 59 deletions(-) diff --git a/contracts/common/StorageSoliditySource.sol b/contracts/common/StorageSoliditySource.sol index 11d925d59f..78c7307f23 100644 --- a/contracts/common/StorageSoliditySource.sol +++ b/contracts/common/StorageSoliditySource.sol @@ -23,7 +23,7 @@ contract Storage { return msg.sender.balance; } - function storeSumOfNumbers(uint256 num1, uint256 num2) public view returns (uint256) { + function storeSumOfNumbers(uint256 num1, uint256 num2) public view returns (uint256) { if (number == 101) { num1 = 0; } diff --git a/contracts/precompiled/CallSolanaCaller.sol b/contracts/precompiled/CallSolanaCaller.sol index 7a47095512..862ef2b964 100644 --- a/contracts/precompiled/CallSolanaCaller.sol +++ b/contracts/precompiled/CallSolanaCaller.sol @@ -2,11 +2,18 @@ pragma solidity >=0.7.0 <0.9.0; pragma abicoder v2; import "../external/neon-evm/call_solana.sol"; - +import "../common/StorageSoliditySource.sol"; contract CallSolanaCaller { + CallSolana constant _callSolana = + CallSolana(0xFF00000000000000000000000000000000000006); + struct Data { + uint256 value1; + uint256 value2; + } + mapping(uint256 => Data) public dataMap; + uint256 public numberToStore; - CallSolana constant _callSolana = CallSolana(0xFF00000000000000000000000000000000000006); struct ExecuteArgs { uint64 lamports; bytes instruction; @@ -17,73 +24,199 @@ contract CallSolanaCaller { bytes instruction; } - event LogBytes(bytes32 value); event LogStr(string value); + event LogInt(uint value); + event LogAddress(address value); event LogData(bytes32 program, bytes value); - function getNeonAddress(address addr) public returns (bytes32){ + function getNeonAddress(address addr) public returns (bytes32) { bytes32 solanaAddr = _callSolana.getNeonAddress(addr); return solanaAddr; } function execute(uint64 lamports, bytes calldata instruction) public { - bytes32 returnData = bytes32(_callSolana.execute(lamports, instruction)); + numberToStore = 190; + bytes32 returnData = bytes32( + _callSolana.execute(lamports, instruction) + ); emit LogBytes(returnData); + } + + + function executeInIterativeMode( + uint256 actionsNumber, + uint64 lamports, + bytes calldata instruction + ) public { + doIterativeActions(actionsNumber); + execute(lamports, instruction); + } + function solanaCallBeforeActionWithMatrix( + uint[][] memory a, + uint64 lamports, + bytes calldata instruction + ) public { + // Matrix matrixContract = new Matrix(); + execute(lamports, instruction); + uint sum = sumMatrixElements(a); + emit LogInt(sum); } - function execute_with_get_return_data(uint64 lamports, bytes calldata instruction) public { + function solanaCallAfterActionWithMatrix( + uint[][] memory a, + uint64 lamports, + bytes calldata instruction + ) public { + uint sum = sumMatrixElements(a); + execute(lamports, instruction); + emit LogInt(sum); + } + + function solanaCallInsideActionWithMatrix( + uint[][] memory a, + uint64 lamports, + bytes calldata instruction + ) public { + numberToStore = 18; + uint sum = 0; + for (uint i = 0; i < a.length; i++) { + for (uint j = 0; j < a[i].length; j++) { + if (i == a.length / 2) { + execute(lamports, instruction); + } + sum += a[i][j]; + } + } + emit LogInt(sum); + } + + + function batchExecuteInIterativeMode( + uint256 actionsNumber, + ExecuteArgs[] memory _args + ) public { + doIterativeActions(actionsNumber); + batchExecute(_args); + } + + function sendTokensAndExecuteInIterativeMode( + uint256 actionsNumber, + uint64 lamports, + bytes calldata instruction + ) public payable { + executeInIterativeMode(actionsNumber, lamports, instruction); + } + + function doIterativeActions(uint actionsNumber) public { + // some actions to make the call iterative + for (uint256 i = 0; i < actionsNumber; i++) { + Data memory newData = Data({value1: 1, value2: 2}); + dataMap[i] = newData; + } + } + + function deployStorageAndCallSolana( + uint64 lamports, + bytes calldata instruction + ) public { + Storage storageContract = new Storage(); + storageContract.store(10); + require(storageContract.retrieve() == 10); + execute(lamports, instruction); + emit LogAddress(address(storageContract)); + } + + function executeWithGetReturnData( + uint64 lamports, + bytes calldata instruction + ) public { _callSolana.execute(lamports, instruction); - (bytes32 program, bytes memory returnData) = _callSolana.getReturnData(); + (bytes32 program, bytes memory returnData) = _callSolana + .getReturnData(); emit LogData(program, returnData); } function batchExecute(ExecuteArgs[] memory _args) public { - for(uint i = 0; i < _args.length; i++) { + for (uint i = 0; i < _args.length; i++) { _callSolana.execute(_args[i].lamports, _args[i].instruction); } - (bytes32 program, bytes memory returnData) = _callSolana.getReturnData(); + (bytes32 program, bytes memory returnData) = _callSolana + .getReturnData(); emit LogData(program, returnData); } - function getPayer() public returns (bytes32){ + function getPayer() public returns (bytes32) { bytes32 payer = _callSolana.getPayer(); return payer; } - function createResource(bytes32 salt, uint64 space, uint64 lamports, bytes32 owner) external returns (bytes32){ - bytes32 resource = _callSolana.createResource(salt, space, lamports, owner); + function createResource( + bytes32 salt, + uint64 space, + uint64 lamports, + bytes32 owner + ) external returns (bytes32) { + bytes32 resource = _callSolana.createResource( + salt, + space, + lamports, + owner + ); return resource; } - function getResourceAddress(bytes32 salt) external returns (bytes32){ + function getResourceAddress(bytes32 salt) external returns (bytes32) { bytes32 resource = _callSolana.getResourceAddress(salt); return resource; } - function getSolanaPDA(bytes32 program_id, bytes memory seeds) external returns (bytes32){ + function getSolanaPDA( + bytes32 program_id, + bytes memory seeds + ) external returns (bytes32) { bytes32 pda = _callSolana.getSolanaPDA(program_id, seeds); return pda; } - function getExtAuthority(bytes32 salt) external returns (bytes32){ + function getExtAuthority(bytes32 salt) external returns (bytes32) { bytes32 authority = _callSolana.getExtAuthority(salt); return authority; } - function executeWithSeed(uint64 lamports, bytes32 salt, bytes calldata instruction) public { - bytes32 returnData = bytes32(_callSolana.executeWithSeed(lamports, salt, instruction)); + function executeWithSeed( + uint64 lamports, + bytes32 salt, + bytes calldata instruction + ) public { + bytes32 returnData = bytes32( + _callSolana.executeWithSeed(lamports, salt, instruction) + ); emit LogBytes(returnData); } - function getReturnData() public returns (bytes32, bytes memory){ + function getReturnData() public returns (bytes32, bytes memory) { return _callSolana.getReturnData(); } function batchExecuteWithSeed(ExecuteWithSeedArgs[] memory _args) public { - for(uint i = 0; i < _args.length; i++) { - _callSolana.executeWithSeed(_args[i].lamports, _args[i].salt, _args[i].instruction); + for (uint i = 0; i < _args.length; i++) { + _callSolana.executeWithSeed( + _args[i].lamports, + _args[i].salt, + _args[i].instruction + ); + } + } + + function sumMatrixElements(uint[][] memory a) public pure returns (uint) { + uint sum = 0; + for (uint i = 0; i < a.length; i++) { + for (uint j = 0; j < a[i].length; j++) { + sum += a[i][j]; + } } + return sum; } -} \ No newline at end of file +} diff --git a/integration/tests/basic/evm/test_solana_interoperability.py b/integration/tests/basic/evm/test_solana_interoperability.py index 12d8e2a913..1eef24e4d6 100644 --- a/integration/tests/basic/evm/test_solana_interoperability.py +++ b/integration/tests/basic/evm/test_solana_interoperability.py @@ -1,4 +1,6 @@ import typing as tp +import web3.exceptions +import random import pytest import spl @@ -16,14 +18,15 @@ ) import allure +from utils.types import TransactionType from utils.accounts import EthAccounts from utils.consts import COUNTER_ID, TRANSFER_TOKENS_ID, wSOL -from utils.helpers import bytes32_to_solana_pubkey, serialize_instruction +from utils.helpers import bytes32_to_solana_pubkey, serialize_instruction, wait_condition from utils.instructions import make_wSOL from utils.web3client import NeonChainWeb3Client -@pytest.fixture(scope="session") +@pytest.fixture(scope="function") def get_counter_value() -> tp.Iterator[int]: def gen_increment_counter(): count = 0 @@ -41,6 +44,57 @@ class TestSolanaInteroperability: accounts: EthAccounts web3_client: NeonChainWeb3Client + @pytest.fixture(scope="class") + def call_solana_caller_sol_network(self, class_account_sol_chain, web3_client_sol): + contract, _ = web3_client_sol.deploy_and_get_contract( + contract="precompiled/CallSolanaCaller.sol", + version="0.8.10", + contract_name="CallSolanaCaller", + account=class_account_sol_chain, + ) + return contract + + def serialized_transfer(self, sol_client, from_wallet, to_wallet, amount, contract, is_set_authority=True): + mint = spl.token.client.Token.create_mint( + conn=sol_client, + payer=from_wallet, + mint_authority=from_wallet.pubkey(), + decimals=9, + program_id=TOKEN_PROGRAM_ID, + ) + mint.payer = from_wallet + from_token_account = mint.create_associated_token_account(from_wallet.pubkey()) + to_token_account = mint.create_associated_token_account(to_wallet.pubkey()) + mint.mint_to( + dest=from_token_account, + mint_authority=from_wallet, + amount=amount, + opts=TxOpts(skip_confirmation=False, skip_preflight=True), + ) + + authority_pubkey: bytes = contract.functions.getSolanaPDA(bytes(TRANSFER_TOKENS_ID), b"authority").call() + if is_set_authority: + mint.set_authority( + from_token_account, + from_wallet, + spl.token.instructions.AuthorityType.ACCOUNT_OWNER, + Pubkey(authority_pubkey), + opts=TxOpts(skip_confirmation=False, skip_preflight=True), + ) + + instruction = Instruction( + program_id=TRANSFER_TOKENS_ID, + accounts=[ + AccountMeta(from_token_account, is_signer=False, is_writable=True), + AccountMeta(mint.pubkey, is_signer=False, is_writable=True), + AccountMeta(to_token_account, is_signer=False, is_writable=True), + AccountMeta(Pubkey(authority_pubkey), is_signer=False, is_writable=True), + AccountMeta(TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), + ], + data=bytes([0x0]), + ) + return serialize_instruction(TRANSFER_TOKENS_ID, instruction), mint, [from_token_account, to_token_account] + def test_counter_execute_with_get_return_data( self, call_solana_caller, counter_resource_address: bytes, get_counter_value ): @@ -57,15 +111,48 @@ def test_counter_execute_with_get_return_data( serialized = serialize_instruction(COUNTER_ID, instruction) tx = self.web3_client.make_raw_tx(sender.address) - instruction_tx = call_solana_caller.functions.execute_with_get_return_data( - lamports, serialized - ).build_transaction(tx) + instruction_tx = call_solana_caller.functions.executeWithGetReturnData(lamports, serialized).build_transaction( + tx + ) + resp = self.web3_client.send_transaction(sender, instruction_tx) assert resp["status"] == 1 + event_logs = call_solana_caller.events.LogData().process_receipt(resp) assert int.from_bytes(event_logs[0].args.value, byteorder="little") == next(get_counter_value) assert bytes32_to_solana_pubkey(event_logs[0].args.program.hex()) == COUNTER_ID + def test_transfer_with_pda_signature_iterative_tx_eip_1559(self, call_solana_caller, sol_client, solana_account): + iterations = 20 + sender = self.accounts[0] + from_wallet = solana_account + to_wallet = Keypair() + amount = 100000 + + serialized, mint, accounts_list = self.serialized_transfer( + sol_client, from_wallet, to_wallet, amount, call_solana_caller + ) + tx = self.web3_client.make_raw_tx( + from_=sender.address, amount=None, data=None, tx_type=TransactionType.EIP_1559 + ) + + instruction_tx = call_solana_caller.functions.executeInIterativeMode( + iterations, 0, serialized + ).build_transaction(tx) + + resp = self.web3_client.send_transaction(sender, instruction_tx) + assert resp["status"] == 1 + assert resp.type == 2 + + assert int(mint.get_balance(accounts_list[1], commitment=Confirmed).value.amount) == amount + event_logs = call_solana_caller.events.LogBytes().process_receipt(resp) + assert int.from_bytes(event_logs[0].args.value, byteorder="little") == 0 + + wait_condition( + lambda: self.web3_client.is_trx_iterative(resp["transactionHash"].hex()) is True, + timeout_sec=120, + ) + def test_counter_with_seed(self, call_solana_caller, counter_resource_address: bytes, get_counter_value): sender = self.accounts[0] lamports = 0 @@ -84,6 +171,7 @@ def test_counter_with_seed(self, call_solana_caller, counter_resource_address: b instruction_tx = call_solana_caller.functions.executeWithSeed(lamports, seed, serialized).build_transaction(tx) resp = self.web3_client.send_transaction(sender, instruction_tx) assert resp["status"] == 1 + event_logs = call_solana_caller.events.LogBytes().process_receipt(resp) assert int.from_bytes(event_logs[0].args.value, byteorder="little") == next(get_counter_value) @@ -104,6 +192,7 @@ def test_counter_execute(self, call_solana_caller, counter_resource_address: byt instruction_tx = call_solana_caller.functions.execute(lamports, serialized).build_transaction(tx) resp = self.web3_client.send_transaction(sender, instruction_tx) assert resp["status"] == 1 + event_logs = call_solana_caller.events.LogBytes().process_receipt(resp) assert int.from_bytes(event_logs[0].args.value, byteorder="little") == next(get_counter_value) @@ -129,21 +218,16 @@ def test_counter_batch_execute(self, call_solana_caller, counter_resource_addres resp = self.web3_client.send_transaction(sender, instruction_tx) assert resp["status"] == 1 + event_logs = call_solana_caller.events.LogData().process_receipt(resp) assert int.from_bytes(event_logs[0].args.value, byteorder="little") == current_counter assert bytes32_to_solana_pubkey(event_logs[0].args.program.hex()) == COUNTER_ID - def test_transfer_with_pda_signature( - self, call_solana_caller, sol_client, solana_account, pytestconfig, bank_account - ): + def test_transfer_with_pda_signature(self, call_solana_caller, sol_client, solana_account): sender = self.accounts[0] - from_wallet = Keypair() + from_wallet = solana_account to_wallet = Keypair() amount = 100000 - if pytestconfig.environment.use_bank: - sol_client.send_sol(bank_account, from_wallet.pubkey(), int(0.5 * 10**9)) - else: - sol_client.request_airdrop(from_wallet.pubkey(), 1000 * 10**9, commitment=Confirmed) mint = spl.token.client.Token.create_mint( conn=sol_client, @@ -194,15 +278,12 @@ def test_transfer_with_pda_signature( event_logs = call_solana_caller.events.LogBytes().process_receipt(resp) assert int.from_bytes(event_logs[0].args.value, byteorder="little") == 0 - def test_transfer_tokens_with_ext_authority(self, call_solana_caller, sol_client, pytestconfig, bank_account): + def test_transfer_tokens_with_ext_authority(self, call_solana_caller, sol_client, solana_account): sender = self.accounts[0] - from_wallet = Keypair() + from_wallet = solana_account to_wallet = Keypair() amount = 100000 - if pytestconfig.environment.use_bank: - sol_client.send_sol(bank_account, from_wallet.pubkey(), int(0.5 * 10**9)) - else: - sol_client.request_airdrop(from_wallet.pubkey(), 1000 * 10**9, commitment=Confirmed) + mint = spl.token.client.Token.create_mint( conn=sol_client, payer=from_wallet, @@ -305,3 +386,374 @@ def test_limit_of_simple_instr_in_one_trx(self, call_solana_caller, counter_reso instruction_tx = call_solana_caller.functions.batchExecute(call_params).build_transaction(tx) resp = self.web3_client.send_transaction(sender, instruction_tx) assert resp["status"] == 0 + + def test_solana_call_after_iterative_actions_sol_network( + self, + web3_client_sol, + counter_resource_address: bytes, + call_solana_caller_sol_network, + get_counter_value, + class_account_sol_chain, + ): + iterations = 29 + sender = class_account_sol_chain + lamports = 0 + + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized = serialize_instruction(COUNTER_ID, instruction) + + tx = web3_client_sol.make_raw_tx(sender.address) + instruction_tx = call_solana_caller_sol_network.functions.executeInIterativeMode( + iterations, lamports, serialized + ).build_transaction(tx) + resp = web3_client_sol.send_transaction(sender, instruction_tx) + assert resp["status"] == 1 + + event_logs = call_solana_caller_sol_network.events.LogBytes().process_receipt(resp) + assert int.from_bytes(event_logs[0].args.value, byteorder="little") == next(get_counter_value) + + def test_solana_call_after_iterative_actions( + self, counter_resource_address: bytes, call_solana_caller, get_counter_value + ): + sender = self.accounts[0] + lamports = 0 + matrix_lenght = 6 + matrix = [[random.randint(1, 100) for _ in range(matrix_lenght)] for _ in range(matrix_lenght)] + + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized = serialize_instruction(COUNTER_ID, instruction) + + tx = self.web3_client.make_raw_tx(sender.address) + instruction_tx = call_solana_caller.functions.solanaCallAfterActionWithMatrix( + matrix, lamports, serialized + ).build_transaction(tx) + resp = self.web3_client.send_transaction(sender, instruction_tx) + assert resp["status"] == 1 + + event_logs_bytes = call_solana_caller.events.LogBytes().process_receipt(resp) + assert int.from_bytes(event_logs_bytes[0].args.value, byteorder="little") == next(get_counter_value) + event_logs_int = call_solana_caller.events.LogInt().process_receipt(resp) + assert event_logs_int[0].args.value == sum(sum(row) for row in matrix) + + wait_condition( + lambda: self.web3_client.is_trx_iterative(resp["transactionHash"].hex()) is True, + timeout_sec=60, + ) + + def test_solana_call_after_iterative_actions_evm_memory_limit( + self, counter_resource_address: bytes, call_solana_caller + ): + sender = self.accounts[0] + lamports = 0 + matrix_lenght = 50 + matrix = [[random.randint(1, 100) for _ in range(matrix_lenght)] for _ in range(matrix_lenght)] + + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized = serialize_instruction(COUNTER_ID, instruction) + + tx = self.web3_client.make_raw_tx(sender.address) + + with pytest.raises( + web3.exceptions.ContractLogicError, + match="out of limits", + ): + call_solana_caller.functions.solanaCallAfterActionWithMatrix( + matrix, lamports, serialized + ).build_transaction(tx) + + def test_failed_solana_call_after_iterative_actions(self, call_solana_caller, sol_client, solana_account): + iterations = 29 + sender = self.accounts[0] + from_wallet = solana_account + to_wallet = Keypair() + amount = 100000 + + serialized, _, _ = self.serialized_transfer( + sol_client, from_wallet, to_wallet, amount, call_solana_caller, False + ) + + tx = self.web3_client.make_raw_tx(from_=sender.address, estimate_gas=True) + + instruction_tx = call_solana_caller.functions.executeInIterativeMode( + iterations, 0, serialized + ).build_transaction(tx) + + resp = self.web3_client.send_transaction(sender, instruction_tx) + assert resp["status"] == 0 + + event_logs = call_solana_caller.events.LogStr().process_receipt(resp) + assert len(event_logs) == 0 + + def test_solana_call_after_iterative_actions_exceed_accounts_limit( + self, counter_resource_address: bytes, call_solana_caller + ): + iterations = 53 + sender = self.accounts[0] + lamports = 0 + + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized = serialize_instruction(COUNTER_ID, instruction) + + tx = self.web3_client.make_raw_tx(sender.address) + + with pytest.raises( + web3.exceptions.ContractLogicError, + match="too many accounts: 65 > 64", + ): + call_solana_caller.functions.executeInIterativeMode(iterations, lamports, serialized).build_transaction(tx) + + def test_solana_call_inside_iterative_actions( + self, counter_resource_address: bytes, call_solana_caller, get_counter_value + ): + sender = self.accounts[0] + lamports = 0 + matrix_lenght = 8 + matrix = [[random.randint(1, 100) for _ in range(matrix_lenght)] for _ in range(matrix_lenght)] + + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized = serialize_instruction(COUNTER_ID, instruction) + + tx = self.web3_client.make_raw_tx(sender.address) + instruction_tx = call_solana_caller.functions.solanaCallInsideActionWithMatrix( + matrix, lamports, serialized + ).build_transaction(tx) + resp = self.web3_client.send_transaction(sender, instruction_tx) + assert resp["status"] == 1 + + event_logs_bytes = call_solana_caller.events.LogBytes().process_receipt(resp) + assert int.from_bytes(event_logs_bytes[0].args.value, byteorder="little") == next(get_counter_value) + + event_logs_int = call_solana_caller.events.LogInt().process_receipt(resp) + assert event_logs_int[0].args.value == sum(sum(row) for row in matrix) + + wait_condition( + lambda: self.web3_client.is_trx_iterative(resp["transactionHash"].hex()) is True, + timeout_sec=60, + ) + + def test_solana_call_of_two_programs_in_one_iterative_tx( + self, counter_resource_address: bytes, call_solana_caller, get_counter_value, sol_client, solana_account + ): + iterations = 10 + sender = self.accounts[0] + from_wallet = solana_account + to_wallet = Keypair() + amount = 100000 + + serialized_transfer, mint, accounts_list = self.serialized_transfer( + sol_client, from_wallet, to_wallet, amount, call_solana_caller + ) + + tx = self.web3_client.make_raw_tx(sender.address) + + instruction_counter = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized_counter = serialize_instruction(COUNTER_ID, instruction_counter) + + instruction_tx = call_solana_caller.functions.batchExecuteInIterativeMode( + iterations, [(0, serialized_transfer), (0, serialized_counter)] + ).build_transaction(tx) + + resp = self.web3_client.send_transaction(sender, instruction_tx) + assert resp["status"] == 1 + assert int(mint.get_balance(accounts_list[1], commitment=Confirmed).value.amount) == amount + + event_logs_data = call_solana_caller.events.LogData().process_receipt(resp) + assert int.from_bytes(event_logs_data[0].args.value, byteorder="little") == next(get_counter_value) + assert bytes32_to_solana_pubkey(event_logs_data[0].args.program.hex()) == COUNTER_ID + + wait_condition( + lambda: self.web3_client.is_trx_iterative(resp["transactionHash"].hex()) is True, + timeout_sec=60, + ) + + def test_solana_call_before_iterative_actions( + self, counter_resource_address: bytes, call_solana_caller, get_counter_value + ): + sender = self.accounts[0] + lamports = 0 + matrix_lenght = 6 + matrix = [[random.randint(1, 100) for _ in range(matrix_lenght)] for _ in range(matrix_lenght)] + + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized = serialize_instruction(COUNTER_ID, instruction) + + tx = self.web3_client.make_raw_tx(sender.address) + instruction_tx = call_solana_caller.functions.solanaCallBeforeActionWithMatrix( + matrix, lamports, serialized + ).build_transaction(tx) + resp = self.web3_client.send_transaction(sender, instruction_tx) + assert resp["status"] == 1 + + event_logs_bytes = call_solana_caller.events.LogBytes().process_receipt(resp) + assert int.from_bytes(event_logs_bytes[0].args.value, byteorder="little") == next(get_counter_value) + event_logs_int = call_solana_caller.events.LogInt().process_receipt(resp) + assert event_logs_int[0].args.value == sum(sum(row) for row in matrix) + + wait_condition( + lambda: self.web3_client.is_trx_iterative(resp["transactionHash"].hex()) is True, + timeout_sec=60, + ) + + def test_solana_call_before_iterative_actions_negative(self, counter_resource_address: bytes, call_solana_caller): + sender = self.accounts[0] + lamports = 0 + matrix_lenght = 12 + matrix = [[random.randint(1, 100) for _ in range(matrix_lenght)] for _ in range(matrix_lenght)] + + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized = serialize_instruction(COUNTER_ID, instruction) + + tx = self.web3_client.make_raw_tx(sender.address) + + instruction_tx = call_solana_caller.functions.solanaCallBeforeActionWithMatrix( + matrix, lamports, serialized + ).build_transaction(tx) + + resp = self.web3_client.send_transaction(sender, instruction_tx) + assert resp["status"] == 0 + + def test_iterative_actions_and_multiple_solana_calls( + self, counter_resource_address: bytes, call_solana_caller, get_counter_value + ): + iterations = 20 + solana_calls = 5 + lamports = 0 + + current_counter = 0 + call_params = [] + + sender = self.accounts[0] + + for _ in range(solana_calls): + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized = serialize_instruction(COUNTER_ID, instruction) + call_params.append((lamports, serialized)) + current_counter = next(get_counter_value) + + tx = self.web3_client.make_raw_tx(sender.address) + instruction_tx = call_solana_caller.functions.batchExecuteInIterativeMode( + iterations, call_params + ).build_transaction(tx) + resp = self.web3_client.send_transaction(sender, instruction_tx) + assert resp["status"] == 1 + + event_logs_data = call_solana_caller.events.LogData().process_receipt(resp) + assert int.from_bytes(event_logs_data[0].args.value, byteorder="little") == current_counter + assert bytes32_to_solana_pubkey(event_logs_data[0].args.program.hex()) == COUNTER_ID + + wait_condition( + lambda: self.web3_client.is_trx_iterative(resp["transactionHash"].hex()) is True, + timeout_sec=60, + ) + + def test_deploy_contract_and_call_solana( + self, counter_resource_address: bytes, call_solana_caller, get_counter_value + ): + sender = self.accounts[0] + lamports = 0 + + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized = serialize_instruction(COUNTER_ID, instruction) + + tx = self.web3_client.make_raw_tx(sender.address) + instruction_tx = call_solana_caller.functions.deployStorageAndCallSolana( + lamports, serialized + ).build_transaction(tx) + resp = self.web3_client.send_transaction(sender, instruction_tx) + assert resp["status"] == 1 + + event_logs_bytes = call_solana_caller.events.LogBytes().process_receipt(resp) + assert int.from_bytes(event_logs_bytes[0].args.value, byteorder="little") == next(get_counter_value) + event_logs_address = call_solana_caller.events.LogAddress().process_receipt(resp) + assert event_logs_address[0].args.value is not None + + def test_iterative_tx_with_send_tokens( + self, counter_resource_address: bytes, call_solana_caller, get_counter_value + ): + iterations = 29 + sender = self.accounts[0] + lamports = 0 + balance_before = self.web3_client.get_balance(call_solana_caller.address) + + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized = serialize_instruction(COUNTER_ID, instruction) + + tx = self.web3_client.make_raw_tx(from_=sender.address, amount=10) + + instruction_tx = call_solana_caller.functions.sendTokensAndExecuteInIterativeMode( + iterations, lamports, serialized + ).build_transaction(tx) + resp = self.web3_client.send_transaction(sender, instruction_tx) + assert resp["status"] == 1 + + event_logs = call_solana_caller.events.LogBytes().process_receipt(resp) + assert int.from_bytes(event_logs[0].args.value, byteorder="little") == next(get_counter_value) + + balance_after = self.web3_client.get_balance(call_solana_caller.address) + assert balance_after == balance_before + 10 diff --git a/integration/tests/basic/indexer/test_instruction_parsing.py b/integration/tests/basic/indexer/test_instruction_parsing.py index 9655b0346e..63b6e3755b 100644 --- a/integration/tests/basic/indexer/test_instruction_parsing.py +++ b/integration/tests/basic/indexer/test_instruction_parsing.py @@ -78,9 +78,9 @@ def test_tx_exec_from_data_solana_call(self, call_solana_caller, counter_resourc serialized = serialize_instruction(COUNTER_ID, instruction) tx = self.web3_client.make_raw_tx(sender.address) - instruction_tx = call_solana_caller.functions.execute_with_get_return_data( - lamports, serialized - ).build_transaction(tx) + instruction_tx = call_solana_caller.functions.executeWithGetReturnData(lamports, serialized).build_transaction( + tx + ) resp = self.web3_client.send_transaction(sender, instruction_tx) response = json_rpc_client.get_neon_trx_receipt(resp["transactionHash"]) diff --git a/integration/tests/conftest.py b/integration/tests/conftest.py index 1268d2a2d5..10f874105a 100644 --- a/integration/tests/conftest.py +++ b/integration/tests/conftest.py @@ -553,16 +553,16 @@ def multiple_actions_erc721(web3_client, accounts): return accounts[0], contract -@pytest.fixture(scope="class") +@pytest.fixture(scope="function") def call_solana_caller(accounts, web3_client): contract, _ = web3_client.deploy_and_get_contract("precompiled/CallSolanaCaller.sol", "0.8.10", accounts[0]) return contract -@pytest.fixture(scope="class") +@pytest.fixture(scope="function") def counter_resource_address(call_solana_caller, accounts, web3_client) -> bytes: tx = web3_client.make_raw_tx(accounts[0].address) - salt = web3_client.text_to_bytes32("1") + salt = web3_client.text_to_bytes32("".join(random.choices(string.ascii_letters, k=5))) instruction_tx = call_solana_caller.functions.createResource(salt, 8, 100000, bytes(COUNTER_ID)).build_transaction( tx ) diff --git a/integration/tests/neon_evm/conftest.py b/integration/tests/neon_evm/conftest.py index d437eadacc..a55ce35fc1 100644 --- a/integration/tests/neon_evm/conftest.py +++ b/integration/tests/neon_evm/conftest.py @@ -126,6 +126,11 @@ def new_holder_acc(operator_keypair: Keypair, evm_loader: EvmLoader) -> Pubkey: return evm_loader.create_holder(operator_keypair) +@pytest.fixture(scope="function") +def new_holder_acc_2(operator_keypair: Keypair, evm_loader: EvmLoader) -> Pubkey: + return evm_loader.create_holder(operator_keypair) + + @pytest.fixture(scope="function") def rw_lock_contract( evm_loader: EvmLoader, diff --git a/integration/tests/neon_evm/test_interoperability.py b/integration/tests/neon_evm/test_interoperability.py index 61bb8d5d14..1e64ef3f3b 100644 --- a/integration/tests/neon_evm/test_interoperability.py +++ b/integration/tests/neon_evm/test_interoperability.py @@ -18,9 +18,13 @@ from conftest import EnvironmentConfig from integration.tests.neon_evm.utils.call_solana import SolanaCaller +from .utils.transaction_checks import check_holder_account_tag, check_transaction_logs_have_text, decode_logs from integration.tests.neon_evm.utils.ethereum import make_eth_transaction, make_contract_call_trx +from utils.layouts import FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT +from .utils.constants import TAG_FINALIZED_STATE, TAG_ACTIVE_STATE +from utils.evm_loader import EVM_STEPS from utils.consts import ( MEMO_PROGRAM_ID, COMPUTE_BUDGET_ID, @@ -30,7 +34,6 @@ TRANSFER_TOKENS_ID, ) -from integration.tests.neon_evm.utils.transaction_checks import check_transaction_logs_have_text, decode_logs from integration.tests.neon_evm.utils.neon_api_client import NeonApiClient from utils.evm_loader import EvmLoader from utils.helpers import serialize_instruction @@ -63,7 +66,7 @@ def _create_mint_and_accounts(evm_loader, from_wallet, to_wallet, amount) -> tup class TestInteroperability: - @pytest.fixture(scope="function") + @pytest.fixture(scope="class") def solana_caller( self, evm_loader: EvmLoader, @@ -75,9 +78,7 @@ def solana_caller( environment: EnvironmentConfig, solana_client: SolanaClient, ) -> SolanaCaller: - return SolanaCaller( - operator_keypair, session_user, evm_loader, treasury_pool, holder_acc, neon_api_client, solana_client - ) + return SolanaCaller(operator_keypair, session_user, evm_loader, treasury_pool, holder_acc, neon_api_client) def test_get_solana_address_by_neon_address(self, sender_with_tokens, solana_caller): sol_addr = solana_caller.get_solana_address_by_neon_address(sender_with_tokens.eth_address.hex()) @@ -160,7 +161,7 @@ def test_execute_from_account_create_acc( def test_execute_several_instr_in_one_trx(self, sender_with_tokens, solana_caller, evm_loader, solana_client): instruction_count = 10 - resource_addr = solana_caller.create_resource(sender_with_tokens, b"123", 8, 1000000000, COUNTER_ID) + resource_addr = solana_caller.create_resource(sender_with_tokens, b"1234", 8, 1000000000, COUNTER_ID) instruction = Instruction( program_id=COUNTER_ID, @@ -182,7 +183,7 @@ def test_execute_several_instr_in_one_trx(self, sender_with_tokens, solana_calle def test_limit_of_simple_instr_in_one_trx(self, sender_with_tokens, solana_caller): instruction_count = 24 - resource_addr = solana_caller.create_resource(sender_with_tokens, b"123", 8, 1000000000, COUNTER_ID) + resource_addr = solana_caller.create_resource(sender_with_tokens, b"dss", 8, 1000000000, COUNTER_ID) instruction = Instruction( program_id=COUNTER_ID, @@ -348,7 +349,7 @@ def test_static_call_does_not_support_external_call( version="0.8.3", ) - resource_addr = solana_caller.create_resource(sender_with_tokens, b"123", 8, 1000000000, COUNTER_ID) + resource_addr = solana_caller.create_resource(sender_with_tokens, b"12ss3", 8, 1000000000, COUNTER_ID) instruction = Instruction( program_id=COUNTER_ID, @@ -436,3 +437,82 @@ def test_call_neon_instruction_by_neon_instruction( assert "Program not allowed to call itself" in decode_logs(err.args[0].data.logs) else: assert False, f"Expected error but got {resp}" + + def test_iterative_transaction_revision_2_txs_from_1_sender( + self, + sender_with_tokens, + solana_caller, + evm_loader, + new_holder_acc_2, + new_holder_acc, + neon_api_client, + operator_keypair, + treasury_pool, + ): + operator_balance_pubkey = evm_loader.get_operator_balance_pubkey(operator_keypair) + + resource_addr = solana_caller.create_resource(sender_with_tokens, b"qqww", 8, 1000000000, COUNTER_ID) + matrix_size = 6 + matrix = [[random.randint(1, 100) for _ in range(matrix_size)] for _ in range(matrix_size)] + + instruction = Instruction( + program_id=COUNTER_ID, + accounts=[ + AccountMeta(resource_addr, is_signer=False, is_writable=True), + ], + data=bytes([0x1]), + ) + serialized_instruction = serialize_instruction(COUNTER_ID, instruction) + + signed_tx = make_contract_call_trx( + evm_loader, + sender_with_tokens, + solana_caller.contract, + "solanaCallInsideActionWithMatrix(uint256[][],uint64,bytes)", + [matrix, 0, serialized_instruction], + ) + + emulate_result = neon_api_client.emulate_contract_call( + sender_with_tokens.eth_address.hex(), + solana_caller.contract.eth_address.hex(), + "solanaCallInsideActionWithMatrix(uint256[][],uint64,bytes)", + [matrix, 0, serialized_instruction], + ) + accounts_from_emulation = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] + + evm_loader.write_transaction_to_holder_account(signed_tx, new_holder_acc_2, operator_keypair) + + for _ in range(11): + evm_loader.send_transaction_step_from_account( + operator_keypair, + operator_balance_pubkey, + treasury_pool, + new_holder_acc_2, + accounts_from_emulation, + EVM_STEPS, + operator_keypair, + ) + + resp = solana_caller.execute( + program_id=COUNTER_ID, instruction=instruction, sender=sender_with_tokens, holder_acc=new_holder_acc + ) + check_transaction_logs_have_text(evm_loader, trx=resp, text="exit_status=0x11") + + check_holder_account_tag( + solana_client=evm_loader, + storage_account=new_holder_acc_2, + layout=FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, + expected_tag=TAG_ACTIVE_STATE, + ) + + evm_loader.execute_transaction_steps_from_account( + operator_keypair, treasury_pool, new_holder_acc_2, accounts_from_emulation, check_invalid_revision=True + ) + + check_holder_account_tag( + solana_client=evm_loader, + storage_account=new_holder_acc_2, + layout=FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, + expected_tag=TAG_FINALIZED_STATE, + ) + check_transaction_logs_have_text(solana_client=evm_loader, trx=resp, text="exit_status=0x11") diff --git a/integration/tests/neon_evm/utils/call_solana.py b/integration/tests/neon_evm/utils/call_solana.py index faa97866ec..ab934e9479 100644 --- a/integration/tests/neon_evm/utils/call_solana.py +++ b/integration/tests/neon_evm/utils/call_solana.py @@ -18,7 +18,6 @@ def __init__( treasury_pool, holder_acc, neon_api_client, - solana_client, ) -> None: self.operator_keypair = operator_keypair self.owner = owner @@ -26,7 +25,6 @@ def __init__( self.treasury_pool = treasury_pool self.holder_acc = holder_acc self.neon_api_client = neon_api_client - self.solana_client = solana_client self.contract = evm_loader.deploy_contract( operator=operator_keypair, user=owner, @@ -204,7 +202,7 @@ def create_resource(self, sender, salt, space, lamports, owner): SYSTEM_PROGRAM_ID, ], ) - check_transaction_logs_have_text(solana_client=self.solana_client, trx=resp, text="exit_status=0x12") + check_transaction_logs_have_text(solana_client=self.evm_loader, trx=resp, text="exit_status=0x12") return resource_address_pubkey @staticmethod diff --git a/utils/evm_loader.py b/utils/evm_loader.py index a37b50afb3..7deee65d25 100644 --- a/utils/evm_loader.py +++ b/utils/evm_loader.py @@ -513,7 +513,6 @@ def execute_transaction_steps_from_account( if check_invalid_revision and not is_invalid_revision: raise AssertionError("INVALID_REVISION not in logs") - return receipt def execute_transaction_steps_from_account_no_chain_id(