diff --git a/spell/cspell-list.txt b/spell/cspell-list.txt index e5797ec973..6a53a742c7 100644 --- a/spell/cspell-list.txt +++ b/spell/cspell-list.txt @@ -11,6 +11,7 @@ backdoors Bahdanau basechain bitcode +bitnumber bitstring bitstrings blockstore @@ -72,6 +73,7 @@ hazyone Héctor hehe heisenbugs +highload hippity Hoppity idict @@ -168,6 +170,7 @@ Sánchez sansx Satoshi sctx +sdest seamus Seamus Sedov @@ -202,6 +205,7 @@ thetonstudio TIMELOCK timeouted Timeouted +Tock Toncoin Toncoins tonstudio diff --git a/spell/cspell-tvm-instructions.txt b/spell/cspell-tvm-instructions.txt index 060a503f38..57c5ffe8a5 100644 --- a/spell/cspell-tvm-instructions.txt +++ b/spell/cspell-tvm-instructions.txt @@ -675,6 +675,7 @@ SGN SHA256U SINGLE SKIPDICT +SKIPOPTREF SPLIT SPLITQ SREFS diff --git a/src/benchmarks/func.build.ts b/src/benchmarks/func.build.ts index 3e8672296e..fabee99c8d 100644 --- a/src/benchmarks/func.build.ts +++ b/src/benchmarks/func.build.ts @@ -7,6 +7,7 @@ const main = async () => { await allInFolderFunc(__dirname, `${__dirname}/../func/stdlib`, [ "./**/func/**/*.fc", + "./**/func/**/*.func", ]); }; diff --git a/src/benchmarks/highload-wallet-v3/bench.spec.ts b/src/benchmarks/highload-wallet-v3/bench.spec.ts new file mode 100644 index 0000000000..40fb931a9a --- /dev/null +++ b/src/benchmarks/highload-wallet-v3/bench.spec.ts @@ -0,0 +1,241 @@ +import "@ton/test-utils"; + +import benchmarkResults from "@/benchmarks/highload-wallet-v3/gas.json"; +import benchmarkCodeSizeResults from "@/benchmarks/highload-wallet-v3/size.json"; + +import { + generateResults, + getStateSizeForAccount, + generateCodeSizeResults, + getUsedGas, + printBenchmarkTable, + type BenchmarkResult, + type CodeSizeResult, +} from "@/benchmarks/utils/gas"; + +import { resolve } from "path"; +import { + beginCell, + Dictionary, + SendMode, + toNano, + internal as internal_relaxed, +} from "@ton/core"; +import { HighloadWalletV3 } from "@/benchmarks/highload-wallet-v3/tact/output/highload-wallet-v3_HighloadWalletV3"; +import type { SandboxContract, TreasuryContract } from "@ton/sandbox"; +import { Blockchain } from "@ton/sandbox"; +import type { KeyPair } from "@ton/crypto"; +import { getSecureRandomBytes, keyPairFromSeed } from "@ton/crypto"; +import { type Step, writeLog } from "@/test/utils/write-vm-log"; +import { HighloadQueryId } from "@/benchmarks/highload-wallet-v3/tests/highload-query-id"; +import { bufferToBigInt } from "@/benchmarks/wallet-v5/utils"; +import { + createExternalRequestCell, + createInternalTransfer, + DEFAULT_TIMEOUT, + fromInitHighloadWalletV3_FunC, + SUBWALLET_ID, + type FromInitHighloadWalletV3, +} from "@/benchmarks/highload-wallet-v3/tests/utils"; + +function benchHighloadWalletV3( + benchmarkResult: BenchmarkResult, + codeSizeResults: CodeSizeResult, + fromInit: FromInitHighloadWalletV3, +) { + let blockchain: Blockchain; + let deployer: SandboxContract; + let receiver: SandboxContract; + let wallet: SandboxContract; + let keypair: KeyPair; + + let step: Step; + + const justTestFlow = async (kind: "external" | "internal") => { + const testReceiver = receiver.address; + const forwardToSelfValue = toNano(0.17239); + const forwardToReceiverValue = toNano(1); + const queryId = HighloadQueryId.fromSeqno(0n); + + const internalMessage = createInternalTransfer(wallet, { + actions: [ + { + type: "sendMsg", + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: internal_relaxed({ + to: testReceiver, + value: forwardToReceiverValue, + body: null, + }), + }, + ], + queryId, + value: forwardToSelfValue, + }); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.PAY_GAS_SEPARATELY, + queryId, + createdAt: ~~(Date.now() / 1000), + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const result = await step("externalTransfer & internalTransfer", () => + wallet.sendExternal(externalRequestCell.asSlice()), + ); + + expect(result.transactions).toHaveTransaction({ + to: wallet.address, + success: true, + exitCode: 0, + outMessagesCount: 1, + }); + + expect(result.transactions).toHaveTransaction({ + from: wallet.address, + to: wallet.address, + value: forwardToSelfValue, + success: true, + exitCode: 0, + outMessagesCount: 1, + }); + + expect(result.transactions).toHaveTransaction({ + from: wallet.address, + to: testReceiver, + value: forwardToReceiverValue, + }); + + const externalTransferGasUsed = getUsedGas(result, "external"); + + const internalTransferGasUsed = getUsedGas(result, "internal"); + + if (kind == "external") { + expect(externalTransferGasUsed).toEqual( + benchmarkResult.gas["externalTransfer"], + ); + } else { + expect(internalTransferGasUsed).toEqual( + benchmarkResult.gas["internalTransfer"], + ); + } + }; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + + deployer = await blockchain.treasury("deployer"); + receiver = await blockchain.treasury("receiver"); + + keypair = keyPairFromSeed(await getSecureRandomBytes(32)); + + step = writeLog({ + path: resolve(__dirname, "output", "log.yaml"), + blockchain, + }); + + wallet = blockchain.openContract( + await fromInit( + bufferToBigInt(keypair.publicKey), + BigInt(SUBWALLET_ID), + Dictionary.empty(), + Dictionary.empty(), + 0n, + BigInt(DEFAULT_TIMEOUT), + ), + ); + + // Deploy wallet + const deployResult = await wallet.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + beginCell().endCell().asSlice(), + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: wallet.address, + deploy: true, + success: true, + }); + + // Top up wallet balance + await deployer.send({ + to: wallet.address, + value: toNano("10"), + sendMode: SendMode.PAY_GAS_SEPARATELY, + }); + }); + + it("check correctness of deploy", async () => { + const lastCleanTime = await wallet.getGetLastCleanTime(); + + expect(lastCleanTime).toBe(0n); + + const walletPublicKey = await wallet.getGetPublicKey(); + + expect(walletPublicKey).toBe(bufferToBigInt(keypair.publicKey)); + }); + + it("externalTransfer", async () => { + await justTestFlow("external"); + }); + + it("internalTransfer", async () => { + await justTestFlow("internal"); + }); + + it("cells", async () => { + expect( + (await getStateSizeForAccount(blockchain, wallet.address)).cells, + ).toEqual(codeSizeResults.size["cells"]); + }); + + it("bits", async () => { + expect( + (await getStateSizeForAccount(blockchain, wallet.address)).bits, + ).toEqual(codeSizeResults.size["bits"]); + }); +} + +describe("Highload Wallet v3 Gas Benchmarks", () => { + const fullResults = generateResults(benchmarkResults); + const fullCodeSizeResults = generateCodeSizeResults( + benchmarkCodeSizeResults, + ); + + describe("func", () => { + const funcCodeSize = fullCodeSizeResults.at(0)!; + const funcResult = fullResults.at(0)!; + + benchHighloadWalletV3( + funcResult, + funcCodeSize, + fromInitHighloadWalletV3_FunC, + ); + }); + + describe("tact", () => { + const tactCodeSize = fullCodeSizeResults.at(-1)!; + const tactResult = fullResults.at(-1)!; + benchHighloadWalletV3( + tactResult, + tactCodeSize, + HighloadWalletV3.fromInit.bind(HighloadWalletV3), + ); + }); + + afterAll(() => { + printBenchmarkTable(fullResults, fullCodeSizeResults, { + implementationName: "FunC", + printMode: "full", + }); + }); +}); diff --git a/src/benchmarks/highload-wallet-v3/func/highload-wallet-v3.func b/src/benchmarks/highload-wallet-v3/func/highload-wallet-v3.func new file mode 100644 index 0000000000..404e2f6d44 --- /dev/null +++ b/src/benchmarks/highload-wallet-v3/func/highload-wallet-v3.func @@ -0,0 +1,245 @@ +#include "stdlib-extended.func"; + +;;; Store binary true b{1} into `builder` [b] +builder store_true(builder b) asm "STONE"; +;;; Stores [x] binary zeroes into `builder` [b]. +builder store_zeroes(builder b, int x) asm "STZEROES"; +;;; Store `cell` [actions] to register c5 (out actions) +() set_actions(cell actions) impure asm "c5 POP"; + +const int op::internal_transfer = 0xae42e5a4; + +const int error::invalid_signature = 33; +const int error::invalid_subwallet_id = 34; +const int error::invalid_created_at = 35; +const int error::already_executed = 36; +const int error::invalid_message_to_send = 37; +const int error::invalid_timeout = 38; + +const int KEY_SIZE = 13; +const int SIGNATURE_SIZE = 512; +const int PUBLIC_KEY_SIZE = 256; +const int SUBWALLET_ID_SIZE = 32; +const int TIMESTAMP_SIZE = 64; +const int TIMEOUT_SIZE = 22; ;; 2^22 / 60 / 60 / 24 - up to ~48 days + +const int CELL_BITS_SIZE = 1023; +const int BIT_NUMBER_SIZE = 10; ;; 2^10 = 1024 + +() recv_internal(cell in_msg_full, slice in_msg_body) impure { + (int body_bits, int body_refs) = in_msg_body.slice_bits_refs(); + ifnot ((body_refs == 1) & (body_bits == MSG_OP_SIZE + MSG_QUERY_ID_SIZE)) { + return (); ;; just accept TONs + } + + slice in_msg_full_slice = in_msg_full.begin_parse(); + int msg_flags = in_msg_full_slice~load_msg_flags(); + if (msg_flags & 1) { ;; is bounced + return (); + } + + slice sender_address = in_msg_full_slice~load_msg_addr(); + + ;; not from myself + if (~ sender_address.equal_slices_bits(my_address())) { + return (); ;; just accept TONs + } + + int op = in_msg_body~load_op(); + + if (op == op::internal_transfer) { + in_msg_body~skip_query_id(); + cell actions = in_msg_body.preload_ref(); + cell old_code = my_code(); + set_actions(actions); + set_code(old_code); ;; prevent to change smart contract code + return (); + } +} + +() recv_external(slice msg_body) impure { + cell msg_inner = msg_body~load_ref(); + slice signature = msg_body~load_bits(SIGNATURE_SIZE); + msg_body.end_parse(); + int msg_inner_hash = msg_inner.cell_hash(); + + slice data_slice = get_data().begin_parse(); + int public_key = data_slice~load_uint(PUBLIC_KEY_SIZE); + int subwallet_id = data_slice~load_uint(SUBWALLET_ID_SIZE); + cell old_queries = data_slice~load_dict(); + cell queries = data_slice~load_dict(); + int last_clean_time = data_slice~load_uint(TIMESTAMP_SIZE); + int timeout = data_slice~load_uint(TIMEOUT_SIZE); + data_slice.end_parse(); + + if (last_clean_time < (now() - timeout)) { + (old_queries, queries) = (queries, null()); + if (last_clean_time < (now() - (timeout * 2))) { + old_queries = null(); + } + last_clean_time = now(); + } + + throw_unless(error::invalid_signature, check_signature(msg_inner_hash, signature, public_key)); + + slice msg_inner_slice = msg_inner.begin_parse(); + int _subwallet_id = msg_inner_slice~load_uint(SUBWALLET_ID_SIZE); + cell message_to_send = msg_inner_slice~load_ref(); + int send_mode = msg_inner_slice~load_uint(8); + int shift = msg_inner_slice~load_uint(KEY_SIZE); + int bit_number = msg_inner_slice~load_uint(BIT_NUMBER_SIZE); + int created_at = msg_inner_slice~load_uint(TIMESTAMP_SIZE); + int _timeout = msg_inner_slice~load_uint(TIMEOUT_SIZE); + msg_inner_slice.end_parse(); + + throw_unless(error::invalid_subwallet_id, _subwallet_id == subwallet_id); + throw_unless(error::invalid_timeout, _timeout == timeout); + + throw_unless(error::invalid_created_at, created_at > now() - timeout); + throw_unless(error::invalid_created_at, created_at <= now()); + + (cell value, int found) = old_queries.udict_get_ref?(KEY_SIZE, shift); + if (found) { + slice value_slice = value.begin_parse(); + value_slice~skip_bits(bit_number); + throw_if(error::already_executed, value_slice.preload_int(1)); + } + + (cell value, int found) = queries.udict_get_ref?(KEY_SIZE, shift); + builder new_value = null(); + if (found) { + slice value_slice = value.begin_parse(); + (slice tail, slice head) = value_slice.load_bits(bit_number); + throw_if(error::already_executed, tail~load_int(1)); + new_value = begin_cell().store_slice(head).store_true().store_slice(tail); + } else { + new_value = begin_cell().store_zeroes(bit_number).store_true().store_zeroes(CELL_BITS_SIZE - bit_number - 1); + } + + accept_message(); + + queries~udict_set_ref(KEY_SIZE, shift, new_value.end_cell()); + + set_data(begin_cell() + .store_uint(public_key, PUBLIC_KEY_SIZE) + .store_uint(subwallet_id, SUBWALLET_ID_SIZE) + .store_dict(old_queries) + .store_dict(queries) + .store_uint(last_clean_time, TIMESTAMP_SIZE) + .store_uint(timeout, TIMEOUT_SIZE) + .end_cell()); + + + commit(); + + ;; after commit, check the message to prevent an error in the action phase + + slice message_slice = message_to_send.begin_parse(); + {- + https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L123C1-L124C33 + currencies$_ grams:Grams other:ExtraCurrencyCollection = CurrencyCollection; + extra_currencies$_ dict:(HashmapE 32 (VarUInteger 32)) = ExtraCurrencyCollection; + + https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L135 + int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool + src:MsgAddress dest:MsgAddressInt + value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams + created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; + + https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L155 + message$_ {X:Type} info:CommonMsgInfoRelaxed + init:(Maybe (Either StateInit ^StateInit)) + body:(Either X ^X) = MessageRelaxed X; + -} + + throw_if(error::invalid_message_to_send, message_slice~load_uint(1)); ;; int_msg_info$0 + int msg_flags = message_slice~load_uint(3); ;; ihr_disabled:Bool bounce:Bool bounced:Bool + if (msg_flags & 1) { ;; is bounced + return (); + } + slice message_source_address = message_slice~load_msg_addr(); ;; src + throw_unless(error::invalid_message_to_send, is_address_none(message_source_address)); + message_slice~load_msg_addr(); ;; dest + message_slice~load_coins(); ;; value.coins + message_slice = message_slice.skip_dict(); ;; value.other extra-currencies + message_slice~load_coins(); ;; ihr_fee + message_slice~load_coins(); ;; fwd_fee + message_slice~skip_bits(64 + 32); ;; created_lt:uint64 created_at:uint32 + int maybe_state_init = message_slice~load_uint(1); + throw_if(error::invalid_message_to_send, maybe_state_init); ;; throw if state-init included (state-init not supported) + int either_body = message_slice~load_int(1); + if (either_body) { + message_slice~load_ref(); + message_slice.end_parse(); + } + + ;; send message with IGNORE_ERRORS flag to ignore errors in the action phase + + send_raw_message(message_to_send, send_mode | SEND_MODE_IGNORE_ERRORS); +} + + +int get_public_key() method_id { + return get_data().begin_parse().preload_uint(PUBLIC_KEY_SIZE); +} + +int get_subwallet_id() method_id { + slice data_slice = get_data().begin_parse(); + data_slice~skip_bits(PUBLIC_KEY_SIZE); ;; skip public_key + return data_slice.preload_uint(SUBWALLET_ID_SIZE); +} + +int get_last_clean_time() method_id { + slice data_slice = get_data().begin_parse(); + data_slice~skip_bits(PUBLIC_KEY_SIZE + SUBWALLET_ID_SIZE + 1 + 1); ;; skip: public_key, subwallet_id, old_queried, queries + return data_slice.preload_uint(TIMESTAMP_SIZE); +} + +int get_timeout() method_id { + slice data_slice = get_data().begin_parse(); + data_slice~skip_bits(PUBLIC_KEY_SIZE + SUBWALLET_ID_SIZE + 1 + 1 + TIMESTAMP_SIZE); ;; skip: public_key, subwallet_id, old_queried, queries, last_clean_time + return data_slice.preload_uint(TIMEOUT_SIZE); +} + +int processed?(int query_id, int need_clean) method_id { + int shift = query_id >> BIT_NUMBER_SIZE; + int bit_number = query_id & CELL_BITS_SIZE; + + slice data_slice = get_data().begin_parse(); + data_slice~skip_bits(PUBLIC_KEY_SIZE + SUBWALLET_ID_SIZE); ;; skip: public_key, subwallet_id + cell old_queries = data_slice~load_dict(); + cell queries = data_slice~load_dict(); + int last_clean_time = data_slice~load_uint(TIMESTAMP_SIZE); + int timeout = data_slice~load_uint(TIMEOUT_SIZE); + data_slice.end_parse(); + + if (need_clean) { + if (last_clean_time < (now() - timeout)) { + (old_queries, queries) = (queries, null()); + if (last_clean_time < (now() - (timeout * 2))) { + old_queries = null(); + } + last_clean_time = now(); + } + } + + (cell value, int found) = old_queries.udict_get_ref?(KEY_SIZE, shift); + if (found) { + slice value_slice = value.begin_parse(); + value_slice~skip_bits(bit_number); + if (value_slice.preload_int(1)) { + return TRUE; + } + } + + (cell value, int found) = queries.udict_get_ref?(KEY_SIZE, shift); + if (found) { + slice value_slice = value.begin_parse(); + value_slice~skip_bits(bit_number); + if (value_slice.preload_int(1)) { + return TRUE; + } + } + + return FALSE; +} \ No newline at end of file diff --git a/src/benchmarks/highload-wallet-v3/func/stdlib-extended.func b/src/benchmarks/highload-wallet-v3/func/stdlib-extended.func new file mode 100644 index 0000000000..282d592a7d --- /dev/null +++ b/src/benchmarks/highload-wallet-v3/func/stdlib-extended.func @@ -0,0 +1,241 @@ +;; CUSTOM: + +;; TVM UPGRADE 2023-07 https://docs.ton.org/learn/tvm-instructions/tvm-upgrade-2023-07 +;; In mainnet since 20 Dec 2023 https://t.me/tonblockchain/226 + +;;; Creates an output action and returns a fee for creating a message. Mode has the same effect as in the case of SENDRAWMSG +int send_message(cell msg, int mode) impure asm "SENDMSG"; + +int gas_consumed() asm "GASCONSUMED"; + +;; TVM V6 https://github.com/ton-blockchain/ton/blob/testnet/doc/GlobalVersions.md#version-6 + +int get_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEE"; +int get_storage_fee(int workchain, int seconds, int bits, int cells) asm(cells bits seconds workchain) "GETSTORAGEFEE"; +int get_forward_fee(int workchain, int bits, int cells) asm(cells bits workchain) "GETFORWARDFEE"; +int get_precompiled_gas_consumption() asm "GETPRECOMPILEDGAS"; + +int get_simple_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEESIMPLE"; +int get_simple_forward_fee(int workchain, int bits, int cells) asm(cells bits workchain) "GETFORWARDFEESIMPLE"; +int get_original_fwd_fee(int workchain, int fwd_fee) asm(fwd_fee workchain) "GETORIGINALFWDFEE"; +int my_storage_due() asm "DUEPAYMENT"; + +tuple get_fee_configs() asm "UNPACKEDCONFIGTUPLE"; + +;; BASIC + +const int TRUE = -1; +const int FALSE = 0; + +const int MASTERCHAIN = -1; +const int BASECHAIN = 0; + +;;; skip (Maybe ^Cell) from `slice` [s]. +(slice, ()) ~skip_maybe_ref(slice s) asm "SKIPOPTREF"; + +(slice, int) ~load_bool(slice s) inline { + return s.load_int(1); +} + +builder store_bool(builder b, int value) inline { + return b.store_int(value, 1); +} + +;; ADDRESS NONE +;; addr_none$00 = MsgAddressExt; https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L100 + +builder store_address_none(builder b) inline { + return b.store_uint(0, 2); +} + +slice address_none() asm " , + queries: map, + lastCleanTime: Int as uint64, + timeout: Int as uint22, +) { + receive(msg: Slice) {} // just accept TONs + + receive(msg: InternalTransfer) { + // not from myself + if (!(sender() == myAddress())) { + stopExecution(); // just accept TONs + } + + setActions(msg.actions); + setCode(myCode()); + } + + external(msgSlice: Slice) { + let msg = ExternalRequest.fromSlice(msgSlice); + + if (self.lastCleanTime < (now() - self.timeout)) { + self.oldQueries = self.queries; + self.queries = null; + if (self.lastCleanTime < (now() - (self.timeout * 2))) { + self.oldQueries = null; + } + self.lastCleanTime = now(); + } + + throwUnless(SignatureMismatch, checkSignature(msg.signedMsg.hash(), msg.signature, self.publicKey)); + + let msgInner = MsgInner.fromCell(msg.signedMsg); + + throwUnless(SubwalletIdMismatch, msgInner.subwalletID == self.subwalletID); + throwUnless(TimeoutMismatch, msgInner.timeout == self.timeout); + + throwUnless(InvalidCreatedAt, msgInner.createdAt.inRange(now() - self.timeout, now())); + + let value = self.oldQueries.get(msgInner.queryID.shift); + + if (value != null) { + let valueSlice = value!!.beginParse(); + valueSlice.skipBits(msgInner.queryID.bitNumber); + throwIf(AlreadyExecuted, valueSlice.preloadInt(1) != 0); + } + + value = self.queries.get(msgInner.queryID.shift); + let newValue = beginCell(); + + if (value != null) { + let valueSlice = value!!.beginParse(); + let head = valueSlice.loadBits(msgInner.queryID.bitNumber); + let tail = valueSlice; + throwIf(AlreadyExecuted, tail.loadInt(1) != 0); + newValue = newValue.storeSlice(head).storeTrue().storeSlice(tail); + } else { + newValue = newValue.storeZeroes(msgInner.queryID.bitNumber).storeTrue().storeZeroes(CELL_BITS_SIZE - msgInner.queryID.bitNumber - 1); + } + + acceptMessage(); + + self.queries.set(msgInner.queryID.shift, newValue.endCell()); + + setData(self.toCell()); + + commit(); + + // after commit, check the message to prevent an error in the action phase + + let messageSlice = msgInner.messageToSend.beginParse(); + /* + https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L123C1-L124C33 + currencies$_ grams:Grams other:ExtraCurrencyCollection = CurrencyCollection; + extra_currencies$_ dict:(HashmapE 32 (VarUInteger 32)) = ExtraCurrencyCollection; + + https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L135 + int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool + src:MsgAddress dest:MsgAddressInt + value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams + created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; + + https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L155 + message$_ {X:Type} info:CommonMsgInfoRelaxed + init:(Maybe (Either StateInit ^StateInit)) + body:(Either X ^X) = MessageRelaxed X; + */ + throwIf(InvalidMessageToSend, messageSlice.loadBool()); // int_msg_info$0 + + let msgFlags = messageSlice.loadUint(3); // ihr_disabled:Bool bounce:Bool bounced:Bool + if ((msgFlags & 1) == 1) { // is bounced + stopExecution(); + } + + let messageSourceAddress = messageSlice.loadAddress(); + throwIf(InvalidMessageToSend, messageSourceAddress.asSlice().preloadUint(2) != 0); + messageSlice.skipAddress(); // dest:MsgAddressInt + messageSlice.skipCoins(); // value.coins + messageSlice.skipMaybeRef(); // value.extra_currencies + messageSlice.skipCoins(); // ihr_fee:Grams + messageSlice.skipCoins(); // fwd_fee:Grams + messageSlice.skipBits(64 + 32); // created_lt:uint64 created_at:uint32 + let maybeStateInit = messageSlice.loadBool(); + throwIf(InvalidMessageToSend, maybeStateInit); // throw if state-init included (state-init not supported) + let eitherBody = messageSlice.loadBool(); + if (eitherBody) { + messageSlice.skipRef(); + messageSlice.endParse(); + } + + sendRawMessage(msgInner.messageToSend, msgInner.sendMode | SendIgnoreErrors); + stopExecution(); + } + + get fun get_public_key(): Int { + return self.publicKey; + } + + get fun get_subwallet_id(): Int { + return self.subwalletID; + } + + get fun get_last_clean_time(): Int { + return self.lastCleanTime; + } + + get fun get_timeout(): Int { + return self.timeout; + } + + get(0x1cbf2) fun is_processed(queryID: Int, needClean: Bool): Bool { + let shift = queryID >> BIT_NUMBER_SIZE; + let bitNumber = queryID & CELL_BITS_SIZE; + + if (needClean) { + if (self.lastCleanTime < (now() - self.timeout)) { + self.oldQueries = self.queries; + self.queries = null; + if (self.lastCleanTime < (now() - (self.timeout * 2))) { + self.oldQueries = null; + } + self.lastCleanTime = now(); + } + } + + let value = self.oldQueries.get(shift); + + if (value != null) { + let valueSlice = value!!.beginParse(); + valueSlice.skipBits(bitNumber); + if (valueSlice.preloadInt(1) != 0) { + return true; + } + } + + value = self.queries.get(shift); + + if (value != null) { + let valueSlice = value!!.beginParse(); + valueSlice.skipBits(bitNumber); + if (valueSlice.preloadInt(1) != 0) { + return true; + } + } + + return false; + } +} + +const CELL_BITS_SIZE: Int = 1023; +const BIT_NUMBER_SIZE: Int = 10; // 2^10 = 1024 + +asm fun setActions(actions: Cell) { c5 POP } +asm fun setCode(code: Cell) { SETCODE } + +asm(self to from) extends fun inRange(self: Int, from: Int, to: Int): Bool { s2 PUSH MAX MIN EQUAL } + +asm extends fun storeZeroes(self: Builder, count: Int): Builder { STZEROES } +asm extends fun storeTrue(self: Builder): Builder { STONE } + +asm fun stopExecution() { <{ }> PUSHCONT CALLCC } diff --git a/src/benchmarks/highload-wallet-v3/test.spec.ts b/src/benchmarks/highload-wallet-v3/test.spec.ts new file mode 100644 index 0000000000..0f56941f00 --- /dev/null +++ b/src/benchmarks/highload-wallet-v3/test.spec.ts @@ -0,0 +1,13 @@ +import { testHighloadWalletV3 } from "@/benchmarks/highload-wallet-v3/tests/highload-wallet"; +import { HighloadWalletV3 } from "@/benchmarks/highload-wallet-v3/tact/output/highload-wallet-v3_HighloadWalletV3"; +import { fromInitHighloadWalletV3_FunC } from "@/benchmarks/highload-wallet-v3/tests/utils"; + +describe("Highload Wallet V3 Tests", () => { + describe("func", () => { + testHighloadWalletV3(fromInitHighloadWalletV3_FunC); + }); + + describe("tact", () => { + testHighloadWalletV3(HighloadWalletV3.fromInit.bind(HighloadWalletV3)); + }); +}); diff --git a/src/benchmarks/highload-wallet-v3/tests/highload-query-id.ts b/src/benchmarks/highload-wallet-v3/tests/highload-query-id.ts new file mode 100644 index 0000000000..3fa984a65f --- /dev/null +++ b/src/benchmarks/highload-wallet-v3/tests/highload-query-id.ts @@ -0,0 +1,96 @@ +import { getRandomInt } from "@/benchmarks/highload-wallet-v3/tests/utils"; + +const BIT_NUMBER_SIZE = 10n; // 10 bit +// const SHIFT_SIZE = 13n; // 13 bit +export const MAX_BIT_NUMBER = 1022n; +export const MAX_SHIFT = 8191n; // 2^13 = 8192 +export const MAX_KEY_COUNT = 1 << 13; + +export class HighloadQueryId { + private shift: bigint; // [0 .. 8191] + private bitnumber: bigint; // [0 .. 1022] + + constructor() { + this.shift = 0n; + this.bitnumber = 0n; + } + + static fromShiftAndBitNumber( + shift: bigint, + bitnumber: bigint, + ): HighloadQueryId { + const q = new HighloadQueryId(); + q.shift = shift; + if (q.shift < 0) throw new Error("invalid shift"); + if (q.shift > MAX_SHIFT) throw new Error("invalid shift"); + q.bitnumber = bitnumber; + if (q.bitnumber < 0) throw new Error("invalid bitnumber"); + if (q.bitnumber > MAX_BIT_NUMBER) throw new Error("invalid bitnumber"); + return q; + } + + static getRandom(): HighloadQueryId { + const shift = getRandomInt(0, Number(MAX_SHIFT)); + const bitnumber = getRandomInt(0, Number(MAX_BIT_NUMBER)); + return HighloadQueryId.fromShiftAndBitNumber( + BigInt(shift), + BigInt(bitnumber), + ); + } + + getNext() { + let newBitnumber = this.bitnumber + 1n; + let newShift = this.shift; + + if (newShift === MAX_SHIFT && newBitnumber > MAX_BIT_NUMBER - 1n) { + throw new Error("Overload"); // NOTE: we left one queryId for emergency withdraw + } + + if (newBitnumber > MAX_BIT_NUMBER) { + newBitnumber = 0n; + newShift += 1n; + if (newShift > MAX_SHIFT) { + throw new Error("Overload"); + } + } + + return HighloadQueryId.fromShiftAndBitNumber(newShift, newBitnumber); + } + + hasNext() { + const isEnd = + this.bitnumber >= MAX_BIT_NUMBER - 1n && this.shift === MAX_SHIFT; // NOTE: we left one queryId for emergency withdraw; + return !isEnd; + } + + getShift(): bigint { + return this.shift; + } + + getBitNumber(): bigint { + return this.bitnumber; + } + + getQueryId(): bigint { + return (this.shift << BIT_NUMBER_SIZE) + this.bitnumber; + } + + static fromQueryId(queryId: bigint): HighloadQueryId { + const shift = queryId >> BIT_NUMBER_SIZE; + const bitnumber = queryId & 1023n; + return this.fromShiftAndBitNumber(shift, bitnumber); + } + + static fromSeqno(i: bigint): HighloadQueryId { + const shift = i / 1023n; + const bitnumber = i % 1023n; + return this.fromShiftAndBitNumber(shift, bitnumber); + } + + /** + * @return {bigint} [0 .. 8380415] + */ + toSeqno(): bigint { + return this.bitnumber + this.shift * 1023n; + } +} diff --git a/src/benchmarks/highload-wallet-v3/tests/highload-wallet.ts b/src/benchmarks/highload-wallet-v3/tests/highload-wallet.ts new file mode 100644 index 0000000000..b3ee2dc8a3 --- /dev/null +++ b/src/benchmarks/highload-wallet-v3/tests/highload-wallet.ts @@ -0,0 +1,1502 @@ +import "@ton/test-utils"; +import { + DEFAULT_TIMEOUT, + SUBWALLET_ID, + type FromInitHighloadWalletV3, + createInternalTransfer, + createExternalRequestCell, + getRandomInt, + createInternalTransferBatch, + createInternalTransferBody, +} from "@/benchmarks/highload-wallet-v3/tests/utils"; +import { + Blockchain, + createShardAccount, + EmulationError, + internal, +} from "@ton/sandbox"; +import { getSecureRandomBytes, keyPairFromSeed } from "@ton/crypto"; +import type { OutActionSendMsg } from "@ton/core"; +import { + beginCell, + BitString, + Dictionary, + SendMode, + toNano, + type Address, + internal as internal_relaxed, +} from "@ton/core"; +import { bufferToBigInt } from "@/benchmarks/wallet-v5/utils"; +import { + HighloadQueryId, + MAX_BIT_NUMBER, + MAX_KEY_COUNT, + MAX_SHIFT, +} from "@/benchmarks/highload-wallet-v3/tests/highload-query-id"; +import { step } from "@/test/allure/allure"; +import { + AlreadyExecuted, + InvalidCreatedAt, + loadHighloadWalletV3$Data, + SignatureMismatch, + storeHighloadWalletV3$Data, + SubwalletIdMismatch, + TimeoutMismatch, +} from "@/benchmarks/highload-wallet-v3/tact/output/highload-wallet-v3_HighloadWalletV3"; +import { findTransactionRequired, randomAddress } from "@ton/test-utils"; +import { MsgGenerator } from "@/benchmarks/highload-wallet-v3/tests/msg-generator"; + +const globalSetup = async (fromInit: FromInitHighloadWalletV3) => { + const blockchain = await Blockchain.create(); + blockchain.now = 1000; + + const keypair = keyPairFromSeed(await getSecureRandomBytes(32)); + + const shouldRejectWith = async (p: Promise, code: bigint) => { + try { + await p; + throw new Error(`Should throw ${code}`); + } catch (e: unknown) { + if (e instanceof EmulationError) { + expect(e.exitCode !== undefined).toBe(true); + expect(BigInt(e.exitCode!)).toEqual(code); + } else { + throw e; + } + } + }; + + const getContractData = async (address: Address) => { + const smc = await blockchain.getContract(address); + if (!smc.account.account) throw new Error("Account not found"); + if (smc.account.account.storage.state.type != "active") + throw new Error("Attempting to get data on inactive account"); + if (!smc.account.account.storage.state.state.data) + throw new Error("Data is not present"); + return smc.account.account.storage.state.state.data; + }; + const getContractCode = async (address: Address) => { + const smc = await blockchain.getContract(address); + if (!smc.account.account) throw new Error("Account not found"); + if (smc.account.account.storage.state.type != "active") + throw new Error("Attempting to get code on inactive account"); + if (!smc.account.account.storage.state.state.code) + throw new Error("Code is not present"); + return smc.account.account.storage.state.state.code; + }; + + const deployer = await blockchain.treasury("deployer"); + + const wallet = blockchain.openContract( + await fromInit( + bufferToBigInt(keypair.publicKey), + BigInt(SUBWALLET_ID), + Dictionary.empty(), + Dictionary.empty(), + 0n, + BigInt(DEFAULT_TIMEOUT), + ), + ); + + // Deploy wallet + const deployResult = await wallet.send( + deployer.getSender(), + { + value: toNano("999999"), + }, + beginCell().endCell().asSlice(), + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: wallet.address, + deploy: true, + success: true, + }); + + return { + blockchain, + keypair, + shouldRejectWith, + getContractCode, + getContractData, + wallet, + }; +}; + +const testDeploy = (fromInit: FromInitHighloadWalletV3) => { + const setup = async () => { + return await globalSetup(fromInit); + }; + + describe("Deploy", () => { + it("should deploy correctly", async () => { + const { keypair, wallet } = await setup(); + const lastCleanTime = await wallet.getGetLastCleanTime(); + + expect(lastCleanTime).toBe(0n); + + const walletPublicKey = await wallet.getGetPublicKey(); + + expect(walletPublicKey).toBe(bufferToBigInt(keypair.publicKey)); + }); + }); +}; + +const testAuth = (fromInit: FromInitHighloadWalletV3) => { + const setup = async () => { + return await globalSetup(fromInit); + }; + + describe("Auth", () => { + it("should pass check sign", async () => { + const { keypair, wallet } = await setup(); + + try { + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const queryId = HighloadQueryId.getRandom(); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell.asSlice(), + ); + + await step( + "Check that external transfer with correct sign is accepted and internal message is send", + () => { + expect(testResult.transactions).toHaveTransaction({ + from: wallet.address, + to: wallet.address, + success: true, + }); + }, + ); + } catch (e: unknown) { + if (e instanceof EmulationError) { + expect(e.exitCode).toBe(SignatureMismatch); + } else { + throw e; + } + } + }); + + it("should fail check sign", async () => { + const { wallet, shouldRejectWith } = await setup(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const badKey = await getSecureRandomBytes(64); + + const queryId = HighloadQueryId.getRandom(); + + const externalRequestCell = createExternalRequestCell(badKey, { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }); + + await step( + "Check that external transfer with incorrect sign is rejected", + async () => { + await shouldRejectWith( + wallet.sendExternal(externalRequestCell.asSlice()), + SignatureMismatch, + ); + }, + ); + }); + }); +}; + +const testParameterValidation = (fromInit: FromInitHighloadWalletV3) => { + const setup = async () => { + return await globalSetup(fromInit); + }; + + describe("Parameter validation", () => { + it("should fail subwallet check", async () => { + const { keypair, wallet, shouldRejectWith } = await setup(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const currentSubwalletId = await wallet.getGetSubwalletId(); + + await step("Check that current subwallet id is correct", () => { + expect(currentSubwalletId).toBe(BigInt(SUBWALLET_ID)); + }); + + const queryId = HighloadQueryId.getRandom(); + + let badSubwalletId; + + do { + badSubwalletId = getRandomInt(0, 1000); + } while (badSubwalletId == Number(currentSubwalletId)); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: badSubwalletId, + timeout: DEFAULT_TIMEOUT, + }, + ); + + await step( + "Check that external transfer with incorrect subwallet id is rejected", + async () => { + await shouldRejectWith( + wallet.sendExternal(externalRequestCell.asSlice()), + SubwalletIdMismatch, + ); + }, + ); + }); + + it("should fail check creation time", async () => { + const { keypair, wallet, shouldRejectWith } = await setup(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const currentTimeout = Number(await wallet.getGetTimeout()); + + const queryId = HighloadQueryId.getRandom(); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: + 1000 - + getRandomInt(currentTimeout + 1, currentTimeout + 200), + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + await step( + "Check that external transfer with incorrect creation time is rejected", + async () => { + await shouldRejectWith( + wallet.sendExternal(externalRequestCell.asSlice()), + InvalidCreatedAt, + ); + }, + ); + }); + + it("should fail check timeout", async () => { + const { keypair, wallet, shouldRejectWith } = await setup(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const currentTimeout = Number(await wallet.getGetTimeout()); + + const queryId = HighloadQueryId.getRandom(); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: currentTimeout + 1, + }, + ); + + await step( + "Check that external transfer with incorrect timeout is rejected", + async () => { + await shouldRejectWith( + wallet.sendExternal(externalRequestCell.asSlice()), + TimeoutMismatch, + ); + }, + ); + }); + }); +}; + +const testQueryId = (fromInit: FromInitHighloadWalletV3) => { + const setup = async () => { + return await globalSetup(fromInit); + }; + + describe("Query ID", () => { + it("should fail check query_id in actual queries", async () => { + const { keypair, wallet, shouldRejectWith } = await setup(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const queryId = HighloadQueryId.getRandom(); + + const externalRequestCell_step1 = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell_step1.asSlice(), + ); + + await step( + "Check that external transfer with correct query_id is accepted and internal message is send", + () => { + expect(testResult.transactions).toHaveTransaction({ + from: wallet.address, + to: wallet.address, + success: true, + }); + }, + ); + + const isProcessed = await wallet.getIsProcessed( + queryId.getQueryId(), + true, + ); + + await step("Check that query_id is processed", () => { + expect(isProcessed).toBe(true); + }); + + const externalRequestCell_step2 = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + await step( + "Check that external transfer with same query_id is rejected", + async () => { + await shouldRejectWith( + wallet.sendExternal( + externalRequestCell_step2.asSlice(), + ), + AlreadyExecuted, + ); + }, + ); + }); + + it("should work with max bitNumber = 1022", async () => { + const { keypair, wallet } = await setup(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const shift = getRandomInt(0, Number(MAX_SHIFT)); + const queryId = HighloadQueryId.fromShiftAndBitNumber( + BigInt(shift), + 1022n, + ); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + await step( + "Check that external transfer with correct query_id is accepted", + async () => { + await expect( + wallet.sendExternal(externalRequestCell.asSlice()), + ).resolves.not.toThrow(EmulationError); + }, + ); + }); + + it("should reject bitNumber = 1023", async () => { + const { keypair, wallet } = await setup(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const shift = getRandomInt(0, Number(MAX_SHIFT)); + const queryId = BigInt((shift << 10) + 1023); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + await step( + "Check that external transfer with incorrect bitNumber is rejected", + async () => { + await expect( + wallet.sendExternal(externalRequestCell.asSlice()), + ).rejects.toThrow(EmulationError); + }, + ); + }); + + it("should work with max shift = maxShift", async () => { + const { keypair, wallet } = await setup(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const bitNumber = getRandomInt(0, Number(MAX_BIT_NUMBER)); + const queryId = HighloadQueryId.fromShiftAndBitNumber( + MAX_SHIFT, + BigInt(bitNumber), + ); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + await step( + "Check that external transfer with correct query_id is accepted", + async () => { + await expect( + wallet.sendExternal(externalRequestCell.asSlice()), + ).resolves.not.toThrow(EmulationError); + }, + ); + }); + + it("should fail check query_id in old queries", async () => { + const { keypair, wallet, blockchain, shouldRejectWith } = + await setup(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const queryId = HighloadQueryId.getRandom(); + + const externalRequestCell_step1 = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell_step1.asSlice(), + ); + + await step( + "Check that external transfer with correct query_id is accepted", + () => { + expect(testResult.transactions).toHaveTransaction({ + from: wallet.address, + to: wallet.address, + success: true, + }); + }, + ); + + blockchain.now = 1000 + DEFAULT_TIMEOUT; + + const isProcessed = await wallet.getIsProcessed( + queryId.getQueryId(), + true, + ); + await step("Check that query_id is processed", () => { + expect(isProcessed).toBe(true); + }); + + const externalRequestCell_step2 = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1050, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + await step( + "Check that external transfer with same query_id is rejected", + async () => { + await shouldRejectWith( + wallet.sendExternal( + externalRequestCell_step2.asSlice(), + ), + AlreadyExecuted, + ); + }, + ); + }); + + it("should be cleared queries hashmaps", async () => { + const { keypair, wallet, blockchain } = await setup(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const queryId_step1 = HighloadQueryId.getRandom(); + + const externalRequestCell_step1 = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId: queryId_step1, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult_step1 = await wallet.sendExternal( + externalRequestCell_step1.asSlice(), + ); + + await step( + "Check that external transfer with correct query_id is accepted and internal message is send", + () => { + expect(testResult_step1.transactions).toHaveTransaction({ + from: wallet.address, + to: wallet.address, + success: true, + }); + }, + ); + + let isProcessed_step1 = await wallet.getIsProcessed( + queryId_step1.getQueryId(), + true, + ); + + await step("Check that query_id is processed", () => { + expect(isProcessed_step1).toBe(true); + }); + + blockchain.now = 1000 + 1 + DEFAULT_TIMEOUT * 2; + + isProcessed_step1 = await wallet.getIsProcessed( + queryId_step1.getQueryId(), + true, + ); + + await step("Check that query_id is not processed", () => { + expect(isProcessed_step1).toBe(false); + }); + + const queryId_step2 = HighloadQueryId.getRandom(); + + const externalRequestCell_step2 = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId: queryId_step2, + createdAt: 1000 + DEFAULT_TIMEOUT * 2, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult_step2 = await wallet.sendExternal( + externalRequestCell_step2.asSlice(), + ); + + await step( + "Check that external transfer with correct query_id is accepted and internal message is send", + () => { + expect(testResult_step2.transactions).toHaveTransaction({ + from: wallet.address, + to: wallet.address, + success: true, + }); + }, + ); + + isProcessed_step1 = await wallet.getIsProcessed( + queryId_step1.getQueryId(), + true, + ); + const isProcessed_step2 = await wallet.getIsProcessed( + queryId_step2.getQueryId(), + true, + ); + + await step("Check that query_id is not processed", () => { + expect(isProcessed_step1).toBe(false); + }); + + await step("Check that another query_id is processed", () => { + expect(isProcessed_step2).toBe(true); + }); + + const lastCleanTime = await wallet.getGetLastCleanTime(); + + await step("Check that last_clean_time is correct", () => { + expect(lastCleanTime).toBe( + BigInt(testResult_step2.transactions[0]!.now), + ); + }); + }); + }); +}; + +const testPerformanceLimits = (fromInit: FromInitHighloadWalletV3) => { + const setup = async () => { + return await globalSetup(fromInit); + }; + + describe("Performance limits", () => { + it("should work with max keys in queries dictionary", async () => { + const { + keypair, + wallet, + blockchain, + getContractData, + getContractCode, + } = await setup(); + + const newQueries = Dictionary.empty( + Dictionary.Keys.Uint(13), + Dictionary.Values.Cell(), + ); + const padding = new BitString(Buffer.alloc(128, 0), 0, 1023 - 13); + + for (let i = 0; i < MAX_KEY_COUNT; i++) { + newQueries.set( + i, + beginCell().storeUint(i, 13).storeBits(padding).endCell(), + ); + } + + const oldQueries = Dictionary.empty( + Dictionary.Keys.Uint(13), + Dictionary.Values.Cell(), + ); + for (let i = 0; i < MAX_KEY_COUNT; i++) { + oldQueries.set( + i, + beginCell().storeBits(padding).storeUint(i, 13).endCell(), + ); + } + + const walletContract = await blockchain.getContract(wallet.address); + + const walletData = await getContractData(wallet.address); + const newData = beginCell() + .store( + storeHighloadWalletV3$Data({ + ...loadHighloadWalletV3$Data(walletData.beginParse()), + queries: newQueries, + oldQueries, + }), + ) + .endCell(); + + await blockchain.setShardAccount( + wallet.address, + createShardAccount({ + address: wallet.address, + code: await getContractCode(wallet.address), + data: newData, + balance: walletContract.balance, + workchain: 0, + }), + ); + + const internalMessage = createInternalTransfer(wallet, { + actions: [], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const queryId = HighloadQueryId.getRandom(); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = wallet.sendExternal( + externalRequestCell.asSlice(), + ); + + await step( + "Check that external transfer with correct query_id is accepted", + async () => { + await expect(testResult).resolves.not.toThrow( + EmulationError, + ); + }, + ); + + await step("Check that internal message is sent", async () => { + expect((await testResult).transactions).toHaveTransaction({ + on: wallet.address, + aborted: false, + outMessagesCount: 1, + }); + }); + }); + }); +}; + +const testMessageSending = (fromInit: FromInitHighloadWalletV3) => { + const setup = async () => { + return await globalSetup(fromInit); + }; + + describe("Message sending", () => { + it("should send internal message", async () => { + const { keypair, wallet, shouldRejectWith } = await setup(); + + const testAddr = randomAddress(0); + const testBody = beginCell() + .storeUint(getRandomInt(0, 1000000), 32) + .endCell(); + + const internalMessage = internal_relaxed({ + to: testAddr, + bounce: false, + value: toNano("123"), + body: testBody, + }); + const queryId = HighloadQueryId.getRandom(); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.PAY_GAS_SEPARATELY, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell.asSlice(), + ); + + await step( + "Check that internal message is sent with correct parameters", + () => { + expect(testResult.transactions).toHaveTransaction({ + on: testAddr, + from: wallet.address, + value: toNano("123"), + body: testBody, + }); + }, + ); + + const isProcessed = await wallet.getIsProcessed( + queryId.getQueryId(), + true, + ); + + await step("Check that query_id is processed", () => { + expect(isProcessed).toBe(true); + }); + + await step("Check that second transfer rejected", async () => { + await shouldRejectWith( + wallet.sendExternal(externalRequestCell.asSlice()), + AlreadyExecuted, + ); + }); + }); + + it("should ignore set_code action", async () => { + const { keypair, wallet, getContractCode } = await setup(); + + const mockCode = beginCell() + .storeUint(getRandomInt(0, 1000000), 32) + .endCell(); + const testBody = beginCell() + .storeUint(getRandomInt(0, 1000000), 32) + .endCell(); + const testAddr = randomAddress(); + + const queryId = HighloadQueryId.getRandom(); + + const walletCode = await getContractCode(wallet.address); + + const internalMessage = createInternalTransfer(wallet, { + actions: [ + { + type: "setCode", + newCode: mockCode, + }, + { + type: "sendMsg", + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: internal_relaxed({ + to: testAddr, + value: toNano("0.1"), + body: testBody, + }), + }, + ], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell.asSlice(), + ); + + await step("Check that set_code action is ignored", async () => { + const newCode = await getContractCode(wallet.address); + expect(newCode.hash()).toEqual(walletCode.hash()); + }); + + await step("Check that internal message is sent", () => { + expect(testResult.transactions).toHaveTransaction({ + on: testAddr, + from: wallet.address, + value: toNano("0.1"), + body: testBody, + }); + }); + }); + + it("should send external-out message", async () => { + const { keypair, wallet } = await setup(); + + const testBody = beginCell() + .storeUint(getRandomInt(0, 1000000), 32) + .endCell(); + + const queryId = HighloadQueryId.getRandom(); + + const internalMessage = createInternalTransfer(wallet, { + actions: [ + { + type: "sendMsg", + mode: SendMode.NONE, + outMsg: { + info: { + type: "external-out", + createdAt: 0, + createdLt: 0n, + }, + body: testBody, + }, + }, + ], + queryId: HighloadQueryId.fromQueryId(0n), + value: 0n, + }); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell.asSlice(), + ); + + const sentTx = await step("Check and find internal message", () => { + return findTransactionRequired(testResult.transactions, { + from: wallet.address, + to: wallet.address, + success: true, + outMessagesCount: 1, + actionResultCode: 0, + }); + }); + + await step("Check that external-out message is sent", () => { + expect(sentTx.externals.length).toBe(1); + expect(sentTx.externals[0]!.body).toEqualCell(testBody); + }); + + const isProcessed = await wallet.getIsProcessed( + queryId.getQueryId(), + true, + ); + + await step("Check that query_id is processed", () => { + expect(isProcessed).toBe(true); + }); + }); + }); +}; + +const testBatchProcessing = (fromInit: FromInitHighloadWalletV3) => { + const setup = async () => { + return await globalSetup(fromInit); + }; + + describe("Batch processing", () => { + it("should handle 254 actions in one go", async () => { + const { keypair, wallet } = await setup(); + + const queryId = HighloadQueryId.getRandom(); + + const outMsgs: OutActionSendMsg[] = new Array(254); + + for (let i = 0; i < 254; i++) { + outMsgs[i] = { + type: "sendMsg", + mode: SendMode.NONE, + outMsg: internal_relaxed({ + to: randomAddress(), + value: toNano("0.015"), + body: beginCell().storeUint(i, 32).endCell(), + }), + }; + } + + const internalMessage = createInternalTransferBatch(wallet, { + actions: outMsgs, + queryId, + value: 0n, + }); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell.asSlice(), + ); + + await step("Check that internal messages are sent", () => { + expect(testResult.transactions).toHaveTransaction({ + on: wallet.address, + outMessagesCount: 254, + }); + }); + + await step("Check messages are processed", () => { + for (let i = 0; i < 254; i++) { + expect(testResult.transactions).toHaveTransaction({ + from: wallet.address, + body: outMsgs[i]!.outMsg.body, + }); + } + }); + + const isProcessed = await wallet.getIsProcessed( + queryId.getQueryId(), + true, + ); + + await step("Check that query_id is processed", () => { + expect(isProcessed).toBe(true); + }); + }); + + it("should be able to go beyond 255 messages with chained internal_transfer", async () => { + const { keypair, wallet } = await setup(); + + const queryId = HighloadQueryId.getRandom(); + + const msgCount = getRandomInt(256, 507); + const msgs: OutActionSendMsg[] = new Array(msgCount); + + for (let i = 0; i < msgCount; i++) { + msgs[i] = { + type: "sendMsg", + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: internal_relaxed({ + to: randomAddress(0), + value: toNano("0.015"), + body: beginCell().storeUint(i, 32).endCell(), + }), + }; + } + + const internalMessage = createInternalTransferBatch(wallet, { + actions: msgs, + queryId, + value: 0n, + }); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internalMessage, + mode: SendMode.CARRY_ALL_REMAINING_BALANCE, + queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell.asSlice(), + ); + + await step("Check that internal messages are sent", () => { + expect(testResult.transactions).toHaveTransaction({ + on: wallet.address, + outMessagesCount: 254, + }); + expect(testResult.transactions).toHaveTransaction({ + on: wallet.address, + outMessagesCount: msgCount - 253, + }); + }); + + await step("Check messages are processed", () => { + for (let i = 0; i < msgCount; i++) { + expect(testResult.transactions).toHaveTransaction({ + from: wallet.address, + body: msgs[i]!.outMsg.body, + }); + } + }); + + const isProcessed = await wallet.getIsProcessed( + queryId.getQueryId(), + true, + ); + + await step("Check that query_id is processed", () => { + expect(isProcessed).toBe(true); + }); + }); + }); +}; + +const testInternalChecks = (fromInit: FromInitHighloadWalletV3) => { + const setup = async () => { + return await globalSetup(fromInit); + }; + + describe("Internal checks", () => { + it("should ignore internal transfer from address different from self", async () => { + const { wallet, blockchain } = await setup(); + + const queryId = HighloadQueryId.getRandom(); + + const testAddr = randomAddress(); + + const transferBody = createInternalTransferBody({ + actions: [ + { + type: "sendMsg", + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: internal_relaxed({ + to: testAddr, + value: toNano("1000"), + }), + }, + ], + queryId, + }); + + let testResult = await blockchain.sendMessage( + internal({ + from: testAddr, + to: wallet.address, + value: toNano("1"), + body: transferBody, + }), + ); + + await step("Check that internal transfer is ignored", () => { + expect(testResult.transactions).not.toHaveTransaction({ + on: testAddr, + from: wallet.address, + value: toNano("1000"), + }); + }); + + testResult = await blockchain.sendMessage( + internal({ + from: wallet.address, + to: wallet.address, + value: toNano("1"), + body: transferBody, + }), + ); + + await step("Check that internal transfer is processed", () => { + expect(testResult.transactions).toHaveTransaction({ + on: testAddr, + from: wallet.address, + value: toNano("1000"), + }); + }); + }); + + it("should ignore bounced messages", async () => { + const { wallet, blockchain } = await setup(); + + const queryId = HighloadQueryId.getRandom(); + + const testAddr = randomAddress(); + + const transferBody = createInternalTransferBody({ + actions: [ + { + type: "sendMsg", + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: internal_relaxed({ + to: testAddr, + value: toNano("1000"), + }), + }, + ], + queryId, + }); + + let testResult = await blockchain.sendMessage( + internal({ + from: wallet.address, + to: wallet.address, + body: transferBody, + value: toNano("1"), + bounced: true, + }), + ); + + await step("Check that internal transfer is ignored", () => { + expect(testResult.transactions).not.toHaveTransaction({ + on: testAddr, + from: wallet.address, + value: toNano("1000"), + }); + }); + + testResult = await blockchain.sendMessage( + internal({ + from: wallet.address, + to: wallet.address, + body: transferBody, + value: toNano("1"), + bounced: false, + }), + ); + + await step("Check that internal transfer is processed", () => { + expect(testResult.transactions).toHaveTransaction({ + on: testAddr, + from: wallet.address, + value: toNano("1000"), + }); + }); + }); + + it("should ignore invalid message in payload", async () => { + const { keypair, wallet } = await setup(); + + const badGenerator = new MsgGenerator(0); + let queryIter = new HighloadQueryId(); + + for (const badMsg of badGenerator.generateBadMsg()) { + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: badMsg, + mode: SendMode.NONE, + queryId: queryIter, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell.asSlice(), + ); + + await step("Check that invalid message is ignored", () => { + expect(testResult.transactions).toHaveTransaction({ + on: wallet.address, + success: true, + outMessagesCount: 0, + }); + }); + + const isProcessed = await wallet.getIsProcessed( + queryIter.getQueryId(), + true, + ); + + await step("Check that query_id is processed", () => { + expect(isProcessed).toBe(true); + }); + + queryIter = queryIter.getNext(); + } + }); + }); +}; + +const testAttackPrevention = (fromInit: FromInitHighloadWalletV3) => { + const setup = async () => { + return await globalSetup(fromInit); + }; + + describe("Attack prevention", () => { + it("timeout replay attack", async () => { + const { keypair, wallet, blockchain } = await setup(); + + /* + * Timeout is not part of the external + * So in theory one could deploy contract with + * different timeout without thinking too much. + * This opens up avenue for replay attack. + * So, at every deploy one should always change key or subwallet id + */ + + const deployer = await blockchain.treasury("new_deployer"); + const attacker = await blockchain.treasury("attacker"); + + // Same contract different timeout + const newWallet = blockchain.openContract( + await fromInit( + bufferToBigInt(keypair.publicKey), + BigInt(SUBWALLET_ID), + Dictionary.empty(), + Dictionary.empty(), + 0n, + BigInt(1234), + ), + ); + + // Deploy wallet + const deployResult = await newWallet.send( + deployer.getSender(), + { + value: toNano("999999"), + }, + beginCell().endCell().asSlice(), + ); + + await step("Check that wallet is deployed", () => { + expect(deployResult.transactions).toHaveTransaction({ + on: newWallet.address, + deploy: true, + success: true, + }); + }); + + // So attacker requested legit withdraw on the exchange + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: internal_relaxed({ + to: attacker.address, + value: toNano("10"), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + queryId: new HighloadQueryId(), + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell.asSlice(), + ); + + const legitTx = findTransactionRequired(testResult.transactions, { + on: wallet.address, + outMessagesCount: 1, + }); + + await step("Check that external request is processed", () => { + expect(testResult.transactions).toHaveTransaction({ + on: attacker.address, + value: toNano("10"), + }); + }); + + // And now can replay it on contract with different timeout + const replyExt = legitTx.inMessage!; + if (replyExt.info.type !== "external-in") { + throw TypeError("No way"); + } + + // Replace dest + replyExt.info = { + ...replyExt.info, + dest: newWallet.address, + }; + + await step("Check that replay is rejected", async () => { + await expect(blockchain.sendMessage(replyExt)).rejects.toThrow( + EmulationError, + ); + }); + }); + + it("should work replay protection, but don't send message", async () => { + const { keypair, wallet } = await setup(); + + const externalRequestCell = createExternalRequestCell( + keypair.secretKey, + { + message: beginCell().storeUint(239, 17).endCell(), + mode: SendMode.PAY_GAS_SEPARATELY, + queryId: new HighloadQueryId(), + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT, + }, + ); + + const testResult = await wallet.sendExternal( + externalRequestCell.asSlice(), + ); + + await step("Check that external request is processed", () => { + expect(testResult.transactions).toHaveTransaction({ + to: wallet.address, + success: true, + outMessagesCount: 0, + actionResultCode: 0, + }); + }); + }); + }); +}; + +export const testHighloadWalletV3 = (fromInit: FromInitHighloadWalletV3) => { + testDeploy(fromInit); + testAuth(fromInit); + testParameterValidation(fromInit); + testQueryId(fromInit); + testPerformanceLimits(fromInit); + testMessageSending(fromInit); + testBatchProcessing(fromInit); + testInternalChecks(fromInit); + testAttackPrevention(fromInit); +}; diff --git a/src/benchmarks/highload-wallet-v3/tests/msg-generator.ts b/src/benchmarks/highload-wallet-v3/tests/msg-generator.ts new file mode 100644 index 0000000000..badea03490 --- /dev/null +++ b/src/benchmarks/highload-wallet-v3/tests/msg-generator.ts @@ -0,0 +1,153 @@ +import type { + CommonMessageInfoExternalIn, + CommonMessageInfoExternalOut, + Message, + MessageRelaxed, + StateInit, +} from "@ton/core"; +import { + Cell, + ExternalAddress, + beginCell, + storeMessage, + storeMessageRelaxed, +} from "@ton/core"; +import { randomAddress } from "@ton/test-utils"; +export class MsgGenerator { + constructor(readonly wc: number) {} + + generateExternalOutWithBadSource() { + const ssrcInvalid = beginCell() + .storeUint(2, 2) // addr_std$10 + .storeUint(0, 1) // anycast nothing + .storeInt(this.wc, 8) // workchain_id: -1 + .storeUint(1, 10) + .endCell(); + + return beginCell() + .storeUint(3, 2) // ext_out_msg_info$11 + .storeBit(0) // src:INVALID + .storeSlice(ssrcInvalid.beginParse()) + .endCell(); + } + generateExternalOutWithBadDst() { + const src = randomAddress(-1); + return beginCell() + .storeUint(3, 2) // ext_out_msg_info$11 + .storeAddress(src) // src:MsgAddressInt + .storeBit(0) // dest:INVALID + .endCell(); + } + generateExternalInWithBadSource() { + const ssrcInvalid = beginCell() + .storeUint(1, 2) // addrExtern$01 + .storeUint(128, 9) + .storeUint(0, 10) + .endCell(); + + return beginCell() + .storeUint(2, 2) //ext_in_msg_info$11 + .storeSlice(ssrcInvalid.beginParse()) // src:INVALID + .endCell(); + } + generateExternalInWithBadDst() { + const src = new ExternalAddress(BigInt(Date.now()), 256); + return beginCell() + .storeUint(2, 2) //ext_in_msg_info$10 + .storeAddress(src) // src:MsgAddressExt + .storeBit(0) // dest:INVALID + .endCell(); + } + generateInternalMessageWithBadGrams() { + const src = randomAddress(this.wc); + const dst = randomAddress(this.wc); + return beginCell() + .storeUint(0, 1) // int_msg_info$0 + .storeUint(0, 1) // ihr_disabled:Bool + .storeUint(0, 1) // bounce:Bool + .storeUint(0, 1) // bounced:Bool + .storeAddress(src) // src:MsgAddress + .storeAddress(dst) // dest:MsgAddress + .storeUint(8, 4) // len of nanograms + .storeUint(1, 1) // INVALID GRAMS amount + .endCell(); + } + generateInternalMessageWithBadInitStateData() { + const ssrc = randomAddress(this.wc); + const sdest = randomAddress(this.wc); + + const init_state_with_bad_data = beginCell() + .storeUint(0, 1) // maybe (##5) + .storeUint(1, 1) // Maybe TickTock + .storeUint(1, 1) // bool Tick + .storeUint(0, 1) // bool Tock + .storeUint(1, 1) // code: Maybe Cell^ + .storeUint(1, 1) // data: Maybe Cell^ + .storeUint(1, 1); // library: Maybe ^Cell + // bits for references but no data + + return beginCell() + .storeUint(0, 1) // int_msg_info$0 + .storeUint(0, 1) // ihr_disabled:Bool + .storeUint(0, 1) // bounce:Bool + .storeUint(0, 1) // bounced:Bool + .storeAddress(ssrc) // src:MsgAddress + .storeAddress(sdest) // dest:MsgAddress + .storeCoins(0) // + .storeMaybeRef(null) // extra currencies + .storeCoins(0) // ihr_fee + .storeCoins(0) // fwd_fee + .storeUint(1000, 64) // created_lt:uint64 + .storeUint(1000, 32) // created_at:uint32 + .storeUint(1, 1) // Maybe init_state + .storeUint(1, 1) // Either (X ^X) init state + .storeRef(init_state_with_bad_data.endCell()) + .storeUint(0, 1) // Either (X ^X) body + .endCell(); + } + + *generateBadMsg() { + // Meh + yield this.generateExternalInWithBadDst(); + yield this.generateExternalOutWithBadDst(); + yield this.generateExternalInWithBadSource(); + yield this.generateExternalOutWithBadSource(); + yield this.generateInternalMessageWithBadGrams(); + yield this.generateInternalMessageWithBadInitStateData(); + } + generateExternalInMsg( + info?: Partial, + body?: Cell, + init?: StateInit, + ) { + const msgInfo: CommonMessageInfoExternalIn = { + type: "external-in", + dest: info?.dest ?? randomAddress(this.wc), + src: info?.src, + importFee: info?.importFee ?? 0n, + }; + const newMsg: Message = { + info: msgInfo, + body: body ?? Cell.EMPTY, + init, + }; + return beginCell().store(storeMessage(newMsg)).endCell(); + } + generateExternalOutMsg( + info?: Partial, + body?: Cell, + ) { + const msgInfo: CommonMessageInfoExternalOut = { + type: "external-out", + createdAt: info?.createdAt ?? 0, + createdLt: info?.createdLt ?? 0n, + src: info?.src ?? randomAddress(this.wc), + dest: info?.dest, + }; + const newMsg: MessageRelaxed = { + info: msgInfo, + body: body ?? Cell.EMPTY, + }; + return beginCell().store(storeMessageRelaxed(newMsg)).endCell(); + } +} diff --git a/src/benchmarks/highload-wallet-v3/tests/utils.ts b/src/benchmarks/highload-wallet-v3/tests/utils.ts new file mode 100644 index 0000000000..d751fec7ab --- /dev/null +++ b/src/benchmarks/highload-wallet-v3/tests/utils.ts @@ -0,0 +1,222 @@ +import type { + ExternalRequest, + MsgInner, +} from "@/benchmarks/highload-wallet-v3/tact/output/highload-wallet-v3_HighloadWalletV3"; +import { + HighloadWalletV3, + storeExternalRequest, + storeHighloadWalletV3$Data, + storeInternalTransfer, + storeMsgInner, +} from "@/benchmarks/highload-wallet-v3/tact/output/highload-wallet-v3_HighloadWalletV3"; +import type { HighloadQueryId } from "@/benchmarks/highload-wallet-v3/tests/highload-query-id"; +import { posixNormalize } from "@/utils/filePath"; +import type { MessageRelaxed, OutAction } from "@ton/core"; +import { + beginCell, + Cell, + contractAddress, + Dictionary, + internal as internal_relaxed, + SendMode, + storeMessageRelaxed, + storeOutList, +} from "@ton/core"; +import { sign } from "@ton/crypto"; +import type { SandboxContract } from "@ton/sandbox"; +import { readFileSync } from "fs"; +import { resolve } from "path"; + +export const DEFAULT_TIMEOUT = 120; +export const SUBWALLET_ID = 0; + +const getRandom = (min: number, max: number) => { + return Math.random() * (max - min) + min; +}; + +export const getRandomInt = (min: number, max: number) => { + return Math.round(getRandom(min, max)); +}; + +const getCodeCellFromBoc = (bocPath: string) => { + const codeBoc = readFileSync(posixNormalize(resolve(__dirname, bocPath))); + + const codeCell = Cell.fromBoc(codeBoc)[0]!; + + return codeCell; +}; + +const getWalletCodeCell_FunC = () => { + return getCodeCellFromBoc("../func/output/highload-wallet-v3.boc"); +}; + +export type FromInitHighloadWalletV3 = ( + publicKey: bigint, + subwalletID: bigint, + oldQueries: Dictionary, + queries: Dictionary, + lastCleanTime: bigint, + timeout: bigint, +) => Promise; + +export async function fromInitHighloadWalletV3_FunC( + publicKey: bigint, + subwalletID: bigint, + _oldQueries: Dictionary, + _queries: Dictionary, + _lastCleanTime: bigint, + timeout: bigint, +): Promise { + const initData = beginCell() + .store( + storeHighloadWalletV3$Data({ + $$type: "HighloadWalletV3$Data", + publicKey, + subwalletID, + oldQueries: Dictionary.empty(), + queries: Dictionary.empty(), + lastCleanTime: 0n, + timeout, + }), + ) + .endCell(); + + const stateInit = { code: getWalletCodeCell_FunC(), data: initData }; + const address = contractAddress(0, stateInit); + + return Promise.resolve(new HighloadWalletV3(address, stateInit)); +} + +export const createInternalTransferBody = (opts: { + actions: OutAction[] | Cell; + queryId: HighloadQueryId; +}) => { + let actionsCell: Cell; + if (opts.actions instanceof Cell) { + actionsCell = opts.actions; + } else { + if (opts.actions.length > 254) { + throw TypeError( + "Max allowed action count is 254. Use packActions instead.", + ); + } + const actionsBuilder = beginCell(); + storeOutList(opts.actions)(actionsBuilder); + actionsCell = actionsBuilder.endCell(); + } + return beginCell() + .store( + storeInternalTransfer({ + $$type: "InternalTransfer", + queryID: opts.queryId.getQueryId(), + actions: actionsCell, + }), + ) + .endCell(); +}; + +export const createInternalTransfer = ( + wallet: SandboxContract, + opts: { + actions: OutAction[] | Cell; + queryId: HighloadQueryId; + value: bigint; + }, +) => { + return internal_relaxed({ + to: wallet.address, + value: opts.value, + body: createInternalTransferBody(opts), + }); +}; + +export const createInternalTransferBatch = ( + wallet: SandboxContract, + opts: { + actions: OutAction[]; + queryId: HighloadQueryId; + value: bigint; + }, +) => { + let batch: OutAction[]; + if (opts.actions.length > 254) { + batch = opts.actions.slice(0, 253); + batch.push({ + type: "sendMsg", + mode: + opts.value > 0n + ? SendMode.PAY_GAS_SEPARATELY + : SendMode.CARRY_ALL_REMAINING_BALANCE, + outMsg: createInternalTransferBatch(wallet, { + actions: opts.actions.slice(253), + queryId: opts.queryId, + value: opts.value, + }), + }); + } else { + batch = opts.actions; + } + return createInternalTransfer(wallet, { + actions: batch, + queryId: opts.queryId, + value: opts.value, + }); +}; + +export const createExternalRequestCell = ( + secretKey: Buffer, + opts: { + message: MessageRelaxed | Cell; + mode: number; + queryId: HighloadQueryId | bigint; + createdAt: number; + subwalletId: number; + timeout: number; + }, +) => { + let shift: bigint; + let bitNumber: bigint; + if (typeof opts.queryId === "bigint") { + shift = opts.queryId >> 10n; + bitNumber = opts.queryId & 1023n; + } else { + shift = opts.queryId.getShift(); + bitNumber = opts.queryId.getBitNumber(); + } + + const messageCell = + opts.message instanceof Cell + ? opts.message + : beginCell().store(storeMessageRelaxed(opts.message)).endCell(); + + const msgInnerStruct: MsgInner = { + $$type: "MsgInner", + subwalletID: BigInt(opts.subwalletId), + messageToSend: messageCell, + sendMode: BigInt(opts.mode), + queryID: { + $$type: "QueryID", + shift, + bitNumber, + }, + createdAt: BigInt(opts.createdAt), + timeout: BigInt(opts.timeout), + }; + + const msgInnerCell = beginCell() + .store(storeMsgInner(msgInnerStruct)) + .endCell(); + + const msgInnerHash = msgInnerCell.hash(); + const signature = sign(msgInnerHash, secretKey); + + const externalRequestStruct: ExternalRequest = { + $$type: "ExternalRequest", + signature: signature, + signedMsg: msgInnerCell, + }; + + return beginCell() + .store(storeExternalRequest(externalRequestStruct)) + .endCell(); +};