diff --git a/Contest.md b/Contest.md new file mode 100644 index 00000000..c3be22f3 --- /dev/null +++ b/Contest.md @@ -0,0 +1,26 @@ +## Contest note! + +
+Contest logo +
+ +**Because of the extreme amount of optimizations, developer's discretion is advised!** *Evil laugh* + +The build system is the same as in the original Wallet V5, **no security features have been sacrificed** +for performance improvements, that is - there are **practically no tradeoffs or compromises**. + +Message and storage layouts were **not changed**, although some rearragement might squeeze a little more gas, +but that may break existing optimizations due to stack reordering. + +Also, **tests were improved** - a **Global Gas Counter** mechanism was added that accounts for gas in all transactions +of all test suites and cases (except for negative and getter ones). This allows to keep an eye on other non-contest +cases to track how bad is tradeoff when performing optimizations here and there. + +Another utility that was developed for contest is ***scalpel script***, that allows for a detailed, *really* detailed optimizations +of the code by comparing lines of code function by function, printing out diffs, and providing detailed TVM files with +stack comments and rewrites. This utility allowed to make some latter optimizations, since with each optimization +next one becomes exponentionally harder to make. While result is not entirely precise and is needed to be verified +by tests, this allows to instantly estimate whether there is some progress or not, since scalpel is executed immediately, +while tests take approximately 10 seconds to execute. + +### Details of optimizations, their rationale and explanations, comparison of consumed gas both in test cases and not in test cases (global gas counter) are provided on a dedicated page: [Gas improvements](Improvements.rst). diff --git a/Improvements.rst b/Improvements.rst new file mode 100644 index 00000000..9560ab9d --- /dev/null +++ b/Improvements.rst @@ -0,0 +1,447 @@ +Improvements log +================ + +In this section a table is presented with optimization results in several projections. + +Contest test cases display how much gas is used by contest-like methods of test suite, total saved and percentage +as compared to the original commit gas use. + +Global gas counters were introduced after commit ``Keep your functions close and vars even closer`` to make a measurable +metric of "tradeoff" between contest test cases and all other cases, that are totalled by their corresponding test suite. + +Only positive (no fail) tests without getters (since there is no point to optimize getters) are included in the table. + +This metric proven to be useful, because an ``Localize extensions in loop and short-circ simple`` commit resulted in very +big jump in savings on the test cases, meanwhile it severly impaired all other cases (GGC increased a lot on it). As a +result, the very next commit ``Refactored internal message flows, good GGC value`` managed to bring the GGC below the initial +total level, with further commits do a ``stable development`` of contest test cases with improving GGC as well. + ++----------------------------------------------------------------+-------------------------------------------+--------------------------------+ +| Commit | Contest test cases | Global gas counters | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| .____________________________________________________________. | Ext | Int | Extn | Total | Save | Perc% | Exter | Inter | Exten | Total | ++================================================================+======+======+======+=======+======+=======+=======+=======+=======+========+ +| *Origin point: INITIAL* | 3235 | 4210 | 2760 | 10205 | 0 | 0.00% | 64038 | 71163 | 38866 | 174067 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Optimized unneccessary cell loads and operations | 3185 | 4014 | 2744 | 9943 | 262 | 2.56% | 65556 | 70764 | 40304 | 176624 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Removed unneccessary always true check | 3185 | 3823 | 2501 | 9509 | 696 | 6.82% | 65504 | 68993 | 38998 | 173495 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Unrolled the common internal handler code | 3185 | 3700 | 2373 | 9258 | 947 | 9.28% | 65504 | 67886 | 38204 | 171594 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Implicitly return from the external handler | 3165 | 3700 | 2373 | 9238 | 967 | 9.48% | 65264 | 67886 | 38204 | 171354 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Reaped benefits of separated internal loaders | 3165 | 3700 | 2295 | 9160 | 1045 | 10.2% | 65264 | 67886 | 37736 | 170886 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Discarded unneccessary slice remains in dispatcher | 3155 | 3690 | 2285 | 9130 | 1075 | 10.5% | 65034 | 67716 | 37646 | 170396 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Loaded auth_kind optionally using LDUQ instruction | 3155 | 3654 | 2249 | 9058 | 1147 | 11.2% | 65050 | 67408 | 37430 | 169888 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Is ifnot a joke for you? (emits less instructions) | 3155 | 3654 | 2231 | 9040 | 1165 | 11.4% | 65050 | 67408 | 37322 | 169780 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Localize extensions in loop and short-circ simple | 3045 | 3644 | 2121 | 8810 | 1395 | 13.7% | 69697 | 71316 | 39314 | 180327 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Reordering int msg handlers somehow saves 10 gas | 3045 | 3567 | 2188 | 8800 | 1405 | 13.8% | 69697 | 70623 | 39716 | 180036 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Moving signature check higher saves some gas | 3027 | 3549 | 2188 | 8764 | 1441 | 14.1% | 69481 | 70461 | 39716 | 179658 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Reordering checks somehow sames some more gas | 3009 | 3531 | 2188 | 8728 | 1477 | 14.5% | 69265 | 70299 | 39716 | 179280 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Removing end_parse is -gas and +reliability | 2983 | 3505 | 2188 | 8676 | 1529 | 15.0% | 68953 | 70065 | 39716 | 178734 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Keep your functions close and vars even closer | 2957 | 3505 | 2188 | 8650 | 1555 | 15.2% | 68641 | 70065 | 39716 | 178422 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| < restore extensions var in loop > | 3067 | 3533 | 2288 | 8888 | | | 65669 | 67568 | 38456 | | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| < move complex logic to inline_ref > | 2957 | 3423 | 2316 | 8696 | | | 65528 | 67495 | 39148 | | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| < optimize tail loading of extensions > | 2957 | 3423 | 2298 | 8678 | | | 65528 | 67495 | 39040 | | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| < optimize preference for simple ext ops > | 2957 | 3423 | 2248 | 8628 | | | 65528 | 67495 | 39324 | | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Refactored internal message flows, good GGC value | 2957 | 3423 | 2248 | 8628 | 1577 | 15.5% | 65528 | 67495 | 39324 | 172347 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| ^^^ This commit finally shows TOTAL GGC less then initial one WHILE providing 15.5% gas save on contest test cases! ^^^ | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Reorganized inlining point for extension message flow | 2957 | 3423 | 2230 | 8610 | 1595 | 15.6% | 65528 | 67495 | 38782 | 171805 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Do not carry around params not needed (ext opt) | 2957 | 3423 | 2176 | 8556 | 1649 | 16.2% | 65176 | 67275 | 38586 | 171037 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Optimize argument order to match stack | 2939 | 3405 | 2148 | 8492 | 1713 | 16.8% | 64960 | 67113 | 38346 | 170419 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Swapping extn and sign order back saves some net gas | 2939 | 3464 | 2063 | 8466 | 1739 | 17.0% | 65004 | 67676 | 37876 | 170556 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Short-circuit optimization of LDUQ with IFNOTRET | 2939 | 3420 | 2019 | 8378 | 1827 | 17.9% | 64929 | 67205 | 37612 | 169746 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| < short-circuit flags check with asm > | 2939 | 3402 | 2001 | 8342 | | | | | | | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| < short-circuit int msg sign last check with asm > | 2939 | 3376 | 2001 | 8316 | | | | | | | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| < short-circuit ext msg sign last check with asm > | 2913 | 3376 | 2001 | 8290 | | | | | | | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| < short-circuit extension dictionary check with asm > | 2913 | 3376 | 1983 | 8272 | | | | | | | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Short-circuited some returns with asm | 2913 | 3376 | 1983 | 8272 | 1933 | 18.9% | 64599 | 66791 | 37373 | 168763 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| ASM-optimized simple action cases | 2885 | 3348 | 1981 | 8214 | 1991 | 19.5% | 64470 | 66700 | 37351 | 168521 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Optimized out more unneeded instructions if may RET | 2885 | 3338 | 1955 | 8178 | 2027 | 19.9% | 64470 | 66610 | 37177 | 168257 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Removed another unneccessary DROP with preload uint | 2875 | 3338 | 1955 | 8168 | 2037 | 20.0% | 64350 | 66610 | 37177 | 168137 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| ^^^ Finally got **20%** optimization in test cases! ^^^ | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Reordered argument order to optimize stack operations | 2857 | 3320 | 1955 | 8132 | 2073 | 20.3% | 64134 | 66448 | 37137 | 167719 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Rewritten RETALT to IFNOTJMP - less gas, more reliable | 2836 | 3299 | 1934 | 8069 | 2136 | 20.9% | 64071 | 66406 | 37220 | 167697 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Another argument stack optimization (psr -> dr call) | 2818 | 3281 | 1934 | 8033 | 2172 | 21.3% | 64017 | 66370 | 37220 | 167607 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| ^^^ Finally this solution is better then original in **EVERY** test suite branch! The last hurdle - Exter GGC is now less!!! ^^^ | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Black magic route optimization (drop some result later) | 2818 | 3281 | 1916 | 8015 | 2190 | 21.5% | 64017 | 66370 | 37130 | 167517 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Another black magic optimization (drop auth_kind later) | 2818 | 3281 | 1906 | 8005 | 2200 | 21.6% | 64017 | 66370 | 37102 | 167489 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Backported FunC optimizations from entrypoint branch | 2782 | 3373 | 1824 | 7979 | 2226 | 21.8% | 60011 | 64810 | 34138 | 158959 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Use SDBEGINS to enforce prefix in external message | 2710 | 3373 | 1824 | 7907 | 2298 | 22.5% | 59147 | 64810 | 34138 | 158095 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Use SDBEGINSQ to check internal message prefixes | 2710 | 3283 | 1736 | 7729 | 2476 | 24.3% | 59237 | 64090 | 33578 | 156905 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Backport some optimizations from EP and coalesce code | 2699 | 3165 | 1828 | 7692 | 2513 | 24.6% | 59108 | 63031 | 34141 | 156280 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| Optimized instructions order for extension and fix args | 2699 | 3165 | 1810 | 7674 | 2531 | 24.8% | 59108 | 63031 | 34033 | 156172 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ +| *Reminder and origin point: INITIAL* | 3235 | 4210 | 2760 | 10205 | 0 | 0.00% | 64038 | 71163 | 38866 | 174067 | ++----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ + +*It seems that backporting optimization wiggles around values here and there.* To get the maximum possible gas savings please consider taking +a look at ``entrypoint`` ("radical") branch. Since optimizations are carefully made there, they decrease used gas by all cases without compromises. + +As an example, here is a comparison of used gas in main ("conservative") and entrypoint ("radical") branches: + ++-----------------+----------+----------------+ +| Test case | main gas | entrypoint gas | ++=================+==========+================+ +| External | 2699 | **2707** | ++-----------------+----------+----------------+ +| Internal | 3165 | **2963** | ++-----------------+----------+----------------+ +| Extension | 1810 | **1608** | ++-----------------+----------+----------------+ +| External GGC | 59108 | **58893** | ++-----------------+----------+----------------+ +| Internal GGC | 63031 | **61011** | ++-----------------+----------+----------------+ +| Extension GGC | 34033 | **32821** | ++-----------------+----------+----------------+ + +The external test case uses a tiny miniscule more gas due to if ordering, making it way around messes up cell slicing completely. +Nevertheless, external global counter is still less, therefore the overall result is not that bad. + +N.B. Contest multiplier: 9905/10205 = 0.9706 (approximate) -> place multipliers ~ 0.3235342, 0.0970602, 0.0485301 + +Details and rationale +===================== + +In this section, details of optimization in each commit are pointed out, sometimes with detailed rationale and reasoning, +when neccessary (in some relatively controversial optimizations). + +Origin point: INITIAL +--------------------- +The origin point, the state of the contract when the contest was started. It is used as a basis point to measure further improvements. + +Optimized unneccessary cell loads and operations +------------------------------------------------ +There is some data that is not needed to be loaded right away, since most likely it won't be used, so that data loading is deferred +until the moment it is actually needed. First of all, that is ``extensions`` dictionary, since loading dict (consequently, a cell) +is a pretty expensive operation. + +Also, reading ``stored_subwallet, public_key, extensions`` and writing them back just to increase ``stored_seqno`` is completely +unneccessary, so I took a snapshot of slice immediately after ``stored_seqno``, and write it as a slice, instead of 3 write operations +when increasing the ``stored_seqno``. + +Instead of ``extensions`` now ``immutable_tail`` is being passed around, and ``extensions`` are extracted from it, when needed. + +Removed unneccessary always true check +-------------------------------------- +Adding return to the if condition decreased amount of gas (due to turning ``IF`` into ``IFJMP``), and, consequently, +second check of opcode is not required, since it is allowed to be only one of two options, one of which was already checked. + +Unrolled the common internal handler code +----------------------------------------- +Copying the common data load code to separate execution paths in internal message handler somehow saves considerable amount +of gas, but, most importantly, allows to optimize the data loading in future (since it is now different code). + +Implicitly return from the external handler +------------------------------------------- +*Explicity* (commit name has logic mistake) returning from the external handler saves some gas due to some TVM optimizations. + +Reaped benefits of separated internal loaders +--------------------------------------------- +Because data loading is now handled separately for signed and extension messages, it is possible to optimize data loading +so as not to waste unneccessary gas to load data that is not required for a specific execution path. + +More precisely, extensions are now loaded from immutable tail, that allows to streamline stack manipulations that decrease +amount of used gas, also, this logic will be even more simplified in future to save even more gas. + +Discarded unneccessary slice remains in dispatcher +-------------------------------------------------- +Using ``preload_ref`` instead of ``load_ref`` on a varible that is not used anymore saves considerable amount of gas, since +it is not required anymore to do stack manipulations and dropping the unneccessary result. + +Loaded auth_kind optionally using LDUQ instruction +-------------------------------------------------- +An ``LDUQ`` TVM instruction was used to construct a ``try_load_uint32`` that attempts to load an ``uint32`` from a slice, +and returns the success indicator alongside with result, that allows to compact checking of availability of bits in slice +and reading the integer itself into one instruction - less branching, instructions, checks and gas. + +Is ifnot a joke for you? (emits less instructions) +-------------------------------------------------- +Using ``ifnot`` instead of ``if ~...`` saves gas, since ``NOT`` instruction is not needed anymore. ``ifnot`` has same price +and bit length as the ``if``, therefore it is **always** advised to use ``ifnot`` for negative conditions. + +Localize extensions in loop and short-circ simple +------------------------------------------------- +In this commit, there are two different changes. First one is localizing ``extensions`` inside loop, that allowed to save +some gas in case ``extensions`` are not needed to be changed. + +**The second one is one of the most important optimizations**, that opens the door for many more further gas optimizations +in the code. The idea is that if the message is simple, that it, has no extended actions (the first bit is right away 0), +it is possible to immediately do the ``set_actions`` and ``return``. + +While the first idea has a noticeable tradeoff, that will be eliminated in future by optimizations all around the code, +the second one does not make other execution paths more pricey, while making the main ones much better in terms of gas. + +Reordering int msg handlers somehow saves 10 gas +------------------------------------------------ +Moving ``sign`` above ``extn`` one in internal message handler somehow saved 10 gas. + +Moving signature check higher saves some gas +-------------------------------------------- +In ``process_signed_request`` moving signature check to the top of the function saves some gas. + +Reordering checks somehow sames some more gas +--------------------------------------------- +In ``process_signed_request`` changing order of parameter checks decreased amount of stack manipulations and saved some gas. + +Removing end_parse is -gas and +reliability +------------------------------------------- +In this commit, ``end_parse`` (and coincidentally now unneeded ``skip_dict``) was removed from this code. This leads to +increased reliability, less gas usage, and opens road to some more optimizations (like tail preloading). + +**While decreasing gas usage and opening road to more optimizations is pretty obvious, let's me explain on the increased reliabilty.** + +The idea behind it is, that usually, ``end_parse`` is used to force structure of user messages. Therefore, mostly, using +it to enforce structure of internal data of the contract is quite excessive, since the contract itself is the one, who +only can write it's own data, and therefore if it cannot be corrupted by the code, then there is no way extra data appears +after the expected end. Therefore, using ``end_parse`` is unneccessary, and just wastes gas. + +However, in this contract the user can directly do ``set_data`` using extended actions on the contract. And here is the point +why reliability of the contract is actually **increased** by removing the ``end_parse``. It is possible in future, that the +user might accidentally append extra data to the end of the contract. This may happen if the user would like to upgrade the +contract, it will have some more extra data, but for some reason failed or forgot to do the code upgrade action, or it failed +one or another way. In this situation the user will end up with **the old contract with the new data**. And in this situation, +all the TONs, tokens and NFTs on this wallet will be locked **forever!!!** just because of that ``end_parse``. Therefore, +removing the ``end_parse`` also helps against such kind of mistakes, and there are no any kind of implications on removing it. + +The only place where it should **really** be used is checking close-structured (without open ends, like in our case, where +the list can be of any length) input user data, in order to make sure, that a specific request can have only one single +implementation in order to prevent some playing with signatures, but that is completely not an our case. + +Keep your functions close and vars even closer +---------------------------------------------- +This refactoring of external message handler streamlines data flows in it, therefore avoiding unneccessary stack manipulations +and saving some gas as a result. More precisely, the ``auth_kind`` is loaded right away from ``body`` (since it is the last +parameter of the function, it is at the top of the stack at that moment), and data is being loaded later after the check. + +Refactored internal message flows, good GGC value +------------------------------------------------- +This commit, and several other technical commits before it (not described here, since they are technical ones and do not +affect the code) lays beginning for calculation and optimizations of **GGC** (global gas counter). While not being a direct +target of the contest, the **GGC** is important metric, that allows to measure the tradeoff, of how optimizing contest paths +inadversely affects all other logic of the code that is not measured. Therefore, keeping an eye on **GGC** is important for +**sustained development** of contest paths, where optimizing them does not severely impair all other code logic. + +This commit, while increasing extension gas usage a little (this problem will be addressed to and solved in later commits), +immensely decreases usage of gas in GGC, and brings it down below the GGC in initial commit. Therefore, starting at this point, +I can strongly assert, that the optimizations of the main contest paths do not impair the other code paths and logic. + +restore extensions var in loop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +First of all, ``extensions`` variable in complex handling loop was reinstated, because saving exts in cell and popping them +off each time required a lot of gas due to recreation of cell each time. + +move complex logic to inline_ref +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Next, the complex dispatch request handling logic was moved off to a separate function, that is called with ``inline_ref`` +modifier. This allows to save some gas on simple cases, and **is actually a very important optimization for future**, because +at some point in future, the *cell breaking point* where TVM Assembler decides to break cell into pieces because a critical +point for further optimization. + +optimize tail loading of extensions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The way how extensions are loaded in internal message handler is optimized so as not to load the unneccessary at that moment data. + +optimize preference for simple ext ops +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Simple operations initiated by extensions now do not require to load the unneccessary data from the contract. + +Reorganized inlining point for extension message flow +----------------------------------------------------- +Some optimizations were made to tell the compiler to break the cell at exact place by using ``inline`` and ``inline_ref`` accurately. + +Do not carry around params not needed (ext opt) +----------------------------------------------- +Getting the data of the contract in place, even accounting for the ``begin_slice`` is more efficient than carrying it around +in many parameters, that forces stack shaping when crossing the function boundary, and constraints on how efficient stack +manipulations may be, therefore all the unneccessary parameters were removed and data is extracted closer to the point +where it is actually needed. + +Optimize argument order to match stack +-------------------------------------- +Some parameters were reordered to match how they are ordered in stack, so that to decrease amount of unneccessary stack operations. + +Swapping extn and sign order back saves some net gas +---------------------------------------------------- +In internal message handler ``sign`` and ``extn`` message handlers were swapped back once again, since somehow, after all the +optimizations carried out above, that order is now more efficient in terms of gas. + +Short-circuit optimization of LDUQ with IFNOTRET +------------------------------------------------ +Instead of pretty complex in terms of instructions and gas FunC construct, a single ``IFNOTRET`` is used to quickly end +execution when there are not enough bits in the slice to obtain the opcode from the internal message. + +Short-circuited some returns with asm +------------------------------------- +Following the idea of the previous commit, some more operations now use ``IF(NOT)RET`` instead of conditionals to save more gas. + +short-circuit flags check with asm +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Flags of internal message (bounced, to be more precise) are now checked by a concise ASM function that does ``IFRET`` to +end the execution in case a bounced message is detected. + +short-circuit int msg sign last check with asm +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The check of second operation can be made shorter by comparing two numbers equality and performing ``IFNOTRET`` in ASM. + +short-circuit ext msg sign last check with asm +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The same applies for opcode check in internal message handler. + +short-circuit extension dictionary check with asm +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... and the ``success?`` result of locating the sender in the ``extensions`` dictionary. + +ASM-optimized simple action cases +--------------------------------- +An optimized code construct was built to replace the not-so-efficient FunC code for simple function cases. This one uses +a specific ordering of result on the stack after executing the neccessary instructions. + +Optimized out more unneeded instructions if may RET +--------------------------------------------------- +An ``udict_get_or_return`` instruction was introduced that instead of returning ``success?`` alongside with the result +returns immediately if the entry is not found in the dictionary. + +Also, I have noticed, that ``public_key`` is read from ``cs`` using ``~load_uint``, but that ``cs`` is not used anymore +in the code, so saved an unneccessary ``DROP`` by using ``.preload_uint`` instead. + +Removed another unneccessary DROP with preload uint +--------------------------------------------------- +The same optimization for ``public_key`` loading was done in the external message handler in this commit. + +Reordered argument order to optimize stack operations +----------------------------------------------------- +Some arguments were reordered to save gas on stack manipulations. Also, another ``public_key`` loading was optimized (the +last one, in the extension handler execution path). + +Rewritten RETALT to IFNOTJMP - less gas, more reliable +------------------------------------------------------ +The simple actions handler was rewritten from ``IFNOT:<{ ... RETALT }>`` to ``IFNOTJMP:<{ ... }>``. This saves some gas +(since implicit returns are cheaper), and makes the code more reliable (since we cannot be 100% sure that ``RETALT`` will +end the execution as expected if the code will be modified in future, therefore using ``IFNOTJMP`` eliminates this uncertainity). + +Another argument stack optimization (psr -> dr call) +---------------------------------------------------- +Some another reordering of function arguments was done to eliminate unneccessary stack operations. + +Black magic route optimization (drop some result later) +------------------------------------------------------- +An unused result of extension dictionary checking is now carried around inside the called function in order to be dropped +later after the simple actions checker. Surprisingly, this does not impair non-test code paths at all, since the ``DROP`` +at the end of simple actions checker is merged with drop of the carried result into ``2DROP``, thus having no drawbacks. + +Another black magic optimization (drop auth_kind later) +------------------------------------------------------- +Another variable is now called around for delayed drop, this time ``auth_kind``, which turns ``2DROP`` into ``3 BLKDROP``, +that is still not bad, increases gas efficiency on primary paths, and does not impair it on other ones. + +Backported FunC optimizations from entrypoint branch +---------------------------------------------------- +Backported some FunC optimizations done in entrypoint branch (although, they may be not as efficient): + +Rearranged entrypoint conditions flow, compiler fix +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +External and internal message processing conditions order are swapped that result in less gas usage overall. Also, some +mistakes in TVM Assembler are fixed and functions were renamed so as not to accidentally compile it using an ordinary compiler. + +some commits not affecting the main test branches +""""""""""""""""""""""""""""""""""""""""""""""""" +Some additional improvements to the complex dispatch case were made to decrease the global gas counters. This did not affect +the gas usage in the main test cases, but made my optimizations for friendly to the natur... to the other code branches. + +Removed unneccessary exploded data parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Moved data (``ds``) variables closer to their actual usage. Therefore it is not required to pass lots of those variables +in the arguments anymore saving some gas on stack reorganizations. + +Moreover, this allows to move data variable code inbetween other code in ``process_signed_request`` function, saving even +more code by optimizing order of operations. + +Use SDBEGINS to enforce prefix in external message +-------------------------------------------------- +I have found out a super useful ``SDBEGINS(Q)`` TVM instruction that allows to verify the prefix of a slice against another +one (in this version of function the prefix is even conveniently embedded into the instruction code itself), and even has +a very convenient behaviour of throwing if prefix does not match (that is very convenient for external message, since +returning from it without accepting message is effectively the same as throwing an exception), and returns the slice without +that prefix is correct, that perfectly matches the previous behaviour. + +As such, replacing compare and return with this instruction saves considerable amount of gas with no implications. + +Use SDBEGINSQ to check internal message prefixes +------------------------------------------------ +The quiet version of aforementioned instruction, ``SDBEGINSQ`` exhibits even more convenient behaviour for multi-case checking +and pipelining: on the top of the stack it puts whether the prefix matched or not, that can be consumed for any kind of condition +checks, and always returns a slice after it. The great behaviour is that if the prefix matched the returned slice is stripped of +it, and if the prefix did not match, the original slice is returned. This allows to use this instruction, branch into processing +code if it matched, or use it again if did not, and keep doing that (something like a switch-case). + +Therefore, I have used this instruction to check for opcode prefix in internal message processing. + +Backport some optimizations from EP and coalesce code +----------------------------------------------------- +Backported some more optimizations from entrypoint branch + +Use SDFIRST instead of PLDU to check first bit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +It is possible to use shorter ``SDFIRST`` instruction to check if first bit of slice is set, that saves some gas. + +I have used it in checking whether to use simple action processing code, that saves some gas in each execution branch. + +Check bounced flag using slices and trail bits +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +It is more efficient to get a 4-bit slice and check trailing bits with ``SDCNTTRAIL1`` (it will always be non-zero +if last bit (bounced) is non-zero, and it always will be zero if it is zero - a perfect instruction to check the last bit). +Therefore by such approach checking bounced flag bit is much more effective than loading 4-bit number from slice, pushing 1 +to stack, and performing the or operation. + +Using SDBEGINSQ to check for starting zero +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Like with internal message prefixes, it is more efficient to use a single ``SDBEGINSQ`` instruction to check that prefix +starts with zero and is a simple action even than preload a single uint1. + +Optimized instructions order for extension and fix args +------------------------------------------------------- +Adjusting order of instructions in extension branch allows to save some gas. Also fixed arguments because TON Plugin +was complaining (no gas or instructions change whatsoever). \ No newline at end of file diff --git a/README.md b/README.md index fe1b1cf6..af25b151 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,13 @@ Wallet V5 has 25% lower fees, can delegate payments for gas to third parties and - `contracts` - source code of all the smart contracts of the project and their dependencies. - `wrappers` - wrapper classes (implementing `Contract` from ton-core) for the contracts, including any [de]serialization primitives and compilation functions. - `tests` - tests for the contracts. -- `scripts` - scripts used by the project, mainly the deployment scripts. +- `scripts` - scripts used by the project, mainly the deployment scripts, additionally contains utilities for gas optimisation. +- `fift` - contains standard Fift v0.4.4 library including the assembler and disassembler for gas optimisation utilities. + +### Additional documentation + +- [Gas improvements](Improvements.rst) - a log of improvements, detailed by primary code paths, global gas counters per commit. +- [Contest](Contest.md) - a note showing some information about interesting improvements during the optimisation contest. ## How to use diff --git a/Specification.md b/Specification.md index 89238060..0e1906cf 100644 --- a/Specification.md +++ b/Specification.md @@ -19,16 +19,18 @@ Thanks to [Andrew Gutarev](https://github.com/pyAndr3w) for the idea to set c5 r Thanks to [@subden](https://t.me/subden), [@botpult](https://t.me/botpult) and [@tvorogme](https://t.me/tvorogme) for ideas and discussion. +Thanks to [Skydev](https://github.com/Skydev0h) for optimization and preparing the second revision of the contract. + ## Features * 25% smaller computation fees. * Arbitrary amount of outgoing messages is supported via action list. -* Wallet code can be upgraded transparently without breaking user's address in the future. * Wallet code can be extended by anyone in a decentralized and conflict-free way: multiple feature extensions can co-exist. * Extensions can perform the same operations as the signer: emit arbitrary messages on behalf of the owner, add and remove extensions. * Signed requests can be delivered via internal message to allow 3rd party pay for gas. * For consistency and ease of indexing, external messages also receive a 32-bit opcode. +* To lay foundation for support of scenarios like 2FA or access recovery it is possible to disable signature authentication. ## Overview @@ -39,8 +41,8 @@ Authentication: * by extension Operation types: -* standard output actions -* “set data” operation +* standard output send message action +* enable or disable public key (signature authentication) * install extension * remove extension @@ -63,40 +65,12 @@ User may delegate this job to other apps via extensions. ### Extending the wallet -**A. Use extensions** - The best way to extend functionality of the wallet is to use the extensions mechanism that permit delegating access to the wallet to other contracts. From the perspective of the wallet, every extension can perform the same actions as the owner of a private key. Therefore limits and capabilities can be embedded in such an extension with a custom storage scheme. Extensions can co-exist simultaneously, so experimental capabilities can be deployed and tested independently from each other. -**B. Code optimization** - -Backwards compatible code optimization **can be performed** with a single `set_code` action (`action_set_code#ad4de08e`) signed by the user. That is, hypothetical upgrade from `v5R1` to `v5R2` can be done in-place without forcing users to change their wallet address. - -If the optimized code requires changes to the data layout (e.g. reordering fields) the user can sign a request with two actions: `set_code` (in the standard action) and `set_data` (an extended action per this specification). Note that `set_data` action must make sure `seqno` is properly incremented after the upgrade as to prevent replays. Also, `set_data` must be performed right before the standard actions to not get overwritten by extension actions. The updated wallet **must** have the new subwallet ID to prevent accidental repeated migrations. - -User agents **should not** make `set_code` and `set_data` actions available via general-purpose API to prevent misuse and mistakes. Instead, they should be used as a part of migration logic for a specific wallet code. - -To restore the wallet by a seed phrase, user agent should use the original code and should expect the upgraded code to work in exactly the same way as previously. - -**C. Emergency upgrades** - -This is a variant of (B), so the same consideration apply. The difference is that functionality may be modified as to prevent user from suffering loss of funds. E.g. some previously possible actions or signed messages would lead to a failure. - -Just like with (B), user agents **should not** make `set_code` and `set_data` actions available via general-purpose API to prevent misuse and mistakes. Instead, they should be used as a part of migration logic for a specific wallet code. - -New users’ wallets **should not** be deployed with upgraded code. Instead, the improved wallet code should also be released as a new wallet version (e.g. v6, with a separate subwallet ID) and new wallets should be deployed with that code. This way `set_code` would be used as an emergency patch for existing wallets, while new wallets would be deployed directly with the major next version. - - -**D. Substantial upgrades** - -We **do not recommend** performing substantial wallet upgrades in-place using `set_code`/`set_data` actions. Instead, user agents should have support for multiple accounts and easy switching between them. - -In-place migration requires maintaining backwards compatibility for all wallet features, which in turn could lead to increase in code size and higher gas and rent costs. - - ### Can the wallet outsource payment for gas fees? Yes! You can deliver signed messages via an internal message from a 3rd party wallet. Also, the message is handled exactly like an external one: after the basic checks the wallet takes care of the fees itself, so that 3rd party does not need to overpay for users who actually do have TONs. @@ -123,6 +97,14 @@ You need to put two requests in your message body: Yes. We have considered constant-size schemes where the wallet only stores trusted extension code. However, extension authentication becomes combursome and expensive: plugin needs to transmit additional data and each request needs to recompute plugin’s address. We estimate that for the reasonably sized wallets (less than 100 plugins) authentication via the dictionary lookup would not exceed costs of indirect address authentication. +### Why it can be useful to disallow authentication with signature? + +Ability to disallow authentication with signature enables two related use-cases: + +1. Two-factor authentication schemes: where control over wallet is fully delegated to an extension that checks two signatures: the user’s one and the signature from the auth service. Naturally, if the signature authentication in the wallet remains allowed, the second factor check is bypassed. + +2. Account recovery: delegating full control to another wallet in case of key compromise or loss. Wallet may contain larger amount of assets and its address could be tied to long-term contracts, therefore delegation to another controlling account is preferred to simply transferring the assets. + ### What is library on masterchain? Library is a special code storage mechanism that allows to reduce storage cost for a new Wallet V5 contract instance. Wallet V5 contract code is stored into a masterchain library. @@ -143,7 +125,7 @@ wallet_id$_ global_id:int32 wc:int8 version:(## 8) subwallet_number:(## 32) = Wa - `global_id` is a TON chain identifier. TON Mainnet `global_id = -239` and TON Testnet `global_id = -3`. - `wc` is a Workchain. -1 for Masterchain and 0 for Basechain. - `version`: current version of wallet v5 is `0`. -- `subwallet_number` can be used to get multiplie wallet contracts binded to the single keypair. +- `subwallet_number` can be used to get multiple wallet contracts bound to the single keypair. ## Packed address @@ -166,48 +148,40 @@ Action types: ```tl-b // Standard actions from block.tlb: out_list_empty$_ = OutList 0; -out_list$_ {n:#} prev:^(OutList n) action:OutAction - = OutList (n + 1); -action_send_msg#0ec3c86d mode:(## 8) - out_msg:^(MessageRelaxed Any) = OutAction; -action_set_code#ad4de08e new_code:^Cell = OutAction; -action_reserve_currency#36e6b809 mode:(## 8) - currency:CurrencyCollection = OutAction; -libref_hash$0 lib_hash:bits256 = LibRef; -libref_ref$1 library:^Cell = LibRef; -action_change_library#26fa1dd4 mode:(## 7) { mode <= 2 } - libref:LibRef = OutAction; +out_list$_ {n:#} prev:^(OutList n) action:OutAction = OutList (n + 1); +action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; // Extended actions in W5: action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0; action_list_extended$1 {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1); -action_set_data#1ff8ea0b data:^Cell = ExtendedAction; action_add_ext#1c40db9f addr:MsgAddressInt = ExtendedAction; action_delete_ext#5eaef4a4 addr:MsgAddressInt = ExtendedAction; +action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; ``` Authentication modes: ```tl-b -signed_request$_ - signature: bits512 // 512 - subwallet_id: uint32 // 512+32 - valid_until: uint32 // 512+32+32 - msg_seqno: uint32 // 512+32+32+32 = 608 - inner: InnerRequest = SignedRequest; - -internal_signed#7369676E signed:SignedRequest = InternalMsgBody; -internal_extension#6578746E inner:InnerRequest = InternalMsgBody; -external_signed#7369676E signed:SignedRequest = ExternalMsgBody; +signed_request$_ // 32 (opcode from outer) + wallet_id: WalletID // 80 + valid_until: # // 32 + msg_seqno: # // 32 + inner: InnerRequest // 1 .. (1 + 32 + 256) + ^Cell + signature: bits512 // 512 += SignedRequest; // Total: 688 .. 976 + ^Cell + +internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; +internal_extension#6578746e inner:InnerRequest = InternalMsgBody; +external_signed#7369676e signed:SignedRequest = ExternalMsgBody; actions$_ {m:#} {n:#} actions:(ActionList n m) = InnerRequest; ``` Contract state: ```tl-b -wallet_id$_ global_id:int32 wc:int8 version:(## 8) subwallet_number:(## 32) = WalletID; -contract_state$_ seqno:# wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; +wallet_id$_ global_id:# wc:int8 version:(## 8) subwallet_number:# = WalletID; +contract_state$_ seqno:int33 wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; ``` ## Source code diff --git a/contest.png b/contest.png new file mode 100644 index 00000000..ab835439 Binary files /dev/null and b/contest.png differ diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index f7d8c104..637b3455 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -2,164 +2,339 @@ #include "imports/stdlib.fc"; +const int size::stored_seqno = 33; +const int size::stored_subwallet = 80; +const int size::public_key = 256; + +const int size::subwallet_id = 80; +const int size::valid_until = 32; +const int size::msg_seqno = 32; + +const int size::flags = 4; + +() return_if(int cond) impure asm "IFRET"; +() return_unless(int cond) impure asm "IFNOTRET"; + +(slice) udict_get_or_return(cell dict, int key_len, int index) impure asm(index dict key_len) "DICTUGET" "IFNOTRET"; + +(slice) enforce_and_remove_sign_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; +(slice, int) check_and_remove_extn_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; +(slice, int) check_and_remove_sint_prefix(slice body) impure asm "x{73696E74} SDBEGINSQ"; + +;; (slice, int) check_and_remove_set_data_prefix(slice body) impure asm "x{1ff8ea0b} SDBEGINSQ"; +(slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; +(slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; +(slice, int) check_and_remove_set_signature_auth_allowed_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; + +(slice) enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; + ;; Extensible wallet contract v5 ;; Compresses 8+256-bit address into 256-bit uint by cutting off one bit from sha256. ;; This allows us to save on wrapping the address in a cell and make plugin requests cheaper. ;; This method also unpacks address hash if you pass packed hash with the original wc. -int pack_address(int wc, int hash) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) +int pack_address((int, int) address) impure asm "SWAP" "INC" "XOR"; ;; hash ^ (wc+1) ;; Stores pre-computed list of actions (mostly `action_send_msg`) in the actions register. () set_actions(cell action_list) impure asm "c5 POP"; +int count_leading_zeroes(slice cs) asm "SDCNTLEAD0"; +int count_trailing_zeroes(slice cs) asm "SDCNTTRAIL0"; +int count_trailing_ones(slice cs) asm "SDCNTTRAIL1"; + +;; (slice, slice) split(slice s, int bits, int refs) asm "SPLIT"; +;; (slice, slice, int) split?(slice s, int bits, int refs) asm "SPLIT" "NULLSWAPIFNOT"; + +slice get_last_bits(slice s, int n) asm "SDCUTLAST"; +slice remove_last_bits(slice s, int n) asm "SDSKIPLAST"; + +cell verify_actions(cell c5) inline { + ;; Comment out code starting from here to disable checks (unsafe version) + ;; {- + slice c5s = c5.begin_parse(); + return_if(c5s.slice_empty?()); + do { + ;; only send_msg is allowed, set_code or reserve_currency are not + c5s = c5s.enforce_and_remove_action_send_msg_prefix(); + ;; enforce that send_mode has 2 bit set + ;; for that load 7 bits and make sure that they end with 1 + throw_if(37, count_trailing_zeroes(c5s.preload_bits(7))); + c5s = c5s.preload_ref().begin_parse(); + } until (c5s.slice_empty?()); + ;; -} + return c5; +} + ;; Dispatches already authenticated request. -() dispatch_request(slice cs, int stored_seqno, int stored_subwallet, int public_key, cell extensions) impure inline { +;; this function is explicitly included as an inline reference - not completely inlined +;; completely inlining it causes undesirable code split and noticeable gas increase in some paths +() dispatch_complex_request(slice cs) impure inline_ref { ;; Recurse into extended actions until we reach standard actions while (cs~load_uint(1)) { - int op = cs~load_uint(32); + var is_add_ext = cs~check_and_remove_add_extension_prefix(); + var is_del_ext = cs~check_and_remove_remove_extension_prefix(); + ;; Add/remove extensions + if ((is_add_ext) | (is_del_ext)) { + (int wc, int hash) = parse_std_addr(cs~load_msg_addr()); + int packed_addr = pack_address((wc, hash) ); - ;; Raw set_data - if (op == 0x1ff8ea0b) { - set_data(cs~load_ref()); - } + var ds = get_data().begin_parse(); + var data_bits = ds~load_bits(size::stored_seqno + size::stored_subwallet + size::public_key); + var extensions = ds.preload_dict(); - ;; Add/remove extensions - if ((op == 0x1c40db9f) | (op == 0x5eaef4a4)) { - (int wc, int hash) = parse_std_addr(cs~load_msg_addr()); - int packed_addr = pack_address(wc, hash); - - ;; Add extension - if (op == 0x1c40db9f) { - (extensions, int success?) = extensions.udict_add_builder?(256, packed_addr, begin_cell().store_int(wc,8)); - throw_unless(39, success?); - } - ;; Remove extension - if (op == 0x5eaef4a4) { - (extensions, int success?) = extensions.udict_delete?(256, packed_addr); - throw_unless(40, success?); - } - - set_data(begin_cell() - .store_uint(stored_seqno, 32) - .store_uint(stored_subwallet, 80) - .store_uint(public_key, 256) - .store_dict(extensions) - .end_cell()); + ;; Add extension + if (is_add_ext) { + (extensions, int success?) = extensions.udict_add_builder?(256, packed_addr, begin_cell().store_int(wc,8)); + throw_unless(39, success?); + } else + ;; Remove extension if (op == 0x5eaef4a4) + ;; It can be ONLY 0x1c40db9f OR 0x5eaef4a4 here. No need for second check. + { + (extensions, int success?) = extensions.udict_delete?(256, packed_addr); + throw_unless(40, success?); } - ;; Other actions are no-op - ;; FIXME: is it costlier to check for unsupported actions and throw? - cs = cs~load_ref().begin_parse(); + set_data(begin_cell() + .store_slice(data_bits) + .store_dict(extensions) + .end_cell()); + } + elseif (cs~check_and_remove_set_signature_auth_allowed_prefix()) { + var allow = cs~load_uint(1); + var ds = get_data().begin_parse(); + var stored_seqno = ds~load_int(size::stored_seqno); + var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions + if (allow) { + ;; allow + throw_unless(43, stored_seqno < 0); + ;; Can't be disallowed with 0 because disallowing increments seqno + ;; -123 -> 123 -> 124 + stored_seqno = - stored_seqno; + stored_seqno = stored_seqno + 1; + } else { + ;; disallow + throw_unless(43, stored_seqno >= 0); + ds = ds.skip_bits(size::stored_subwallet + size::public_key); + var extensions_is_not_null = ds.preload_uint(1); + throw_unless(42, extensions_is_not_null); + ;; Corner case: 0 -> 1 -> -1 + ;; 123 -> 124 -> -124 + stored_seqno = stored_seqno + 1; + stored_seqno = - stored_seqno; + } + set_data(begin_cell() + .store_int(stored_seqno, size::stored_seqno) + .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions + .end_cell()); + } + ;; Uncomment to allow set_data (for unsafe version) + {- + elseif (cs~check_and_remove_set_data_prefix()) { + set_data(cs~load_ref()); + } + -} + else { + ;; need to throw on unsupported actions for correct flow and for testability + throw(41); ;; unsupported action + } + cs = cs.preload_ref().begin_parse(); } ;; At this point we are at `action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0;` - ;; Simply set the C5 register with all pre-computed actions: - set_actions(cs~load_ref()); + ;; Simply set the C5 register with all pre-computed actions after verification: + set_actions(cs.preload_ref().verify_actions()); return (); } +;; ------------------------------------------------------------------------------------------------ + ;; Verifies signed request, prevents replays and proceeds with `dispatch_request`. -() process_signed_request(slice body, int stored_seqno, int stored_subwallet, int public_key, cell extensions) impure inline { - var signature = body~load_bits(512); - var cs = body; - var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(80), cs~load_uint(32), cs~load_uint(32)); +() process_signed_request_from_external_message(slice full_body) impure inline { + ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. + slice signature = full_body.get_last_bits(512); + slice signed = full_body.remove_last_bits(512); - throw_if(36, valid_until <= now()); + var cs = signed.skip_bits(32); + var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); + + var ds = get_data().begin_parse(); + var stored_seqno = ds~load_int(size::stored_seqno); + var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions + var stored_subwallet = ds~load_uint(size::stored_subwallet); + var public_key = ds.preload_uint(size::public_key); + + ;; TODO: Consider moving signed into separate ref, slice_hash consumes 500 gas just like cell creation! + ;; Only such checking order results in least amount of gas + throw_unless(35, check_signature(slice_hash(signed), signature, public_key)); + ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 + ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 + ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed throw_unless(33, msg_seqno == stored_seqno); throw_unless(34, subwallet_id == stored_subwallet); - throw_unless(35, check_signature(slice_hash(body), signature, public_key)); + throw_if(36, valid_until <= now()); accept_message(); ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. stored_seqno = stored_seqno + 1; set_data(begin_cell() - .store_uint(stored_seqno, 32) - .store_uint(stored_subwallet, 80) - .store_uint(public_key, 256) - .store_dict(extensions) - .end_cell()); + .store_int(stored_seqno, size::stored_seqno) + .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions + .end_cell()); commit(); - dispatch_request(cs, stored_seqno, stored_subwallet, public_key, extensions); + if (count_leading_zeroes(cs)) { ;; starts with bit 0 + return set_actions(cs.preload_ref().verify_actions()); + } + ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> + + ;; inline_ref required because otherwise it will produce undesirable JMPREF + dispatch_complex_request(cs); +} + +() recv_external(slice body) impure inline { + slice full_body = body; + ;; 0x7369676E ("sign") external message authenticated by signature + body = enforce_and_remove_sign_prefix(body); + process_signed_request_from_external_message(full_body); + return(); } -() recv_external(slice body) impure { +;; ------------------------------------------------------------------------------------------------ + +() dispatch_extension_request(slice cs, var dummy1) impure inline { + if (count_leading_zeroes(cs)) { ;; starts with bit 0 + return set_actions(cs.preload_ref().verify_actions()); + } + ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> + ;; + dummy1~impure_touch(); ;; DROP merged to 2DROP! + dispatch_complex_request(cs); +} + +;; Same logic as above function but with return_* instead of throw_* and additional checks to prevent bounces +() process_signed_request_from_internal_message(slice full_body) impure inline { + ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) + return_if(full_body.slice_bits() < 32 + size::subwallet_id + size::valid_until + size::msg_seqno + 1 + 512); + + ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. + slice signature = full_body.get_last_bits(512); + slice signed = full_body.remove_last_bits(512); + + var cs = signed.skip_bits(32); + var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); + var ds = get_data().begin_parse(); - var (stored_seqno, stored_subwallet, public_key, extensions) = (ds~load_uint(32), ds~load_uint(80), ds~load_uint(256), ds~load_dict()); - ds.end_parse(); - int auth_kind = body~load_uint(32); - if (auth_kind == 0x7369676E) { ;; "sign" - process_signed_request(body, stored_seqno, stored_subwallet, public_key, extensions); - } else { - ;; FIXME: probably need to throw here? - return (); + var stored_seqno = ds~load_int(size::stored_seqno); + var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions + var stored_subwallet = ds~load_uint(size::stored_subwallet); + var public_key = ds.preload_uint(size::public_key); + + ;; TODO: Consider moving signed into separate ref, slice_hash consumes 500 gas just like cell creation! + ;; Only such checking order results in least amount of gas + return_unless(check_signature(slice_hash(signed), signature, public_key)); + ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 + ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 + ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed + return_unless(msg_seqno == stored_seqno); + return_unless(subwallet_id == stored_subwallet); + return_if(valid_until <= now()); + + ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. + stored_seqno = stored_seqno + 1; + set_data(begin_cell() + .store_int(stored_seqno, size::stored_seqno) + .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions + .end_cell()); + + commit(); + + if (count_leading_zeroes(cs)) { ;; starts with bit 0 + return set_actions(cs.preload_ref().verify_actions()); } + ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> + + ;; inline_ref required because otherwise it will produce undesirable JMPREF + dispatch_complex_request(cs); } +() recv_internal(cell full_msg, slice body) impure inline { -() recv_internal(int msg_value, cell full_msg, slice body) impure { + ;; return right away if there are no references + ;; correct messages always have a ref, because any code paths ends with preload_ref + return_if(body.slice_refs_empty?()); + + ;; Any attempt to postpone msg_value deletion will result in s2 POP -> SWAP change. No use at all. var full_msg_slice = full_msg.begin_parse(); - var flags = full_msg_slice~load_uint(4); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... - if (flags & 1) { - ;; ignore all bounced messages - return (); - } - if (body.slice_bits() < 32) { - ;; ignore simple transfers - return (); - } - int auth_kind = body~load_uint(32); + + var s_flags = full_msg_slice~load_bits(size::flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... + + ;; If bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. + return_if(count_trailing_ones(s_flags)); + + ;; slicy_return_if_bounce(begin_cell().store_uint(3, 4).end_cell().begin_parse()); ;; TEST!!! ;; We accept two kinds of authenticated messages: ;; - 0x6578746E "extn" authenticated by extension - ;; - 0x7369676E "sign" authenticated by signature - if (auth_kind != 0x6578746E) & (auth_kind != 0x7369676E) { ;; "extn" & "sign" - ;; ignore all unauthenticated messages - return (); - } + ;; - 0x73696E74 "sint" internal message authenticated by signature - var ds = get_data().begin_parse(); - var (stored_seqno, stored_subwallet, public_key, extensions) = (ds~load_uint(32), ds~load_uint(80), ds~load_uint(256), ds~load_dict()); - ds.end_parse(); + (body, int is_extn) = check_and_remove_extn_prefix(body); ;; 0x6578746E ("extn") + + ;; IFJMPREF because unconditionally returns inside + if (is_extn) { ;; "extn" authenticated by extension - if (auth_kind == 0x6578746E) { ;; "extn" ;; Authenticate extension by its address. - int packed_sender_addr = pack_address(parse_std_addr(full_msg_slice~load_msg_addr())); - var (_, success?) = extensions.udict_get?(256, packed_sender_addr); - if ~(success?) { - ;; Note that some random contract may have deposited funds with this prefix, - ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). - return (); - } - dispatch_request(body, stored_seqno, stored_subwallet, public_key, extensions); - } - if (auth_kind == 0x7369676E) { ;; "sign" - ;; Process the rest of the slice just like the signed request. - process_signed_request(body, stored_seqno, stored_subwallet, public_key, extensions); + int packed_sender_addr = pack_address(parse_std_addr(full_msg_slice~load_msg_addr())); ;; no PLDMSGADDR exists + + var ds = get_data().begin_parse(); + ;; It is not required to read this data here, maybe ext is doing simple transfer where those are not needed + var extensions = ds.skip_bits(size::stored_seqno + size::stored_subwallet + size::public_key).preload_dict(); + + ;; Note that some random contract may have deposited funds with this prefix, + ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). + var wc = extensions.udict_get_or_return(256, packed_sender_addr); ;; kindof ifnot (success?) { return(); } + + ;; auth_kind and wc are passed into dispatch_extension_request and later are dropped in batch with 3 BLKDROP + dispatch_extension_request(body, wc); ;; Special route for external address authenticated request + return (); + } -} + slice full_body = body; + (_, int is_sint) = check_and_remove_sint_prefix(body); ;; 0x73696E74 ("sint") - sign internal + return_unless(is_sint); + ;; Process the rest of the slice just like the signed request. + process_signed_request_from_internal_message(full_body); + return (); ;; Explicit returns escape function faster and const less gas (suddenly!) + +} + +;; ------------------------------------------------------------------------------------------------ ;; Get methods int seqno() method_id { - return get_data().begin_parse().preload_uint(32); + ;; Use absolute value to do not confuse apps with negative seqno if key is disabled + return abs(get_data().begin_parse().preload_int(size::stored_seqno)); } int get_wallet_id() method_id { - return get_data().begin_parse().skip_bits(32).preload_uint(80); + return get_data().begin_parse().skip_bits(size::stored_seqno).preload_uint(size::stored_subwallet); } int get_public_key() method_id { - var cs = get_data().begin_parse().skip_bits(32 + 80); - return cs.preload_uint(256); + var cs = get_data().begin_parse().skip_bits(size::stored_seqno + size::stored_subwallet); + return cs.preload_uint(size::public_key); } ;; Returns raw dictionary (or null if empty) where keys are packed addresses and the `wc` is stored in leafs. ;; User should unpack the address using the same packing function using `wc` to restore the original address. cell get_extensions() method_id { - var ds = get_data().begin_parse().skip_bits(32 + 80 + 256); + var ds = get_data().begin_parse().skip_bits(size::stored_seqno + size::stored_subwallet + size::public_key); return ds~load_dict(); } + +int get_is_signature_auth_allowed() method_id { + return get_data().begin_parse().preload_int(size::stored_seqno) >= 0; +} \ No newline at end of file diff --git a/fift/Asm.fif b/fift/Asm.fif new file mode 100644 index 00000000..70503554 --- /dev/null +++ b/fift/Asm.fif @@ -0,0 +1,1457 @@ +library TVM_Asm +// simple TVM Assembler +namespace Asm +Asm definitions +"0.4.4" constant asm-fif-version + +variable @atend +variable @was-split +false @was-split ! +{ "not in asm context" abort } @atend ! +{ `normal eq? not abort"must be terminated by }>" } : @normal? +{ context@ @atend @ 2 { @atend ! context! @normal? } does @atend ! } : @pushatend +{ @pushatend Asm +{ }> b> } : }>c +{ }>c s +{ @atend @ 2 { true @was-split ! @atend ! rot b> ref, swap @endblk } does @atend ! = -rot <= and } : 2x<= +{ 2 pick brembitrefs 1- 2x<= } : @havebitrefs +{ @havebits ' @| ifnot } : @ensurebits +{ @havebitrefs ' @| ifnot } : @ensurebitrefs +{ rot over @ensurebits -rot u, } : @simpleuop +{ tuck sbitrefs @ensurebitrefs swap s, } : @addop +{ tuck bbitrefs @ensurebitrefs swap b+ } : @addopb +' @addopb : @inline +{ 1 ' @addop does create } : @Defop +{ 1 { } : si() +// x mi ma -- ? +{ rot tuck >= -rot <= and } : @range +{ rot tuck < -rot > or } : @-range +{ @-range abort"Out of range" } : @rangechk +{ dup 0 < over 255 > or abort"Invalid stack register number" si() } : s() +{ si() constant } : @Sreg +-2 @Sreg s(-2) +-1 @Sreg s(-1) +0 @Sreg s0 +1 @Sreg s1 +2 @Sreg s2 +3 @Sreg s3 +4 @Sreg s4 +5 @Sreg s5 +6 @Sreg s6 +7 @Sreg s7 +8 @Sreg s8 +9 @Sreg s9 +10 @Sreg s10 +11 @Sreg s11 +12 @Sreg s12 +13 @Sreg s13 +14 @Sreg s14 +15 @Sreg s15 +{ dup 0 < over 7 > or abort"Invalid control register number" } : c() +{ c() constant } : @Creg +0 @Creg c0 +1 @Creg c1 +2 @Creg c2 +3 @Creg c3 +4 @Creg c4 +5 @Creg c5 +7 @Creg c7 +{ abort"not a stack register" 12 i@+ s> } : @bigsridx +{ @bigsridx dup 16 >= over 0< or abort"stack register s0..s15 expected" } : @sridx +{ rot @bigsridx tuck < -rot tuck > rot or abort"stack register out of range" } : @sridxrange +{ swap @bigsridx + dup 16 >= over 0< or abort"stack register out of range" } : @sridx+ +{ swap 0xcc <> over 7 > or over 6 = or abort"not a control register c0..c5 or c7" } : @cridx +{ = + { tuck 16 >= + { = and + { 15 and abort"integer too large" 8 + 2dup fits } until + > 2- 5 u, -rot i, + } cond + } cond + } cond + @addopb } dup : PUSHINT : INT +{ dup 256 = abort"use PUSHNAN instead of 256 PUSHPOW2" = or abort"invalid slice padding" + swap 1 1 u, 0 rot u, } : @scomplete +{ tuck sbitrefs swap 26 + swap @havebitrefs not + { PUSHREFSLICE } + { over sbitrefs 2dup 123 0 2x<= + { drop tuck 4 + 3 >> swap x{8B} s, over 4 u, 3 roll s, + -rot 3 << 4 + swap - @scomplete } + { 2dup 1 >= swap 248 <= and + { rot x{8C} s, swap 1- 2 u, over 7 + 3 >> tuck 5 u, 3 roll s, + -rot 3 << 1 + swap - @scomplete } + { rot x{8D} s, swap 3 u, over 2 + 3 >> tuck 7 u, 3 roll s, + -rot 3 << 6 + swap - @scomplete + } cond + } cond + } cond +} dup : PUSHSLICE : SLICE +// ( b' -- ? ) +{ bbitrefs or 0= } : @cont-empty? +{ bbits 7 and 0= } : @cont-aligned? +// ( b b' -- ? ) +{ bbitrefs over 7 and { 2drop drop false } { + swap 16 + swap @havebitrefs nip + } cond +} : @cont-fits? +// ( b b' -- ? ) +{ bbitrefs over 7 and { 2drop drop false } { + 32 1 pair+ @havebitrefs nip + } cond +} : @cont-ref-fit? +// ( b b' b'' -- ? ) +{ over @cont-aligned? over @cont-aligned? and not { 2drop drop false } { + bbitrefs rot bbitrefs pair+ swap 32 + swap @havebitrefs nip + } cond +} : @two-cont-fit? +{ 2dup @cont-fits? not + { b> PUSHREFCONT } + { swap over bbitrefs 2dup 120 0 2x<= + { drop swap x{9} s, swap 3 >> 4 u, swap b+ } + { rot x{8F_} s, swap 2 u, swap 3 >> 7 u, swap b+ } cond + } cond +} dup : PUSHCONT : CONT +{ }> PUSHCONT } : }>CONT +{ { @normal? PUSHCONT } @doafter<{ } : CONT:<{ + +// arithmetic operations +{ 2 { rot dup 8 fits + { nip = { rot drop -rot PUSHINT swap LSHIFT# } { + { drop PUSHINT } { + not pow2decomp swap -1 = { nip PUSHPOW2DEC } { + drop PUSHINT + } cond } cond } cond } cond } cond } cond +} dup : PUSHINTX : INTX + +// integer comparison +x{B8} @Defop SGN +x{B9} @Defop LESS +x{BA} @Defop EQUAL +x{BB} @Defop LEQ +x{BC} @Defop GREATER +x{BD} @Defop NEQ +x{BE} @Defop GEQ +x{BF} @Defop CMP +x{C0} x{BA} @Defop(8i,alt) EQINT +x{C000} @Defop ISZERO +x{C1} x{B9} @Defop(8i,alt) LESSINT +{ 1+ LESSINT } : LEQINT +x{C100} @Defop ISNEG +x{C101} @Defop ISNPOS +x{C2} x{BC} @Defop(8i,alt) GTINT +{ 1- GTINT } : GEQINT +x{C200} @Defop ISPOS +x{C2FF} @Defop ISNNEG +x{C3} x{BD} @Defop(8i,alt) NEQINT +x{C300} @Defop ISNZERO +x{C4} @Defop ISNAN +x{C5} @Defop CHKNAN + +// other comparison +x{C700} @Defop SEMPTY +x{C701} @Defop SDEMPTY +x{C702} @Defop SREMPTY +x{C703} @Defop SDFIRST +x{C704} @Defop SDLEXCMP +x{C705} @Defop SDEQ +x{C708} @Defop SDPFX +x{C709} @Defop SDPFXREV +x{C70A} @Defop SDPPFX +x{C70B} @Defop SDPPFXREV +x{C70C} @Defop SDSFX +x{C70D} @Defop SDSFXREV +x{C70E} @Defop SDPSFX +x{C70F} @Defop SDPSFXREV +x{C710} @Defop SDCNTLEAD0 +x{C711} @Defop SDCNTLEAD1 +x{C712} @Defop SDCNTTRAIL0 +x{C713} @Defop SDCNTTRAIL1 + +// cell serialization (Builder manipulation primitives) +x{C8} @Defop NEWC +x{C9} @Defop ENDC +x{CA} @Defop(8u+1) STI +x{CB} @Defop(8u+1) STU +x{CC} @Defop STREF +x{CD} dup @Defop STBREFR @Defop ENDCST +x{CE} @Defop STSLICE +x{CF00} @Defop STIX +x{CF01} @Defop STUX +x{CF02} @Defop STIXR +x{CF03} @Defop STUXR +x{CF04} @Defop STIXQ +x{CF05} @Defop STUXQ +x{CF06} @Defop STIXRQ +x{CF07} @Defop STUXRQ +x{CF08} @Defop(8u+1) STI_l +x{CF09} @Defop(8u+1) STU_l +x{CF0A} @Defop(8u+1) STIR +x{CF0B} @Defop(8u+1) STUR +x{CF0C} @Defop(8u+1) STIQ +x{CF0D} @Defop(8u+1) STUQ +x{CF0E} @Defop(8u+1) STIRQ +x{CF0F} @Defop(8u+1) STURQ +x{CF10} @Defop STREF_l +x{CF11} @Defop STBREF +x{CF12} @Defop STSLICE_l +x{CF13} @Defop STB +x{CF14} @Defop STREFR +x{CF15} @Defop STBREFR_l +x{CF16} @Defop STSLICER +x{CF17} dup @Defop STBR @Defop BCONCAT +x{CF18} @Defop STREFQ +x{CF19} @Defop STBREFQ +x{CF1A} @Defop STSLICEQ +x{CF1B} @Defop STBQ +x{CF1C} @Defop STREFRQ +x{CF1D} @Defop STBREFRQ +x{CF1E} @Defop STSLICERQ +x{CF1F} dup @Defop STBRQ @Defop BCONCATQ +x{CF20} @Defop(ref) STREFCONST +{ > tuck 3 u, 3 roll s, + -rot 3 << 2 + swap - @scomplete } + { 2drop swap PUSHSLICE STSLICER } cond + } cond +} : STSLICECONST +x{CF81} @Defop STZERO +x{CF83} @Defop STONE + +// cell deserialization (CellSlice primitives) +x{D0} @Defop CTOS +x{D1} @Defop ENDS +x{D2} @Defop(8u+1) LDI +x{D3} @Defop(8u+1) LDU +x{D4} @Defop LDREF +x{D5} @Defop LDREFRTOS +x{D6} @Defop(8u+1) LDSLICE +x{D700} @Defop LDIX +x{D701} @Defop LDUX +x{D702} @Defop PLDIX +x{D703} @Defop PLDUX +x{D704} @Defop LDIXQ +x{D705} @Defop LDUXQ +x{D706} @Defop PLDIXQ +x{D707} @Defop PLDUXQ +x{D708} @Defop(8u+1) LDI_l +x{D709} @Defop(8u+1) LDU_l +x{D70A} @Defop(8u+1) PLDI +x{D70B} @Defop(8u+1) PLDU +x{D70C} @Defop(8u+1) LDIQ +x{D70D} @Defop(8u+1) LDUQ +x{D70E} @Defop(8u+1) PLDIQ +x{D70F} @Defop(8u+1) PLDUQ +{ dup 31 and abort"argument must be a multiple of 32" 5 >> 1- + > swap x{D72A_} s, over 7 u, 3 roll s, + -rot 3 << 3 + swap - @scomplete } : SDBEGINS:imm +{ tuck sbitrefs abort"no references allowed in slice" dup 26 <= + { drop > swap x{D72E_} s, over 7 u, 3 roll s, + -rot 3 << 3 + swap - @scomplete } : SDBEGINSQ:imm +{ tuck sbitrefs abort"no references allowed in slice" dup 26 <= + { drop rot 2 } { + swap @| swap 2dup @cont-fits? { rot 1 } { + b> rot 2 + } cond } cond } cond } cond + [] execute +} : @run-cont-op +{ triple 1 ' @run-cont-op does create } : @def-cont-op +{ DROP } { PUSHCONT IF } { IFREF } @def-cont-op IF-cont +{ IFRET } { PUSHCONT IFJMP } { IFJMPREF } @def-cont-op IFJMP-cont +{ DROP } { PUSHCONT IFNOT } { IFNOTREF } @def-cont-op IFNOT-cont +{ IFNOTRET } { PUSHCONT IFNOTJMP } { IFNOTJMPREF } @def-cont-op IFNOTJMP-cont +{ dup 2over rot } : 3dup + +recursive IFELSE-cont2 { + dup @cont-empty? { drop IF-cont } { + over @cont-empty? { nip IFNOT-cont } { + 3dup @two-cont-fit? { -rot PUSHCONT swap PUSHCONT IFELSE } { + 3dup nip @cont-ref-fit? { rot swap PUSHCONT swap b> IFREFELSE } { + 3dup drop @cont-ref-fit? { -rot PUSHCONT swap b> IFELSEREF } { + rot 32 2 @havebitrefs { rot b> rot b> IFREFELSEREF } { + @| -rot IFELSE-cont2 + } cond } cond } cond } cond } cond } cond +} swap ! + +{ }> IF-cont } : }>IF +{ }> IFNOT-cont } : }>IFNOT +{ }> IFJMP-cont } : }>IFJMP +{ }> IFNOTJMP-cont } : }>IFNOTJMP +{ { @normal? IFJMP-cont } @doafter<{ } : IFJMP:<{ +{ { @normal? IFNOTJMP-cont } @doafter<{ } : IFNOTJMP:<{ +{ `else @endblk } : }>ELSE<{ +{ `else: @endblk } : }>ELSE: +{ 1 { swap @normal? swap IFELSE-cont2 } does @doafter<{ } : @doifelse +{ 1 { swap @normal? IFELSE-cont2 } does @doafter<{ } : @doifnotelse +{ + { dup `else eq? + { drop @doifelse } + { dup `else: eq? + { drop IFJMP-cont } + { @normal? IF-cont + } cond + } cond + } @doafter<{ +} : IF:<{ +{ + { dup `else eq? + { drop @doifnotelse } + { dup `else: eq? + { drop IFNOTJMP-cont } + { @normal? IFNOT-cont + } cond + } cond + } @doafter<{ +} : IFNOT:<{ + +x{E304} @Defop CONDSEL +x{E305} @Defop CONDSELCHK +x{E308} @Defop IFRETALT +x{E309} @Defop IFNOTRETALT +{ DO<{ +{ `do: @endblk } : }>DO: +{ }> PUSHCONT REPEAT } : }>REPEAT +{ { @normal? PUSHCONT REPEAT } @doafter<{ } : REPEAT:<{ +{ }> PUSHCONT UNTIL } : }>UNTIL +{ { @normal? PUSHCONT UNTIL } @doafter<{ } : UNTIL:<{ +{ PUSHCONT { @normal? PUSHCONT WHILE } @doafter<{ } : @dowhile +{ + { dup `do eq? + { drop @dowhile } + { `do: eq? not abort"`}>DO<{` expected" PUSHCONT WHILEEND + } cond + } @doafter<{ +} : WHILE:<{ +{ }> PUSHCONT AGAIN } : }>AGAIN +{ { @normal? PUSHCONT AGAIN } @doafter<{ } : AGAIN:<{ + +x{E314} @Defop REPEATBRK +x{E315} @Defop REPEATENDBRK +x{E316} @Defop UNTILBRK +x{E317} dup @Defop UNTILENDBRK @Defop UNTILBRK: +x{E318} @Defop WHILEBRK +x{E319} @Defop WHILEENDBRK +x{E31A} @Defop AGAINBRK +x{E31B} dup @Defop AGAINENDBRK @Defop AGAINBRK: + +{ }> PUSHCONT REPEATBRK } : }>REPEATBRK +{ { @normal? PUSHCONT REPEATBRK } @doafter<{ } : REPEATBRK:<{ +{ }> PUSHCONT UNTILBRK } : }>UNTILBRK +{ { @normal? PUSHCONT UNTILBRK } @doafter<{ } : UNTILBRK:<{ +{ PUSHCONT { @normal? PUSHCONT WHILEBRK } @doafter<{ } : @dowhile +{ + { dup `do eq? + { drop @dowhile } + { `do: eq? not abort"`}>DO<{` expected" PUSHCONT WHILEENDBRK + } cond + } @doafter<{ +} : WHILEBRK:<{ +{ }> PUSHCONT AGAINBRK } : }>AGAINBRK +{ { @normal? PUSHCONT AGAINBRK } @doafter<{ } : AGAINBRK:<{ + + +// +// continuation stack manipulation and continuation creation +// +{ PUSHCONT ATEXIT } : }>ATEXIT +{ { @normal? PUSHCONT ATEXIT } @doafter<{ } : ATEXIT:<{ +x{EDF4} @Defop ATEXITALT +{ }> PUSHCONT ATEXITALT } : }>ATEXITALT +{ { @normal? PUSHCONT ATEXITALT } @doafter<{ } : ATEXITALT:<{ +x{EDF5} @Defop SETEXITALT +{ }> PUSHCONT SETEXITALT } : }>SETEXITALT +{ { @normal? PUSHCONT SETEXITALT } @doafter<{ } : SETEXITALT:<{ +x{EDF6} @Defop THENRET +x{EDF7} @Defop THENRETALT +x{EDF8} @Defop INVERT +x{EDF9} @Defop BOOLEVAL +x{EDFA} @Defop SAMEALT +x{EDFB} @Defop SAMEALTSAVE +// x{EE} is BLESSARGS +// +// dictionary subroutine call/jump primitives +{ c3 PUSH EXECUTE } : CALLVAR +{ c3 PUSH JMPX } : JMPVAR +{ c3 PUSH } : PREPAREVAR +{ dup 14 ufits { + dup 8 ufits { + CATCH<{ +{ PUSHCONT { @normal? PUSHCONT TRY } @doafter<{ } : @trycatch +{ + { `catch eq? not abort"`}>CATCH<{` expected" @trycatch + } @doafter<{ +} : TRY:<{ +// +// dictionary manipulation +' NULL : NEWDICT +' ISNULL : DICTEMPTY +' STSLICE : STDICTS +x{F400} dup @Defop STDICT @Defop STOPTREF +x{F401} dup @Defop SKIPDICT @Defop SKIPOPTREF +x{F402} @Defop LDDICTS +x{F403} @Defop PLDDICTS +x{F404} dup @Defop LDDICT @Defop LDOPTREF +x{F405} dup @Defop PLDDICT @Defop PLDOPTREF +x{F406} @Defop LDDICTQ +x{F407} @Defop PLDDICTQ + +x{F40A} @Defop DICTGET +x{F40B} @Defop DICTGETREF +x{F40C} @Defop DICTIGET +x{F40D} @Defop DICTIGETREF +x{F40E} @Defop DICTUGET +x{F40F} @Defop DICTUGETREF + +x{F412} @Defop DICTSET +x{F413} @Defop DICTSETREF +x{F414} @Defop DICTISET +x{F415} @Defop DICTISETREF +x{F416} @Defop DICTUSET +x{F417} @Defop DICTUSETREF +x{F41A} @Defop DICTSETGET +x{F41B} @Defop DICTSETGETREF +x{F41C} @Defop DICTISETGET +x{F41D} @Defop DICTISETGETREF +x{F41E} @Defop DICTUSETGET +x{F41F} @Defop DICTUSETGETREF + +x{F422} @Defop DICTREPLACE +x{F423} @Defop DICTREPLACEREF +x{F424} @Defop DICTIREPLACE +x{F425} @Defop DICTIREPLACEREF +x{F426} @Defop DICTUREPLACE +x{F427} @Defop DICTUREPLACEREF +x{F42A} @Defop DICTREPLACEGET +x{F42B} @Defop DICTREPLACEGETREF +x{F42C} @Defop DICTIREPLACEGET +x{F42D} @Defop DICTIREPLACEGETREF +x{F42E} @Defop DICTUREPLACEGET +x{F42F} @Defop DICTUREPLACEGETREF + +x{F432} @Defop DICTADD +x{F433} @Defop DICTADDREF +x{F434} @Defop DICTIADD +x{F435} @Defop DICTIADDREF +x{F436} @Defop DICTUADD +x{F437} @Defop DICTUADDREF +x{F43A} @Defop DICTADDGET +x{F43B} @Defop DICTADDGETREF +x{F43C} @Defop DICTIADDGET +x{F43D} @Defop DICTIADDGETREF +x{F43E} @Defop DICTUADDGET +x{F43F} @Defop DICTUADDGETREF + +x{F441} @Defop DICTSETB +x{F442} @Defop DICTISETB +x{F443} @Defop DICTUSETB +x{F445} @Defop DICTSETGETB +x{F446} @Defop DICTISETGETB +x{F447} @Defop DICTUSETGETB + +x{F449} @Defop DICTREPLACEB +x{F44A} @Defop DICTIREPLACEB +x{F44B} @Defop DICTUREPLACEB +x{F44D} @Defop DICTREPLACEGETB +x{F44E} @Defop DICTIREPLACEGETB +x{F44F} @Defop DICTUREPLACEGETB + +x{F451} @Defop DICTADDB +x{F452} @Defop DICTIADDB +x{F453} @Defop DICTUADDB +x{F455} @Defop DICTADDGETB +x{F456} @Defop DICTIADDGETB +x{F457} @Defop DICTUADDGETB + +x{F459} @Defop DICTDEL +x{F45A} @Defop DICTIDEL +x{F45B} @Defop DICTUDEL + +x{F462} @Defop DICTDELGET +x{F463} @Defop DICTDELGETREF +x{F464} @Defop DICTIDELGET +x{F465} @Defop DICTIDELGETREF +x{F466} @Defop DICTUDELGET +x{F467} @Defop DICTUDELGETREF + +x{F469} @Defop DICTGETOPTREF +x{F46A} @Defop DICTIGETOPTREF +x{F46B} @Defop DICTUGETOPTREF +x{F46D} @Defop DICTSETGETOPTREF +x{F46E} @Defop DICTISETGETOPTREF +x{F46F} @Defop DICTUSETGETOPTREF + +x{F470} @Defop PFXDICTSET +x{F471} @Defop PFXDICTREPLACE +x{F472} @Defop PFXDICTADD +x{F473} @Defop PFXDICTDEL + +x{F474} @Defop DICTGETNEXT +x{F475} @Defop DICTGETNEXTEQ +x{F476} @Defop DICTGETPREV +x{F477} @Defop DICTGETPREVEQ +x{F478} @Defop DICTIGETNEXT +x{F479} @Defop DICTIGETNEXTEQ +x{F47A} @Defop DICTIGETPREV +x{F47B} @Defop DICTIGETPREVEQ +x{F47C} @Defop DICTUGETNEXT +x{F47D} @Defop DICTUGETNEXTEQ +x{F47E} @Defop DICTUGETPREV +x{F47F} @Defop DICTUGETPREVEQ + +x{F482} @Defop DICTMIN +x{F483} @Defop DICTMINREF +x{F484} @Defop DICTIMIN +x{F485} @Defop DICTIMINREF +x{F486} @Defop DICTUMIN +x{F487} @Defop DICTUMINREF +x{F48A} @Defop DICTMAX +x{F48B} @Defop DICTMAXREF +x{F48C} @Defop DICTIMAX +x{F48D} @Defop DICTIMAXREF +x{F48E} @Defop DICTUMAX +x{F48F} @Defop DICTUMAXREF + +x{F492} @Defop DICTREMMIN +x{F493} @Defop DICTREMMINREF +x{F494} @Defop DICTIREMMIN +x{F495} @Defop DICTIREMMINREF +x{F496} @Defop DICTUREMMIN +x{F497} @Defop DICTUREMMINREF +x{F49A} @Defop DICTREMMAX +x{F49B} @Defop DICTREMMAXREF +x{F49C} @Defop DICTIREMMAX +x{F49D} @Defop DICTIREMMAXREF +x{F49E} @Defop DICTUREMMAX +x{F49F} @Defop DICTUREMMAXREF + +x{F4A0} @Defop DICTIGETJMP +x{F4A1} @Defop DICTUGETJMP +x{F4A2} @Defop DICTIGETEXEC +x{F4A3} @Defop DICTUGETEXEC +{ dup sbitrefs tuck 1 > swap 1 <> or abort"not a dictionary" swap 1 u@ over <> abort"not a dictionary" } : @chkdicts +{ dup null? tuck { idict! + not abort"cannot add key to procedure info dictionary" + @procinfo ! +} : @procinfo! +// ( x v1 v2 -- ) +{ not 2 pick @procinfo@ and xor swap @procinfo! } : @procinfo~! +// ( s i f -- ) +{ over @procdictkeylen fits not abort"procedure index out of range" + over swap dup @procinfo~! 2dup @proclistadd + 1 'nop does swap 0 (create) +} : @declproc +{ 1 'nop does swap 0 (create) } : @declglobvar +{ @proccnt @ 1+ dup @proccnt ! 1 @declproc } : @newproc +{ @gvarcnt @ 1+ dup @gvarcnt ! @declglobvar } : @newglobvar +variable @oldcurrent variable @oldctx +Fift-wordlist dup @oldcurrent ! @oldctx ! +{ current@ @oldcurrent ! context@ @oldctx ! Asm definitions + @proccnt @ @proclist @ @procdict @ @procinfo @ @gvarcnt @ @parent-state @ current@ @oldcurrent @ @oldctx @ + 9 tuple @parent-state ! + hole current! + 0 =: main @proclist null! @proccnt 0! @gvarcnt 0! + { bl word @newproc } : NEWPROC + { bl word dup (def?) ' drop ' @newproc cond } : DECLPROC + { bl word dup find + { nip execute <> abort"method redefined with different id" } + { swap 17 @declproc } + cond } : DECLMETHOD + { bl word @newglobvar } : DECLGLOBVAR + "main" 0 @proclistadd + dictnew dup @procdict ! + @procinfo ! 16 0 @procinfo! +} : PROGRAM{ +{ over sbits < { s>c + swap @addop + } { + drop + swap @procdictkeylen DICTPUSHCONST DICTIGETJMPZ 11 THROWARG + } cond +}> } : }END> +{ }END> b> } : }END>c +{ }END>c s + +0 constant recv_internal +-1 constant recv_external +-2 constant run_ticktock +-3 constant split_prepare +-4 constant split_install +-1111 constant entry_point +-1112 constant entry_point_recv + +{ asm-mode 0 3 ~! } : asm-no-remove-unused +{ asm-mode 1 1 ~! } : asm-remove-unused // enabled by default +{ asm-mode 3 3 ~! } : asm-warn-remove-unused +{ asm-mode 4 4 ~! } : asm-warn-inline-mix +{ asm-mode 0 4 ~! } : asm-no-warn-inline-mix // disabled by default +{ asm-mode 8 8 ~! } : asm-warn-unused +{ asm-mode 0 8 ~! } : asm-no-warn-unused // disabled by default + +// ( c -- ) add vm library for later use with runvmcode +{ spec } : hash>libref +// ( c -- c' ) +{ hash hash>libref } : >libref + +{ dup "." $pos dup -1 = + { drop 0 } + { $| 1 $| nip swap (number) 1- abort"invalid version" + dup dup 0 < swap 999 > or abort"invalid version" + } + cond +} : parse-version-level + +{ + 0 swap + "." $+ + { swap 1000 * swap parse-version-level rot + swap } 3 times + "" $= not abort"invalid version" +} : parse-asm-fif-version + +{ + dup =: required-version parse-asm-fif-version + asm-fif-version parse-asm-fif-version + = 1+ { + "Required Asm.fif version: " @' required-version "; actual Asm.fif version: " asm-fif-version $+ $+ $+ abort + } if +} : require-asm-fif-version + +{ + dup =: required-version parse-asm-fif-version + asm-fif-version parse-asm-fif-version + swap + >= 1+ { + "Required Asm.fif version: " @' required-version "; actual Asm.fif version: " asm-fif-version $+ $+ $+ abort + } if +} : require-asm-fif-version>= + + +Fift definitions Asm +' <{ : <{ +' PROGRAM{ : PROGRAM{ +' asm-fif-version : asm-fif-version +' require-asm-fif-version : require-asm-fif-version +' require-asm-fif-version>= : require-asm-fif-version>= +Fift diff --git a/fift/Disasm.fif b/fift/Disasm.fif new file mode 100644 index 00000000..26f8f4ad --- /dev/null +++ b/fift/Disasm.fif @@ -0,0 +1,141 @@ +library TVM_Disasm +// simple TVM Disassembler +"Lists.fif" include + +variable 'disasm +{ 'disasm @ execute } : disasm // disassemble a slice +// usage: x{74B0} disasm + +variable @dismode @dismode 0! +{ rot over @ and rot xor swap ! } : andxor! +{ -2 0 @dismode andxor! } : stack-disasm // output 's1 s4 XCHG' +{ -2 1 @dismode andxor! } : std-disasm // output 'XCHG s1, s4' +{ -3 2 @dismode andxor! } : show-vm-code +{ -3 0 @dismode andxor! } : hide-vm-code +{ @dismode @ 1 and 0= } : stack-disasm? + +variable @indent @indent 0! +{ ' space @indent @ 2* times } : .indent +{ @indent 1+! } : +indent +{ @indent 1-! } : -indent + +{ " " $pos } : spc-pos +{ dup " " $pos swap "," $pos dup 0< { drop } { + over 0< { nip } { min } cond } cond +} : spc-comma-pos +{ { dup spc-pos 0= } { 1 $| nip } while } : -leading +{ -leading -trailing dup spc-pos dup 0< { + drop dup $len { atom single } { drop nil } cond } { + $| swap atom swap -leading 2 { over spc-comma-pos dup 0>= } { + swap 1+ -rot $| 1 $| nip -leading rot + } while drop tuple + } cond +} : parse-op +{ dup "s-1" $= { drop "s(-1)" true } { + dup "s-2" $= { drop "s(-2)" true } { + dup 1 $| swap "x" $= { nip "x{" swap $+ +"}" true } { + 2drop false } cond } cond } cond +} : adj-op-arg +{ over count over <= { drop } { 2dup [] adj-op-arg { swap []= } { drop } cond } cond } : adj-arg[] +{ 1 adj-arg[] 2 adj-arg[] 3 adj-arg[] + dup first + dup `XCHG eq? { + drop dup count 2 = { tpop swap "s0" , swap , } if } { + dup `LSHIFT eq? { + drop dup count 2 = stack-disasm? and { second `LSHIFT# swap pair } if } { + dup `RSHIFT eq? { + drop dup count 2 = stack-disasm? and { second `RSHIFT# swap pair } if } { + drop + } cond } cond } cond +} : adjust-op + +variable @cp @cp 0! +variable @curop +variable @contX variable @contY variable @cdict + +{ atom>$ type } : .atom +{ dup first .atom dup count 1 > { space 0 over count 2- { 1+ 2dup [] type .", " } swap times 1+ [] type } { drop } cond } : std-show-op +{ 0 over count 1- { 1+ 2dup [] type space } swap times drop first .atom } : stk-show-op +{ @dismode @ 2 and { @curop @ csr. } if } : .curop? +{ .curop? .indent @dismode @ 1 and ' std-show-op ' stk-show-op cond cr +} : show-simple-op +{ dup 4 u@ 9 = { 8 u@+ swap 15 and 3 << s@ } { + dup 7 u@ 0x47 = { 7 u@+ nip 2 u@+ 7 u@+ -rot 3 << swap sr@ } { + dup 8 u@ 0x8A = { ref@ " cr } : show-cont-op +{ swap scont-swap ":<{" show-cont-bodyx scont-swap + "" show-cont-bodyx .indent ."}>" cr } : show-cont2-op + +{ @contX @ null? { "CONT" show-cont-op } ifnot +} : flush-contX +{ @contY @ null? { scont-swap "CONT" show-cont-op scont-swap } ifnot +} : flush-contY +{ flush-contY flush-contX } : flush-cont +{ @contX @ null? not } : have-cont? +{ @contY @ null? not } : have-cont2? +{ flush-contY @contY ! scont-swap } : save-cont-body + +{ @cdict ! } : save-const-dict +{ @cdict null! } : flush-dict +{ @cdict @ null? not } : have-dict? + +{ flush-cont .indent type .":<{" cr + @curop @ ref@ " cr +} : show-ref-op +{ flush-contY .indent rot type .":<{" cr + @curop @ ref@ " cr +} : show-cont-ref-op +{ flush-cont .indent swap type .":<{" cr + @curop @ ref@+ " cr +} : show-ref2-op + +{ flush-cont first atom>$ dup 5 $| drop "DICTI" $= swap + .indent type ." {" cr +indent @cdict @ @cdict null! unpair + rot { + swap .indent . ."=> <{" cr +indent disasm -indent .indent ."}>" cr true + } swap ' idictforeach ' dictforeach cond drop + -indent .indent ."}" cr +} : show-const-dict-op + +( `PUSHCONT `PUSHREFCONT ) constant @PushContL +( `REPEAT `UNTIL `IF `IFNOT `IFJMP `IFNOTJMP ) constant @CmdC1 +( `IFREF `IFNOTREF `IFJMPREF `IFNOTJMPREF `CALLREF `JMPREF ) constant @CmdR1 +( `DICTIGETJMP `DICTIGETJMPZ `DICTUGETJMP `DICTUGETJMPZ `DICTIGETEXEC `DICTUGETEXEC ) constant @JmpDictL +{ dup first `DICTPUSHCONST eq? { + flush-cont @curop @ get-const-dict save-const-dict show-simple-op } { + dup first @JmpDictL list-member? have-dict? and { + flush-cont show-const-dict-op } { + flush-dict + dup first @PushContL list-member? { + drop @curop @ get-cont-body save-cont-body } { + dup first @CmdC1 list-member? have-cont? and { + flush-contY first atom>$ .curop? show-cont-op } { + dup first @CmdR1 list-member? { + flush-cont first atom>$ dup $len 3 - $| drop .curop? show-ref-op } { + dup first `WHILE eq? have-cont2? and { + drop "WHILE" "}>DO<{" .curop? show-cont2-op } { + dup first `IFELSE eq? have-cont2? and { + drop "IF" "}>ELSE<{" .curop? show-cont2-op } { + dup first dup `IFREFELSE eq? swap `IFELSEREF eq? or have-cont? and { + first `IFREFELSE eq? "IF" "}>ELSE<{" rot .curop? show-cont-ref-op } { + dup first `IFREFELSEREF eq? { + drop "IF" "}>ELSE<{" .curop? show-ref2-op } { + flush-cont show-simple-op + } cond } cond } cond } cond } cond } cond } cond } cond } cond +} : show-op +{ dup @cp @ (vmoplen) dup 0> { 65536 /mod swap sr@+ swap dup @cp @ (vmopdump) parse-op swap s> true } { drop false } cond } : fetch-one-op +{ { fetch-one-op } { swap @curop ! adjust-op show-op } while } : disasm-slice +{ { disasm-slice dup sbitrefs 1- or 0= } { ref@ B 1 'nop } ::_ B{ +{ swap ({) over 2+ -roll swap (compile) (}) } : does +{ 1 'nop does create } : constant +{ 2 'nop does create } : 2constant +{ hole constant } : variable +10 constant ten +{ bl word 1 { find 0= abort"word not found" } } :: (') +{ bl word find not abort"-?" 0 swap } :: [compile] +{ bl word 1 { + dup find { " -?" $+ abort } ifnot nip execute +} } :: @' +{ bl word 1 { swap 1 'nop does swap 0 (create) } +} :: =: +{ bl word 1 { -rot 2 'nop does swap 0 (create) } +} :: 2=: +{ } : s>c +{ s>c hashB } : shash +// to be more efficiently re-implemented in C++ in the future +{ dup 0< ' negate if } : abs +{ 2dup > ' swap if } : minmax +{ minmax drop } : min +{ minmax nip } : max +"" constant <# +' $reverse : #> +{ swap 10 /mod char 0 + rot swap hold } : # +{ { # over 0<= } until } : #s +{ 0< { char - hold } if } : sign +// { dup abs <# #s rot sign #> nip } : (.) +// { (.) type } : ._ +// { ._ space } : . +{ dup 10 < { 48 } { 55 } cond + } : Digit +{ dup 10 < { 48 } { 87 } cond + } : digit +// x s b -- x' s' +{ rot swap /mod Digit rot swap hold } : B# +{ rot swap /mod digit rot swap hold } : b# +{ 16 B# } : X# +{ 16 b# } : x# +// x s b -- 0 s' +{ -rot { 2 pick B# over 0<= } until rot drop } : B#s +{ -rot { 2 pick b# over 0<= } until rot drop } : b#s +{ 16 B#s } : X#s +{ 16 b#s } : x#s +variable base +{ 10 base ! } : decimal +{ 16 base ! } : hex +{ 8 base ! } : octal +{ 2 base ! } : binary +{ base @ B# } : Base# +{ base @ b# } : base# +{ base @ B#s } : Base#s +{ base @ b#s } : base#s +// x w -- s +{ over abs <# rot 1- ' X# swap times X#s rot sign #> nip } : (0X.) +{ over abs <# rot 1- ' x# swap times x#s rot sign #> nip } : (0x.) +{ (0X.) type } : 0X._ +{ 0X._ space } : 0X. +{ (0x.) type } : 0x._ +{ 0x._ space } : 0x. +{ bl (-trailing) } : -trailing +{ char 0 (-trailing) } : -trailing0 +{ char " word 1 ' $+ } ::_ +" +{ find 0<> dup ' nip if } : (def?) +{ bl word 1 ' (def?) } :: def? +{ bl word 1 { (def?) not } } :: undef? +{ def? ' skip-to-eof if } : skip-ifdef +{ bl word dup (def?) { drop skip-to-eof } { 'nop swap 0 (create) } cond } : library +{ bl word dup (def?) { 2drop skip-to-eof } { swap 1 'nop does swap 0 (create) } cond } : library-version +{ hole dup 1 'nop does swap 1 { context! } does bl word tuck 0 (create) +"-wordlist" 0 (create) } : namespace +{ context@ current! } : definitions +{ char ) word "$" swap $+ 1 { find 0= abort"undefined parameter" execute } } ::_ $( +// b s -- ? +{ sbitrefs rot brembitrefs rot >= -rot <= and } : s-fits? +// b s x -- ? +{ swap sbitrefs -rot + rot brembitrefs -rot <= -rot <= and } : s-fits-with? +{ 0 swap ! } : 0! +{ tuck @ + swap ! } : +! +{ tuck @ swap - swap ! } : -! +{ 1 swap +! } : 1+! +{ -1 swap +! } : 1-! +{ null swap ! } : null! +{ not 2 pick @ and xor swap ! } : ~! +0 tuple constant nil +{ 1 tuple } : single +{ 2 tuple } : pair +{ 3 tuple } : triple +{ 1 untuple } : unsingle +{ 2 untuple } : unpair +{ 3 untuple } : untriple +{ over tuple? { swap count = } { 2drop false } cond } : tuple-len? +{ 0 tuple-len? } : nil? +{ 1 tuple-len? } : single? +{ 2 tuple-len? } : pair? +{ 3 tuple-len? } : triple? +{ 0 [] } : first +{ 1 [] } : second +{ 2 [] } : third +' pair : cons +' unpair : uncons +{ 0 [] } : car +{ 1 [] } : cdr +{ cdr car } : cadr +{ cdr cdr } : cddr +{ cdr cdr car } : caddr +{ null ' cons rot times } : list +{ -rot pair swap ! } : 2! +{ @ unpair } : 2@ +{ true (atom) drop } : atom +{ bl word atom 1 'nop } ::_ ` +{ hole dup 1 { @ execute } does create } : recursive +{ 0 { 1+ dup 1 ' $() does over (.) "$" swap $+ 0 (create) } rot times drop } : :$1..n +{ 10 hold } : +cr +{ 9 hold } : +tab +{ "" swap { 0 word 2dup $cmp } { rot swap $+ +cr swap } while 2drop } : scan-until-word +{ 0 word -trailing scan-until-word 1 'nop } ::_ $<< +{ 0x40 runvmx } : runvmcode +{ 0x48 runvmx } : gasrunvmcode +{ 0xc8 runvmx } : gas2runvmcode +{ 0x43 runvmx } : runvmdict +{ 0x4b runvmx } : gasrunvmdict +{ 0xcb runvmx } : gas2runvmdict +{ 0x45 runvmx } : runvm +{ 0x4d runvmx } : gasrunvm +{ 0xcd runvmx } : gas2runvm +{ 0x55 runvmx } : runvmctx +{ 0x5d runvmx } : gasrunvmctx +{ 0xdd runvmx } : gas2runvmctx +{ 0x75 runvmx } : runvmctxact +{ 0x7d runvmx } : gasrunvmctxact +{ 0xfd runvmx } : gas2runvmctxact +{ 0x35 runvmx } : runvmctxactq +{ 0x3d runvmx } : gasrunvmctxactq diff --git a/fift/Lists.fif b/fift/Lists.fif new file mode 100644 index 00000000..b59e40a0 --- /dev/null +++ b/fift/Lists.fif @@ -0,0 +1,220 @@ +library Lists // List utilities +// +{ hole dup 1 { @ execute } does create } : recursive +// x x' -- ? recursively compares two S-expressions +recursive equal? { + dup tuple? { + over tuple? { + over count over count over = { // t t' l ? + 0 { dup 0>= { 2dup [] 3 pick 2 pick [] equal? { 1+ } { drop -1 } cond + } if } rot times + nip nip 0>= + } { drop 2drop false } cond + } { 2drop false } cond + } { eqv? } cond +} swap ! +// (a1 .. an) -- (an .. a1) +{ null swap { dup null? not } { uncons swap rot cons swap } while drop } : list-reverse +// (a1 .. an) -- an Computes last element of non-empty list l +{ { uncons dup null? { drop true } { nip false } cond } until } : list-last +// l l' -- l++l' Concatenates two lists +recursive list+ { + over null? { nip } { swap uncons rot list+ cons } cond +} swap ! +// l l' -- l'' -1 or 0, where l = l' ++ l'' +// Removes prefix from list +{ { dup null? { drop true true } { + swap dup null? { 2drop false true } { // l' l + uncons swap rot uncons -rot equal? { false } { + 2drop false true + } cond } cond } cond } until +} : list- +// (a1 .. an) -- a1 .. an n Explodes a list +{ 0 { over null? not } { swap uncons rot 1+ } while nip } : explode-list +// (a1 .. an) x -- a1 .. an n x Explodes a list under the topmost element +{ swap explode-list dup 1+ roll } : explode-list-1 +// l -- t Transforms a list into a tuple with the same elements +{ explode-list tuple } : list>tuple +// a1 ... an n x -- (a1 .. an) x +{ null swap rot { -rot cons swap } swap times } : mklist-1 +// (s1 ... sn) -- s1+...+sn Concatenates a list of strings +{ "" { over null? not } { swap uncons -rot $+ } while nip +} : concat-string-list +// (x1 ... xn) -- x1+...+xn Sums a list of integers +{ 0 { over null? not } { swap uncons -rot + } while nip +} : sum-list +// (a1 ... an) a e -- e(...e(e(a,a1),a2),...),an) +{ -rot { over null? not } { swap uncons -rot 3 pick execute } while nip nip +} : foldl +// (a1 ... an) e -- e(...e(e(a1,a2),a3),...),an) +{ swap uncons swap rot foldl } : foldl-ne +// (a1 ... an) a e -- e(a1,e(a2,...,e(an,a)...)) +recursive foldr { + rot dup null? { 2drop } { + uncons -rot 2swap swap 3 pick foldr rot execute + } cond +} swap ! +// (a1 ... an) e -- e(a1,e(a2,...,e(a[n-1],an)...)) +recursive foldr-ne { + over cdr null? { drop car } { + swap uncons 2 pick foldr-ne rot execute + } cond +} swap ! +// (l1 ... ln) -- l1++...++ln Concatenates a list of lists +{ dup null? { ' list+ foldr-ne } ifnot } : concat-list-lists +// (a1 .. an . t) n -- t Computes the n-th tail of a list +{ ' cdr swap times } : list-tail +// (a0 .. an ..) n -- an Computes the n-th element of a list +{ list-tail car } : list-ref +// l -- ? +{ { dup null? { drop true true } { + dup pair? { cdr false } { + drop false true + } cond } cond } until +} : list? +// l -- n +{ 0 { over null? not } { 1+ swap uncons nip swap } while nip +} : list-length +// l e -- t // returns tail of l after first member that satisfies e +{ swap { + dup null? { nip true } { + tuck car over execute { drop true } { + swap cdr false + } cond } cond } until +} : list-tail-from +// a l -- t // tail of l after first occurence of a using eq? +{ swap 1 ' eq? does list-tail-from } : list-member-eq +{ swap 1 ' eqv? does list-tail-from } : list-member-eqv +{ swap 1 ' equal? does list-tail-from } : list-member-equal +// a l -- ? +{ list-member-eq null? not } : list-member? +{ list-member-eqv null? not } : list-member-eqv? +// l -- a -1 or 0 // returns car l if l is non-empty +{ dup null? { drop false } { car true } cond +} : safe-car +{ dup null? { drop false } { car second true } cond +} : get-first-value +// l e -- v -1 or 0 +{ list-tail-from safe-car } : assoc-gen +{ list-tail-from get-first-value } : assoc-gen-x +// a l -- (a.v) -1 or 0 -- returns first entry (a . v) in l +{ swap 1 { swap first eq? } does assoc-gen } : assq +{ swap 1 { swap first eqv? } does assoc-gen } : assv +{ swap 1 { swap first equal? } does assoc-gen } : assoc +// a l -- v -1 or 0 -- returns v from first entry (a . v) in l +{ swap 1 { swap first eq? } does assoc-gen-x } : assq-val +{ swap 1 { swap first eqv? } does assoc-gen-x } : assv-val +{ swap 1 { swap first equal? } does assoc-gen-x } : assoc-val +// (a1 .. an) e -- (e(a1) .. e(an)) +recursive list-map { + over null? { drop } { + swap uncons -rot over execute -rot list-map cons + } cond +} swap ! + +variable ctxdump variable curctx +// (a1 .. an) e -- executes e for a1, ..., an +{ ctxdump @ curctx @ ctxdump 2! curctx 2! + { curctx 2@ over null? not } { swap uncons rot tuck curctx 2! execute } + while 2drop ctxdump 2@ curctx ! ctxdump ! +} : list-foreach +forget ctxdump forget curctx + +// +// Experimental implementation of `for` loops with index +// +variable loopdump variable curloop +{ curloop @ loopdump @ loopdump 2! } : push-loop-ctx +{ loopdump 2@ loopdump ! curloop ! } : pop-loop-ctx +// ilast i0 e -- executes e for i=i0,i0+1,...,ilast-1 +{ -rot 2dup > { + push-loop-ctx { + triple dup curloop ! first execute curloop @ untriple 1+ 2dup <= + } until pop-loop-ctx + } if 2drop drop +} : for +// ilast i0 e -- same as 'for', but pushes current index i before executing e +{ -rot 2dup > { + push-loop-ctx { + triple dup curloop ! untriple nip swap execute curloop @ untriple 1+ 2dup <= + } until pop-loop-ctx + } if 2drop drop +} : for-i +// ( -- i ) Returns innermost loop index +{ curloop @ third } : i +// ( -- j ) Returns outer loop index +{ loopdump @ car third } : j +{ loopdump @ cadr third } : k +forget curloop forget loopdump + +// +// create Lisp-style lists using words "(" and ")" +// +variable ') +'nop box constant ', +{ ") without (" abort } ') ! +{ ') @ execute } : ) +anon constant dot-marker +// m x1 ... xn t m -- (x1 ... xn . t) +{ swap + { -rot 2dup eq? not } + { over dot-marker eq? abort"invalid dotted list" + swap rot cons } while 2drop +} : list-tail-until-marker +// m x1 ... xn m -- (x1 ... xn) +{ null swap list-tail-until-marker } : list-until-marker +{ over dot-marker eq? { nip 2dup eq? abort"invalid dotted list" } + { null swap } cond + list-tail-until-marker +} : list-until-marker-ext +{ ') @ ', @ } : ops-get +{ ', ! ') ! } : ops-set +{ anon dup ops-get 3 { ops-set list-until-marker-ext } does ') ! 'nop ', ! +} : ( +// test of Lisp-style lists +// ( 42 ( `+ 9 ( `* 3 4 ) ) "test" ) .l cr +// ( `eq? ( `* 3 4 ) 3 4 * ) .l cr +// `alpha ( `beta `gamma `delta ) cons .l cr +// { ( `eq? ( `* 3 5 pick ) 3 4 roll * ) } : 3*sample +// 17 3*sample .l cr + +// similar syntax _( x1 .. xn ) for tuples +{ 2 { 1+ 2dup pick eq? } until 3 - nip } : count-to-marker +{ count-to-marker tuple nip } : tuple-until-marker +{ anon dup ops-get 3 { ops-set tuple-until-marker } does ') ! 'nop ', ! } : _( +// test of tuples +// _( _( 2 "two" ) _( 3 "three" ) _( 4 "four" ) ) .dump cr + +// pseudo-Lisp tokenizer +"()[]'" 34 hold constant lisp-delims +{ lisp-delims 11 (word) } : lisp-token +{ null cons `quote swap cons } : do-quote +{ 1 { ', @ 2 { 2 { ', ! execute ', @ execute } does ', ! } + does ', ! } does +} : postpone-prefix +{ ', @ 1 { ', ! } does ', ! } : postpone-', +( `( ' ( pair + `) ' ) pair + `[ ' _( pair + `] ' ) pair + `' ' do-quote postpone-prefix pair + `. ' dot-marker postpone-prefix pair + `" { char " word } pair + `;; { 0 word drop postpone-', } pair +) constant lisp-token-dict +variable eol +{ eol @ eol 0! anon dup ') @ 'nop 3 + { ops-set list-until-marker-ext true eol ! } does ') ! rot ', ! + { lisp-token dup (number) dup { roll drop } { + drop atom dup lisp-token-dict assq { nip second execute } if + } cond + ', @ execute + eol @ + } until + -rot eol ! execute +} :_ List-generic( +{ 'nop 'nop List-generic( } :_ LIST( +// LIST((lambda (x) (+ x 1)) (* 3 4)) +// LIST('(+ 3 4)) +// LIST(2 3 "test" . 9) +// LIST((process '[plus 3 4])) diff --git a/fift/dasm.fif b/fift/dasm.fif new file mode 100644 index 00000000..e406553b --- /dev/null +++ b/fift/dasm.fif @@ -0,0 +1,16 @@ +#!/usr/bin/fift -s +"Asm.fif" include +"Disasm.fif" include + +"../build/wallet_v5_compiled.txt" file>B B>$ x>B B>boc + +show-vm-code std-disasm +indent +indent + +."Disasm" cr +dup B B>x .dump cr cr + +."Free bits in root cell" +dup ../build/wallet_v5_compiled.txt + +fift dasm.fif diff --git a/scripts/print.sh b/scripts/print.sh new file mode 100755 index 00000000..70db9540 --- /dev/null +++ b/scripts/print.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +cd "$(dirname "$0")" || exit +cd ../fift || exit + +func -SP ../contracts/wallet_v5.fc > ../build/wallet_v5_code.fif + +fift print.fif diff --git a/scripts/scalpel.sh b/scripts/scalpel.sh new file mode 100755 index 00000000..52a29707 --- /dev/null +++ b/scripts/scalpel.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +cd "$(dirname "$0")" || exit +cd .. + +RED="\e[31;1m" +YELLOW="\e[33;1m" +GREEN="\e[32;1m" +ENDCOLOR="\e[0m" + +if [[ "$1" == "-r" ]]; then + echo -e "$RED* Cleaning up the instrument *$ENDCOLOR" + rm -f build/wallet_v5*.fif +fi + +if [ ! -f build/wallet_v5.fif ]; then + mkdir -p build + echo -e "$YELLOW* Creating comparation origin *$ENDCOLOR" + echo "Use scalpel.sh -r to clean the instrument after commiting" + func contracts/imports/stdlib.fc contracts/wallet_v5.fc > build/wallet_v5.fif + func -SR contracts/imports/stdlib.fc contracts/wallet_v5.fc >build/wallet_v5_x.fif 2>&1 +fi + +declare -A mlen +declare -A mlen_new + +func contracts/imports/stdlib.fc contracts/wallet_v5.fc > build/wallet_v5_vs.fif +func -SR contracts/imports/stdlib.fc contracts/wallet_v5.fc >build/wallet_v5_vs_x.fif 2>&1 + +KEY="" +CNT=0 +while IFS= read -r line +do + if [[ "$line" =~ ^"//" ]]; then continue; fi + if [[ "$line" =~ ^"DECLPROC" ]]; then continue; fi + if [[ "$line" =~ ^[0-9]+" DECLMETHOD" ]]; then continue; fi + if ! [[ "$line" =~ ^" " ]]; then + if [[ "$line" == "}>" ]]; then + mlen["$KEY"]="$CNT" + else + CNT=0 + KEY="$line" + fi + else + CNT=$((CNT+1)) + fi +done < build/wallet_v5.fif + +KEY="" +CNT=0 +while IFS= read -r line +do + if [[ "$line" =~ ^"//" ]]; then continue; fi + if [[ "$line" =~ ^"DECLPROC" ]]; then continue; fi + if [[ "$line" =~ ^[0-9]+" DECLMETHOD" ]]; then continue; fi + if ! [[ "$line" =~ ^" " ]]; then + if [[ "$line" == "}>" ]]; then + mlen_new["$KEY"]="$CNT" + else + CNT=0 + KEY="$line" + fi + else + CNT=$((CNT+1)) + fi +done < build/wallet_v5_vs.fif + +diff -C 5 build/wallet_v5.fif build/wallet_v5_vs.fif + +LINES=$(grep -v -e '^//' -e '^DECLPROC' -e '^[0-9]\+ DECLMETHOD' build/wallet_v5.fif | wc -l) +NLINES=$(grep -v -e '^//' -e '^DECLPROC' -e '^[0-9]\+ DECLMETHOD' build/wallet_v5_vs.fif | wc -l) +echo "" +echo -e "${YELLOW}Lines: $LINES -> $NLINES$ENDCOLOR" +for key in "${!mlen_new[@]}" +do + PFX="" + if [ "${mlen_new[$key]}" -gt "${mlen[$key]}" ]; then + PFX=$RED + fi + if [ "${mlen_new[$key]}" -lt "${mlen[$key]}" ]; then + PFX=$GREEN + fi + echo -e "$PFX${mlen[$key]} -> ${mlen_new[$key]} | $key$ENDCOLOR"; +done +echo "" diff --git a/tests/actions.ts b/tests/actions.ts index 28fe6295..6212e603 100644 --- a/tests/actions.ts +++ b/tests/actions.ts @@ -1,15 +1,5 @@ -import { - Address, - beginCell, - Cell, - CurrencyCollection, - MessageRelaxed, - SendMode, - storeMessageRelaxed, - storeCurrencyCollection -} from 'ton-core'; - -export type LibRef = Cell | bigint; +import { Address, beginCell, Cell, MessageRelaxed, SendMode, storeMessageRelaxed } from 'ton-core'; +import { isTestOnlyExtendedAction, TestOnlyExtendedAction, TestOnlyOutAction } from './test-only-actions'; export class ActionSendMsg { public static readonly tag = 0x0ec3c86d; @@ -21,69 +11,12 @@ export class ActionSendMsg { public serialize(): Cell { return beginCell() .storeUint(this.tag, 32) - .storeUint(this.mode, 8) + .storeUint(this.mode | SendMode.IGNORE_ERRORS, 8) .storeRef(beginCell().store(storeMessageRelaxed(this.outMsg)).endCell()) .endCell(); } } -export class ActionSetCode { - public static readonly tag = 0xad4de08e; - - public readonly tag = ActionSetCode.tag; - - constructor(public readonly newCode: Cell) {} - - public serialize(): Cell { - return beginCell().storeUint(this.tag, 32).storeRef(this.newCode).endCell(); - } -} - -export class ActionReserveCurrency { - public static readonly tag = 0x36e6b809; - - public readonly tag = ActionReserveCurrency.tag; - - constructor(public readonly mode: SendMode, public readonly currency: CurrencyCollection) {} - - public serialize(): Cell { - return beginCell() - .storeUint(this.tag, 32) - .storeUint(this.mode, 8) - .store(storeCurrencyCollection(this.currency)) - .endCell(); - } -} - -export class ActionChangeLibrary { - public static readonly tag = 0x26fa1dd4; - - public readonly tag = ActionChangeLibrary.tag; - - constructor(public readonly mode: number, public readonly libRef: LibRef) {} - - public serialize(): Cell { - const cell = beginCell().storeUint(this.tag, 32).storeUint(this.mode, 7); - if (typeof this.libRef === 'bigint') { - return cell.storeUint(0, 1).storeUint(this.libRef, 256).endCell(); - } - - return cell.storeUint(1, 1).storeRef(this.libRef).endCell(); - } -} - -export class ActionSetData { - public static readonly tag = 0x1ff8ea0b; - - public readonly tag = ActionSetData.tag; - - constructor(public readonly data: Cell) {} - - public serialize(): Cell { - return beginCell().storeUint(this.tag, 32).storeRef(this.data).endCell(); - } -} - export class ActionAddExtension { public static readonly tag = 0x1c40db9f; @@ -108,14 +41,34 @@ export class ActionRemoveExtension { } } -export type OutAction = ActionSendMsg | ActionSetCode | ActionReserveCurrency | ActionChangeLibrary; -export type ExtendedAction = ActionSetData | ActionAddExtension | ActionRemoveExtension; +export class ActionSetSignatureAuthAllowed { + public static readonly tag = 0x20cbb95a; + + public readonly tag = ActionSetSignatureAuthAllowed.tag; + + constructor(public readonly allowed: Boolean) {} + + public serialize(): Cell { + return beginCell() + .storeUint(this.tag, 32) + .storeUint(this.allowed ? 1 : 0, 1) + .endCell(); + } +} + +export type OutAction = ActionSendMsg | TestOnlyOutAction; +export type ExtendedAction = + | ActionAddExtension + | ActionRemoveExtension + | ActionSetSignatureAuthAllowed + | TestOnlyExtendedAction; export function isExtendedAction(action: OutAction | ExtendedAction): action is ExtendedAction { return ( - action.tag === ActionSetData.tag || action.tag === ActionAddExtension.tag || - action.tag === ActionRemoveExtension.tag + action.tag === ActionRemoveExtension.tag || + action.tag === ActionSetSignatureAuthAllowed.tag || + isTestOnlyExtendedAction(action) ); } diff --git a/tests/config.ts b/tests/config.ts new file mode 100644 index 00000000..fc688698 --- /dev/null +++ b/tests/config.ts @@ -0,0 +1,6 @@ +const config: any = { + // Inspect per-instruction execution in primary (contest) test cases + microscope: false +} + +export default config; diff --git a/tests/test-only-actions.ts b/tests/test-only-actions.ts new file mode 100644 index 00000000..75173517 --- /dev/null +++ b/tests/test-only-actions.ts @@ -0,0 +1,82 @@ +import { + Address, + beginCell, + Cell, + CurrencyCollection, + MessageRelaxed, + SendMode, + storeCurrencyCollection, + storeMessageRelaxed +} from 'ton-core'; +import { + ExtendedAction, + OutAction +} from './actions'; + +export type LibRef = Cell | bigint; + +export class ActionSetCode { + public static readonly tag = 0xad4de08e; + + public readonly tag = ActionSetCode.tag; + + constructor(public readonly newCode: Cell) {} + + public serialize(): Cell { + return beginCell().storeUint(this.tag, 32).storeRef(this.newCode).endCell(); + } +} + +export class ActionReserveCurrency { + public static readonly tag = 0x36e6b809; + + public readonly tag = ActionReserveCurrency.tag; + + constructor(public readonly mode: SendMode, public readonly currency: CurrencyCollection) {} + + public serialize(): Cell { + return beginCell() + .storeUint(this.tag, 32) + .storeUint(this.mode, 8) + .store(storeCurrencyCollection(this.currency)) + .endCell(); + } +} + +export class ActionChangeLibrary { + public static readonly tag = 0x26fa1dd4; + + public readonly tag = ActionChangeLibrary.tag; + + constructor(public readonly mode: number, public readonly libRef: LibRef) {} + + public serialize(): Cell { + const cell = beginCell().storeUint(this.tag, 32).storeUint(this.mode, 7); + if (typeof this.libRef === 'bigint') { + return cell.storeUint(0, 1).storeUint(this.libRef, 256).endCell(); + } + + return cell.storeUint(1, 1).storeRef(this.libRef).endCell(); + } +} + +export class ActionSetData { + public static readonly tag = 0x1ff8ea0b; + + public readonly tag = ActionSetData.tag; + + constructor(public readonly data: Cell) {} + + public serialize(): Cell { + return beginCell().storeUint(this.tag, 32).storeRef(this.data).endCell(); + } +} + +export type TestOnlyOutAction = ActionSetCode | ActionReserveCurrency | ActionChangeLibrary; +export type TestOnlyExtendedAction = ActionSetData; + +export function isTestOnlyExtendedAction(action: OutAction | ExtendedAction): action is ExtendedAction { + return ( + action.tag === ActionSetData.tag + ); +} \ No newline at end of file diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index f6df8339..4d2f30d2 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -1,6 +1,6 @@ -import { Blockchain, SandboxContract } from '@ton-community/sandbox'; +import {Blockchain, BlockchainTransaction, SandboxContract} from '@ton-community/sandbox'; import { Address, beginCell, Cell, Dictionary, Sender, SendMode, toNano } from 'ton-core'; -import { WalletId, WalletV5 } from '../wrappers/wallet-v5'; +import { Opcodes, WalletId, WalletV5 } from '../wrappers/wallet-v5'; import '@ton-community/test-utils'; import { compile } from '@ton-community/blueprint'; import { getSecureRandomBytes, KeyPair, keyPairFromSeed, sign } from 'ton-crypto'; @@ -8,12 +8,13 @@ import { bufferToBigInt, createMsgInternal, packAddress, validUntil } from './ut import { ActionAddExtension, ActionRemoveExtension, - ActionSendMsg, + ActionSendMsg, ActionSetSignatureAuthAllowed, packActionsList } from './actions'; import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDescription'; import { TransactionComputeVm } from 'ton-core/src/types/TransactionComputePhase'; import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; +import { default as config } from './config'; const WALLET_ID = new WalletId({ networkGlobalId: -239, workChain: 0, subwalletNumber: 0 }); @@ -30,8 +31,20 @@ describe('Wallet V5 extensions auth', () => { let sender: Sender; let seqno: number; + let ggc: bigint = BigInt(0); + function accountForGas(transactions: BlockchainTransaction[]) { + transactions.forEach((tx) => { + ggc += ((tx?.description as TransactionDescriptionGeneric)?.computePhase as TransactionComputeVm)?.gasUsed ?? BigInt(0); + }) + } + + afterAll(async() => { + console.log("EXTENSIONS TESTS: Total gas " + ggc); + }); + function createBody(actionsList: Cell) { const payload = beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -41,8 +54,8 @@ describe('Wallet V5 extensions auth', () => { const signature = sign(payload.hash(), keypair.secretKey); seqno++; return beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); } @@ -93,12 +106,20 @@ describe('Wallet V5 extensions auth', () => { const actions = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); + if (config.microscope) + blockchain.verbosity = { ...blockchain.verbosity, blockchainLogs: true, vmLogs: 'vm_logs_gas', debugLogs: true, print: true } + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { value: toNano('0.1'), body: actions }); + if (config.microscope) + blockchain.verbosity = { ...blockchain.verbosity, blockchainLogs: false, vmLogs: 'none', debugLogs: false, print: false } + expect(receipt.transactions.length).toEqual(3); + accountForGas(receipt.transactions); + expect(receipt.transactions).toHaveTransaction({ from: walletV5.address, to: testReceiver, @@ -152,6 +173,8 @@ describe('Wallet V5 extensions auth', () => { }); expect(receipt.transactions.length).toEqual(4); + accountForGas(receipt.transactions); + expect(receipt.transactions).toHaveTransaction({ from: walletV5.address, to: testReceiver1, @@ -202,6 +225,8 @@ describe('Wallet V5 extensions auth', () => { }); expect(receipt1.transactions.length).toEqual(2); + accountForGas(receipt1.transactions); + const extensionsDict1 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8), @@ -222,6 +247,8 @@ describe('Wallet V5 extensions auth', () => { }); expect(receipt2.transactions.length).toEqual(2); + accountForGas(receipt2.transactions); + const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8), @@ -247,6 +274,8 @@ describe('Wallet V5 extensions auth', () => { }); expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8), @@ -271,6 +300,8 @@ describe('Wallet V5 extensions auth', () => { }); expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, to: testReceiver, @@ -289,4 +320,199 @@ describe('Wallet V5 extensions auth', () => { const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; expect(receiverBalanceAfter).toEqual(receiverBalanceBefore); }); + + it('Disallow signature auth and do a transfer from extension', async () => { + await walletV5.sendInternalSignedMessage(sender, { + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!), + new ActionSetSignatureAuthAllowed(false) + ])) + }); + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + + const actions = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); + + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: actions + }); + + expect(receipt.transactions.length).toEqual(3); + accountForGas(receipt.transactions); + + expect(receipt.transactions).toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const fee = receipt.transactions[2].totalFees.coins; + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + }); + + it('Disallow signature auth; re-allow and self-delete by extension; do signed transfer', async () => { + await walletV5.sendInternalSignedMessage(sender, { + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!), + new ActionSetSignatureAuthAllowed(false) + ])) + }); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(0); + + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: packActionsList([ + new ActionRemoveExtension(sender.address!), + new ActionSetSignatureAuthAllowed(true) + ]) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed1 = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed1).toEqual(-1); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 2); + + // Allowing or disallowing signature auth increments seqno, need to re-read + seqno = contract_seqno; + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + + const actionsList2 = packActionsList([ + new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg) + ]); + + const receipt2 = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList2) + }); + + expect(receipt2.transactions.length).toEqual(3); + accountForGas(receipt2.transactions); + + expect(receipt2.transactions).toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const fee = receipt2.transactions[2].totalFees.coins; + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + }); + + it('Add ext; disallow signature auth by ext; re-allow and self-delete by extension; do signed transfer', async () => { + await walletV5.sendInternalSignedMessage(sender, { + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!) + ])) + }); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + + const receipt0 = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]) + }); + + expect(receipt0.transactions.length).toEqual(2); + accountForGas(receipt0.transactions); + + expect( + ( + (receipt0.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed0 = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed0).toEqual(0); + + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: packActionsList([ + new ActionRemoveExtension(sender.address!), + new ActionSetSignatureAuthAllowed(true) + ]) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed1 = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed1).toEqual(-1); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 2); + + // Allowing or disallowing signature auth increments seqno, need to re-read + seqno = contract_seqno; + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + + const actionsList2 = packActionsList([ + new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg) + ]); + + const receipt2 = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList2) + }); + + expect(receipt2.transactions.length).toEqual(3); + accountForGas(receipt2.transactions); + + expect(receipt2.transactions).toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const fee = receipt2.transactions[2].totalFees.coins; + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + }); }); diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 41d892ff..1dadade0 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -1,4 +1,4 @@ -import { Blockchain, SandboxContract } from '@ton-community/sandbox'; +import {Blockchain, BlockchainTransaction, SandboxContract} from '@ton-community/sandbox'; import { Address, beginCell, Cell, Dictionary, internal, Sender, SendMode, toNano } from 'ton-core'; import { Opcodes, WalletId, WalletV5 } from '../wrappers/wallet-v5'; import '@ton-community/test-utils'; @@ -14,15 +14,15 @@ import { import { ActionAddExtension, ActionRemoveExtension, - ActionSendMsg, - ActionSetCode, - ActionSetData, + ActionSendMsg, ActionSetSignatureAuthAllowed, packActionsList } from './actions'; -import { WalletV4 } from '../wrappers/wallet-v4'; import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDescription'; import { TransactionComputeVm } from 'ton-core/src/types/TransactionComputePhase'; import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; +import { default as config } from './config'; +import { ActionSetCode, ActionSetData } from './test-only-actions'; +import { WalletV4 } from '../wrappers/wallet-v4'; const WALLET_ID = new WalletId({ networkGlobalId: -239, workChain: -1, subwalletNumber: 0 }); @@ -39,6 +39,17 @@ describe('Wallet V5 sign auth external', () => { let sender: Sender; let seqno: number; + let ggc: bigint = BigInt(0); + function accountForGas(transactions: BlockchainTransaction[]) { + transactions.forEach((tx) => { + ggc += ((tx?.description as TransactionDescriptionGeneric)?.computePhase as TransactionComputeVm)?.gasUsed ?? BigInt(0); + }) + } + + afterAll(async() => { + console.log("EXTERNAL TESTS: Total gas " + ggc); + }); + async function deployOtherWallet( params?: Partial[0]> ) { @@ -65,6 +76,7 @@ describe('Wallet V5 sign auth external', () => { function createBody(actionsList: Cell) { const payload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -74,8 +86,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), keypair.secretKey); seqno++; return beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); } @@ -128,7 +140,7 @@ describe('Wallet V5 sign auth external', () => { const sendTxactionAction = beginCell() .storeUint(Opcodes.action_send_msg, 32) - .storeInt(SendMode.PAY_GAS_SEPARATELY, 8) + .storeInt(SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, 8) .storeRef(sendTxMsg) .endCell(); @@ -142,9 +154,16 @@ describe('Wallet V5 sign auth external', () => { ) .endCell(); + if (config.microscope) + blockchain.verbosity = { ...blockchain.verbosity, blockchainLogs: true, vmLogs: 'vm_logs_gas', debugLogs: true, print: true } + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + if (config.microscope) + blockchain.verbosity = { ...blockchain.verbosity, blockchainLogs: false, vmLogs: 'none', debugLogs: false, print: false } + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); expect(receipt.transactions).toHaveTransaction({ from: walletV5.address, @@ -182,6 +201,7 @@ describe('Wallet V5 sign auth external', () => { const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); expect(receipt.transactions.length).toEqual(1); + accountForGas(receipt.transactions); const extensions = await walletV5.getExtensions(); const extensionsDict = Dictionary.loadDirect( @@ -210,6 +230,7 @@ describe('Wallet V5 sign auth external', () => { const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); expect(receipt.transactions).toHaveTransaction({ from: walletV5.address, @@ -247,6 +268,7 @@ describe('Wallet V5 sign auth external', () => { const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); expect(receipt.transactions.length).toEqual(3); + accountForGas(receipt.transactions); expect(receipt.transactions).toHaveTransaction({ from: walletV5.address, @@ -290,6 +312,7 @@ describe('Wallet V5 sign auth external', () => { const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); expect(receipt.transactions).toHaveTransaction({ from: walletV5.address, @@ -308,6 +331,7 @@ describe('Wallet V5 sign auth external', () => { ); expect(extensionsDict.size).toEqual(2); + accountForGas(receipt.transactions); expect(extensionsDict.get(packAddress(testExtension1))).toEqual( BigInt(testExtension1.workChain) @@ -317,107 +341,11 @@ describe('Wallet V5 sign auth external', () => { ); }); - it('Set data and do two transfers', async () => { - const testReceiver1 = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const forwardValue1 = toNano(0.001); - - const testReceiver2 = Address.parse('EQCgYDKqfTh7zVj9BQwOIPs4SuOhM7wnIjb6bdtM2AJf_Z9G'); - const forwardValue2 = toNano(0.0012); - - const receiver1BalanceBefore = (await blockchain.getContract(testReceiver1)).balance; - const receiver2BalanceBefore = (await blockchain.getContract(testReceiver2)).balance; - - const msg1 = createMsgInternal({ dest: testReceiver1, value: forwardValue1 }); - const msg2 = createMsgInternal({ dest: testReceiver2, value: forwardValue2 }); - - const actionsList = packActionsList([ - new ActionSetData(beginCell().storeUint(239, 32).endCell()), - new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg1), - new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg2) - ]); - - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - - expect(receipt.transactions.length).toEqual(3); - - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver1, - value: forwardValue1 - }); - - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver2, - value: forwardValue2 - }); - - const fee1 = receipt.transactions[1].totalFees.coins; - const fee2 = receipt.transactions[2].totalFees.coins; - - const receiver1BalanceAfter = (await blockchain.getContract(testReceiver1)).balance; - const receiver2BalanceAfter = (await blockchain.getContract(testReceiver2)).balance; - expect(receiver1BalanceAfter).toEqual(receiver1BalanceBefore + forwardValue1 - fee1); - expect(receiver2BalanceAfter).toEqual(receiver2BalanceBefore + forwardValue2 - fee2); - - const storedSeqno = await walletV5.getSeqno(); - expect(storedSeqno).toEqual(239); - }); - - it('Send 255 transfers and do set data', async () => { - await ( - await blockchain.treasury('mass-messages') - ).send({ to: walletV5.address, value: toNano(100) }); - - const range = [...new Array(255)].map((_, index) => index); - - const receivers = range.map(i => Address.parseRaw('0:' + i.toString().padStart(64, '0'))); - const balancesBefore = ( - await Promise.all(receivers.map(r => blockchain.getContract(r))) - ).map(i => i.balance); - - const forwardValues = range.map(i => BigInt(toNano(0.000001 * i))); - - const msges = receivers.map((dest, i) => - createMsgInternal({ dest: dest, value: forwardValues[i] }) - ); - - const actionsList = packActionsList([ - new ActionSetData(beginCell().storeUint(239, 32).endCell()), - ...msges.map(msg => new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)) - ]); - - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - - expect(receipt.transactions.length).toEqual(range.length + 1); - - receivers.forEach((to, i) => { - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to, - value: forwardValues[i] - }); - }); - - const balancesAfter = ( - await Promise.all(receivers.map(r => blockchain.getContract(r))) - ).map(i => i.balance); - - const fees = receipt.transactions.slice(1).map(tx => tx.totalFees.coins); - - balancesAfter.forEach((balanceAfter, i) => { - expect(balanceAfter).toEqual(balancesBefore[i] + forwardValues[i] - fees[i]); - }); - - const storedSeqno = await walletV5.getSeqno(); - expect(storedSeqno).toEqual(239); - }); - it('Remove extension', async () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const actionsList1 = packActionsList([new ActionAddExtension(testExtension)]); - await walletV5.sendExternalSignedMessage(createBody(actionsList1)); + const receipt1 = await walletV5.sendExternalSignedMessage(createBody(actionsList1)); const extensionsDict1 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8), @@ -429,7 +357,7 @@ describe('Wallet V5 sign auth external', () => { ); const actionsList2 = packActionsList([new ActionRemoveExtension(testExtension)]); - await walletV5.sendExternalSignedMessage(createBody(actionsList2)); + const receipt2 = await walletV5.sendExternalSignedMessage(createBody(actionsList2)); const extensionsDict2 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8), @@ -438,81 +366,49 @@ describe('Wallet V5 sign auth external', () => { expect(extensionsDict2.size).toEqual(0); expect(extensionsDict2.get(packAddress(testExtension))).toEqual(undefined); + + accountForGas(receipt1.transactions); + accountForGas(receipt2.transactions); }); - it('Change code and data to wallet v4', async () => { - const code_v4 = await compile('wallet_v4'); - const data_v4 = beginCell() - .storeUint(0, 32) - .storeUint(0, 32) - .storeBuffer(keypair.publicKey, 32) - .storeDict(Dictionary.empty()) - .endCell(); + it('Should fail SetData action', async () => { + const cell = beginCell().endCell(); const actionsList = packActionsList([ - new ActionSetData(data_v4), - new ActionSetCode(code_v4) + new ActionSetData(cell) ]); - await walletV5.sendExternalSignedMessage(createBody(actionsList)); - - const walletV4 = blockchain.openContract(WalletV4.createFromAddress(walletV5.address)); - const seqno = await walletV4.getSeqno(); - const subwalletId = await walletV4.getSubWalletID(); - const publicKey = await walletV4.getPublicKey(); - const extensions = Dictionary.loadDirect( - Dictionary.Keys.Address(), - Dictionary.Values.BigInt(0), - await walletV4.getExtensions() - ); - - expect(seqno).toEqual(0); - expect(subwalletId).toEqual(0); - expect(publicKey).toEqual(bufferToBigInt(keypair.publicKey)); - expect(extensions.size).toEqual(0); - - const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const forwardValue = toNano(0.001); - - const sendTxMsg = beginCell() - .storeUint(0x10, 6) - .storeAddress(testReceiver) - .storeCoins(forwardValue) - .storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - .storeRef(beginCell().endCell()) - .endCell(); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - const mesagesCell = beginCell() - .storeUint(0, 8) - .storeUint(SendMode.PAY_GAS_SEPARATELY, 8) - .storeRef(sendTxMsg) - .endCell(); + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(41); + }); - const payload = beginCell() - .storeUint(0, 32) - .storeUint(validUntil(), 32) - .storeUint(0, 32) - .storeSlice(mesagesCell.beginParse()) - .endCell(); + it('Should fail SetCode action', async () => { + const cell = beginCell().endCell(); - const signature = sign(payload.hash(), keypair.secretKey); - const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) - .storeSlice(payload.beginParse()) - .endCell(); + const actionsList = packActionsList([ + new ActionSetCode(cell) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - const receipt = await walletV4.sendExternalSignedMessage(body); - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver, - value: forwardValue - }); + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(9); }); it('Should fail adding existing extension', async () => { const testExtension = Address.parseRaw('0:' + '0'.repeat(64)); const actionsList1 = packActionsList([new ActionAddExtension(testExtension)]); - await walletV5.sendExternalSignedMessage(createBody(actionsList1)); + const receipt1 = await walletV5.sendExternalSignedMessage(createBody(actionsList1)); + accountForGas(receipt1.transactions); const extensionsDict1 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8), @@ -584,6 +480,7 @@ describe('Wallet V5 sign auth external', () => { .endCell(); const fakePayload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(vu, 32) .storeUint(seqno + 1, 32) // seqno @@ -592,8 +489,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(fakePayload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => @@ -614,6 +511,7 @@ describe('Wallet V5 sign auth external', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -624,8 +522,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), fakeKeypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => @@ -646,6 +544,7 @@ describe('Wallet V5 sign auth external', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno + 1, 32) // seqno @@ -654,8 +553,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => @@ -676,6 +575,7 @@ describe('Wallet V5 sign auth external', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(Math.round(Date.now() / 1000) - 600, 32) .storeUint(seqno, 32) @@ -684,8 +584,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => @@ -706,6 +606,7 @@ describe('Wallet V5 sign auth external', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(new WalletId({ ...WALLET_ID, subwalletNumber: 1 }).serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) @@ -714,8 +615,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => @@ -735,7 +636,8 @@ describe('Wallet V5 sign auth external', () => { const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); - const payload = beginCell() + const payload = beginCell() // auth_signed_internal used instead of auth_signed + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) @@ -744,14 +646,14 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => expect( walletV5.sendExternal( - beginCell().storeUint(1111, 32).storeSlice(body.beginParse()).endCell() + beginCell().storeSlice(body.beginParse()).endCell() ) ).rejects.toThrow() ); @@ -788,4 +690,231 @@ describe('Wallet V5 sign auth external', () => { expect(walletBalanceBefore).toEqual(walletBalanceAfter); }); + + it('Should fail disallowing signature auth with no exts', async () => { + const actionsList = packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(42); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + }); + + it('Should fail allowing signature auth when allowed', async () => { + const actionsList = packActionsList([ + new ActionSetSignatureAuthAllowed(true) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(43); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + }); + + it('Should add ext and disallow signature auth', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + }); + + it('Should add ext and disallow signature auth in separate txs', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const extensionsDict = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + await walletV5.getExtensions() + ); + + expect(extensionsDict.size).toEqual(1); + + expect(extensionsDict.get(packAddress(testExtension))).toEqual( + BigInt(testExtension.workChain) + ); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + + const actionsList2 = packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]); + const receipt2 = await walletV5.sendExternalSignedMessage(createBody(actionsList2)); + accountForGas(receipt2.transactions); + + expect( + ( + (receipt2.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed2 = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed2).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + }); + + it('Should add ext, disallow sign, remove ext, allow sign in one tx; send in other', async () => { + // N.B. Test that zero extensions do not prevent re-allowing the signature authentication + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false), + new ActionRemoveExtension(testExtension), + new ActionSetSignatureAuthAllowed(true) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 2); + + // Allowing or disallowing signature auth increments seqno, need to re-read + seqno = contract_seqno; + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + + const actionsList2 = packActionsList([ + new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg) + ]); + + const receipt2 = await walletV5.sendExternalSignedMessage(createBody(actionsList2)); + + expect(receipt2.transactions.length).toEqual(2); + accountForGas(receipt2.transactions); + + expect(receipt2.transactions).toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const fee = receipt2.transactions[1].totalFees.coins; + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + }); + + it('Should fail disallowing signature auth twice in tx', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false), + new ActionSetSignatureAuthAllowed(false) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(43); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); // throw when handling, packet is dropped + }); + + it('Should add ext, disallow sig auth; fail different signed tx', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const extensionsDict = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + await walletV5.getExtensions() + ); + + expect(extensionsDict.size).toEqual(1); + + expect(extensionsDict.get(packAddress(testExtension))).toEqual( + BigInt(testExtension.workChain) + ); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + + await disableConsoleError(() => + expect(walletV5.sendExternalSignedMessage(createBody(packActionsList([])))).rejects.toThrow() + ); + }); }); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 7eea0262..b86f75e9 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -1,22 +1,21 @@ -import { Blockchain, SandboxContract } from '@ton-community/sandbox'; +import {Blockchain, BlockchainTransaction, SandboxContract} from '@ton-community/sandbox'; import { Address, beginCell, Cell, Dictionary, Sender, SendMode, toNano } from 'ton-core'; import { Opcodes, WalletId, WalletV5 } from '../wrappers/wallet-v5'; import '@ton-community/test-utils'; import { compile } from '@ton-community/blueprint'; import { getSecureRandomBytes, KeyPair, keyPairFromSeed, sign } from 'ton-crypto'; -import { bufferToBigInt, createMsgInternal, packAddress, validUntil } from './utils'; +import { bufferToBigInt, createMsgInternal, disableConsoleError, packAddress, validUntil } from './utils'; import { ActionAddExtension, ActionRemoveExtension, - ActionSendMsg, - ActionSetCode, - ActionSetData, + ActionSendMsg, ActionSetSignatureAuthAllowed, packActionsList } from './actions'; -import { WalletV4 } from '../wrappers/wallet-v4'; import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDescription'; import { TransactionComputeVm } from 'ton-core/src/types/TransactionComputePhase'; import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; +import { default as config } from './config'; +import { ActionSetCode, ActionSetData } from './test-only-actions'; const WALLET_ID = new WalletId({ networkGlobalId: -239, workChain: 0, subwalletNumber: 0 }); @@ -33,6 +32,17 @@ describe('Wallet V5 sign auth internal', () => { let sender: Sender; let seqno: number; + let ggc: bigint = BigInt(0); + function accountForGas(transactions: BlockchainTransaction[]) { + transactions.forEach((tx) => { + ggc += ((tx?.description as TransactionDescriptionGeneric)?.computePhase as TransactionComputeVm)?.gasUsed ?? BigInt(0); + }) + } + + afterAll(async() => { + console.log("INTERNAL TESTS: Total gas " + ggc); + }); + async function deployOtherWallet( params?: Partial[0]> ) { @@ -59,6 +69,7 @@ describe('Wallet V5 sign auth internal', () => { function createBody(actionsList: Cell) { const payload = beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -68,8 +79,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), keypair.secretKey); seqno++; return beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); } @@ -122,7 +133,7 @@ describe('Wallet V5 sign auth internal', () => { const sendTxactionAction = beginCell() .storeUint(Opcodes.action_send_msg, 32) - .storeInt(SendMode.PAY_GAS_SEPARATELY, 8) + .storeInt(SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, 8) .storeRef(sendTxMsg) .endCell(); @@ -136,12 +147,19 @@ describe('Wallet V5 sign auth internal', () => { ) .endCell(); + if (config.microscope) + blockchain.verbosity = { ...blockchain.verbosity, blockchainLogs: true, vmLogs: 'vm_logs_gas', debugLogs: true, print: true } + const receipt = await walletV5.sendInternalSignedMessage(sender, { value: toNano(0.1), body: createBody(actionsList) }); + if (config.microscope) + blockchain.verbosity = { ...blockchain.verbosity, blockchainLogs: false, vmLogs: 'none', debugLogs: false, print: false } + expect(receipt.transactions.length).toEqual(3); + accountForGas(receipt.transactions); expect(receipt.transactions).toHaveTransaction({ from: walletV5.address, @@ -182,6 +200,7 @@ describe('Wallet V5 sign auth internal', () => { }); expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); const extensions = await walletV5.getExtensions(); const extensionsDict = Dictionary.loadDirect( @@ -222,6 +241,7 @@ describe('Wallet V5 sign auth internal', () => { }); expect(receipt.transactions.length).toEqual(4); + accountForGas(receipt.transactions); expect(receipt.transactions).toHaveTransaction({ from: walletV5.address, @@ -268,6 +288,7 @@ describe('Wallet V5 sign auth internal', () => { }); expect(receipt.transactions.length).toEqual(3); + accountForGas(receipt.transactions); expect(receipt.transactions).toHaveTransaction({ from: walletV5.address, @@ -295,109 +316,11 @@ describe('Wallet V5 sign auth internal', () => { ); }); - it('Set data and do two transfers', async () => { - const testReceiver1 = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const forwardValue1 = toNano(0.001); - - const testReceiver2 = Address.parse('EQCgYDKqfTh7zVj9BQwOIPs4SuOhM7wnIjb6bdtM2AJf_Z9G'); - const forwardValue2 = toNano(0.0012); - - const receiver1BalanceBefore = (await blockchain.getContract(testReceiver1)).balance; - const receiver2BalanceBefore = (await blockchain.getContract(testReceiver2)).balance; - - const msg1 = createMsgInternal({ dest: testReceiver1, value: forwardValue1 }); - const msg2 = createMsgInternal({ dest: testReceiver2, value: forwardValue2 }); - - const actionsList = packActionsList([ - new ActionSetData(beginCell().storeUint(239, 32).endCell()), - new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg1), - new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg2) - ]); - - const receipt = await walletV5.sendInternalSignedMessage(sender, { - value: toNano(0.1), - body: createBody(actionsList) - }); - - expect(receipt.transactions.length).toEqual(4); - - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver1, - value: forwardValue1 - }); - - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver2, - value: forwardValue2 - }); - - const fee1 = receipt.transactions[2].totalFees.coins; - const fee2 = receipt.transactions[3].totalFees.coins; - - const receiver1BalanceAfter = (await blockchain.getContract(testReceiver1)).balance; - const receiver2BalanceAfter = (await blockchain.getContract(testReceiver2)).balance; - expect(receiver1BalanceAfter).toEqual(receiver1BalanceBefore + forwardValue1 - fee1); - expect(receiver2BalanceAfter).toEqual(receiver2BalanceBefore + forwardValue2 - fee2); - - const storedSeqno = await walletV5.getSeqno(); - expect(storedSeqno).toEqual(239); - }); - - it('Send 255 transfers and do set data', async () => { - const range = [...new Array(255)].map((_, index) => index); - - const receivers = range.map(i => Address.parseRaw('0:' + i.toString().padStart(64, '0'))); - const balancesBefore = ( - await Promise.all(receivers.map(r => blockchain.getContract(r))) - ).map(i => i.balance); - - const forwardValues = range.map(i => BigInt(toNano(0.0001 * i))); - - const msges = receivers.map((dest, i) => - createMsgInternal({ dest: dest, value: forwardValues[i] }) - ); - - const actionsList = packActionsList([ - new ActionSetData(beginCell().storeUint(239, 32).endCell()), - ...msges.map(msg => new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)) - ]); - - const receipt = await walletV5.sendInternalSignedMessage(sender, { - value: toNano(10), - body: createBody(actionsList) - }); - - expect(receipt.transactions.length).toEqual(range.length + 2); - - receivers.forEach((to, i) => { - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to, - value: forwardValues[i] - }); - }); - - const balancesAfter = ( - await Promise.all(receivers.map(r => blockchain.getContract(r))) - ).map(i => i.balance); - - const fees = receipt.transactions.slice(2).map(tx => tx.totalFees.coins); - - balancesAfter.forEach((balanceAfter, i) => { - expect(balanceAfter).toEqual(balancesBefore[i] + forwardValues[i] - fees[i]); - }); - - const storedSeqno = await walletV5.getSeqno(); - expect(storedSeqno).toEqual(239); - }); - it('Remove extension', async () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const actionsList1 = packActionsList([new ActionAddExtension(testExtension)]); - await walletV5.sendInternalSignedMessage(sender, { + const receipt1 = await walletV5.sendInternalSignedMessage(sender, { value: toNano(0.1), body: createBody(actionsList1) }); @@ -412,7 +335,7 @@ describe('Wallet V5 sign auth internal', () => { ); const actionsList2 = packActionsList([new ActionRemoveExtension(testExtension)]); - await walletV5.sendInternalSignedMessage(sender, { + const receipt2 = await walletV5.sendInternalSignedMessage(sender, { value: toNano(0.1), body: createBody(actionsList2) }); @@ -424,77 +347,47 @@ describe('Wallet V5 sign auth internal', () => { expect(extensionsDict2.size).toEqual(0); expect(extensionsDict2.get(packAddress(testExtension))).toEqual(undefined); + + accountForGas(receipt1.transactions); + accountForGas(receipt2.transactions); }); - it('Change code and data to wallet v4', async () => { - const code_v4 = await compile('wallet_v4'); - const data_v4 = beginCell() - .storeUint(0, 32) - .storeUint(0, 32) - .storeBuffer(keypair.publicKey, 32) - .storeDict(Dictionary.empty()) - .endCell(); + it('Should fail SetData action', async () => { + const cell = beginCell().endCell(); const actionsList = packActionsList([ - new ActionSetData(data_v4), - new ActionSetCode(code_v4) + new ActionSetData(cell) ]); - await walletV5.sendInternalSignedMessage(sender, { + const receipt = await walletV5.sendInternalSignedMessage(sender, { value: toNano(0.1), body: createBody(actionsList) }); - const walletV4 = blockchain.openContract(WalletV4.createFromAddress(walletV5.address)); - const seqno = await walletV4.getSeqno(); - const subwalletId = await walletV4.getSubWalletID(); - const publicKey = await walletV4.getPublicKey(); - const extensions = Dictionary.loadDirect( - Dictionary.Keys.Address(), - Dictionary.Values.BigInt(0), - await walletV4.getExtensions() - ); - - expect(seqno).toEqual(0); - expect(subwalletId).toEqual(0); - expect(publicKey).toEqual(bufferToBigInt(keypair.publicKey)); - expect(extensions.size).toEqual(0); - - const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const forwardValue = toNano(0.001); - - const sendTxMsg = beginCell() - .storeUint(0x10, 6) - .storeAddress(testReceiver) - .storeCoins(forwardValue) - .storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - .storeRef(beginCell().endCell()) - .endCell(); - - const mesagesCell = beginCell() - .storeUint(0, 8) - .storeUint(SendMode.PAY_GAS_SEPARATELY, 8) - .storeRef(sendTxMsg) - .endCell(); - - const payload = beginCell() - .storeUint(0, 32) - .storeUint(validUntil(), 32) - .storeUint(0, 32) - .storeSlice(mesagesCell.beginParse()) - .endCell(); + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(41); + }); - const signature = sign(payload.hash(), keypair.secretKey); - const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) - .storeSlice(payload.beginParse()) - .endCell(); + it('Should fail SetCode action', async () => { + const cell = beginCell().endCell(); - const receipt = await walletV4.sendExternalSignedMessage(body); - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver, - value: forwardValue + const actionsList = packActionsList([ + new ActionSetCode(cell) + ]); + const receipt = await walletV5.sendInternalSignedMessage(sender, { + value: toNano(0.1), + body: createBody(actionsList) }); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(9); }); it('Should fail adding existing extension', async () => { @@ -575,6 +468,7 @@ describe('Wallet V5 sign auth internal', () => { const vu = validUntil(); const payload = beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(vu, 32) .storeUint(seqno, 32) // seqno @@ -590,8 +484,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(fakePayload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternalSignedMessage(sender, { @@ -604,7 +498,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(35); + ).toEqual(0); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -626,6 +520,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -636,21 +531,28 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), fakeKeypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternalSignedMessage(sender, { value: toNano(0.1), body }); + console.debug( + 'SINGLE WRONG SIGNATURE INTERNAL TRANSFER GAS USED:', + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).gasUsed + ); expect( ( (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(35); + ).toEqual(0); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -672,6 +574,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno + 1, 32) // seqno @@ -680,8 +583,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternalSignedMessage(sender, { @@ -694,7 +597,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(33); + ).toEqual(0); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -716,6 +619,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(Math.round(Date.now() / 1000) - 600, 32) .storeUint(seqno, 32) @@ -724,8 +628,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternalSignedMessage(sender, { @@ -738,7 +642,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(36); + ).toEqual(0); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -760,6 +664,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(new WalletId({ ...WALLET_ID, subwalletNumber: 1 }).serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) @@ -768,8 +673,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternalSignedMessage(sender, { @@ -782,7 +687,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(34); + ).toEqual(0); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -803,7 +708,8 @@ describe('Wallet V5 sign auth internal', () => { const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); - const payload = beginCell() + const payload = beginCell() // auth_signed used instead of auth_signed_internal + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) @@ -812,14 +718,14 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternal(sender, { sendMode: SendMode.PAY_GAS_SEPARATELY, value: toNano(0.1), - body: beginCell().storeUint(1111, 32).storeSlice(body.beginParse()).endCell() + body: beginCell().storeSlice(body.beginParse()).endCell() }); expect(receipt.transactions.length).toEqual(2); @@ -859,6 +765,43 @@ describe('Wallet V5 sign auth internal', () => { ).toEqual(0); }); + it('Should not revert on short "sint" messages', async () => { + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: beginCell().storeUint(Opcodes.auth_signed_internal, 32).endCell() + }); + + expect(receipt.transactions.length).toEqual(2); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + }); + + it('Should not revert on long incorrect "sint" messages', async () => { + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) + .storeUint(0, 657) + .endCell() + }); + + expect(receipt.transactions.length).toEqual(2); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + }); + it('Should skip message with simple text comment', async () => { const receipt = await walletV5.sendInternal(sender, { sendMode: SendMode.PAY_GAS_SEPARATELY, @@ -874,5 +817,365 @@ describe('Wallet V5 sign auth internal', () => { .computePhase as TransactionComputeVm ).exitCode ).toEqual(0); + + console.debug( + 'SINGLE SIMPLE INTERNAL TRANSFER GAS USED:', + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).gasUsed + ); + }); + + it('Should skip message with longer text comment', async () => { + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: beginCell().storeUint(0, 32).storeStringTail('Hello world'.repeat(20)).endCell() + }); + + expect(receipt.transactions.length).toEqual(2); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + console.debug( + 'SINGLE LONGER SIMPLE INTERNAL TRANSFER GAS USED:', + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).gasUsed + ); + }); + + it('Should fail disallowing signature auth with no exts', async () => { + const actionsList = packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(42); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + }); + + it('Should fail allowing signature auth when allowed', async () => { + const actionsList = packActionsList([ + new ActionSetSignatureAuthAllowed(true) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(43); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + }); + + it('Should add ext and disallow signature auth', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + + accountForGas(receipt.transactions); + + const extensionsDict = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + await walletV5.getExtensions() + ); + + expect(extensionsDict.size).toEqual(1); + + expect(extensionsDict.get(packAddress(testExtension))).toEqual( + BigInt(testExtension.workChain) + ); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + }); + + it('Should add ext and disallow signature auth in separate txs', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + + accountForGas(receipt.transactions); + + const extensionsDict = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + await walletV5.getExtensions() + ); + + expect(extensionsDict.size).toEqual(1); + + expect(extensionsDict.get(packAddress(testExtension))).toEqual( + BigInt(testExtension.workChain) + ); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + + const actionsList2 = packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]); + + const receipt2 = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList2) + }); + + expect(receipt2.transactions.length).toEqual(2); + + accountForGas(receipt2.transactions); + + expect( + ( + (receipt2.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed2 = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed2).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + }); + + it('Should add ext, disallow sign, remove ext, allow sign in one tx; send in other', async () => { + // N.B. Test that zero extensions do not prevent re-allowing the signature authentication + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false), + new ActionRemoveExtension(testExtension), + new ActionSetSignatureAuthAllowed(true) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 2); + + // Allowing or disallowing signature auth increments seqno, need to re-read + seqno = contract_seqno; + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + + const actionsList2 = packActionsList([ + new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg) + ]); + + const receipt2 = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList2) + }); + + expect(receipt2.transactions.length).toEqual(3); + accountForGas(receipt2.transactions); + + expect(receipt2.transactions).toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const fee = receipt2.transactions[2].totalFees.coins; + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + }); + + it('Should fail disallowing signature auth twice in tx', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false), + new ActionSetSignatureAuthAllowed(false) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(43); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); // throw when handling, packet is dropped + }); + + it('Should add ext, disallow sig auth; fail different signed tx', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + const extensionsDict = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + await walletV5.getExtensions() + ); + + expect(extensionsDict.size).toEqual(1); + + expect(extensionsDict.get(packAddress(testExtension))).toEqual( + BigInt(testExtension.workChain) + ); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + const actionsList2 = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); + + const receipt2 = await walletV5.sendInternalSignedMessage(sender, { + value: toNano(0.1), + body: createBody(actionsList2) + }); + + expect( + ( + (receipt2.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + expect(receipt2.transactions).not.toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore); }); }); diff --git a/types.tlb b/types.tlb index 13460898..0ac3940a 100644 --- a/types.tlb +++ b/types.tlb @@ -1,33 +1,25 @@ // Standard actions from block.tlb: out_list_empty$_ = OutList 0; -out_list$_ {n:#} prev:^(OutList n) action:OutAction - = OutList (n + 1); -action_send_msg#0ec3c86d mode:(## 8) - out_msg:^(MessageRelaxed Any) = OutAction; -action_set_code#ad4de08e new_code:^Cell = OutAction; -action_reserve_currency#36e6b809 mode:(## 8) - currency:CurrencyCollection = OutAction; -libref_hash$0 lib_hash:bits256 = LibRef; -libref_ref$1 library:^Cell = LibRef; -action_change_library#26fa1dd4 mode:(## 7) { mode <= 2 } - libref:LibRef = OutAction; +out_list$_ {n:#} prev:^(OutList n) action:OutAction = OutList (n + 1); +action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; // Extended actions in W5: action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0; action_list_extended$1 {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1); -action_set_data#1ff8ea0b data:^Cell = ExtendedAction; action_add_ext#1c40db9f addr:MsgAddressInt = ExtendedAction; action_delete_ext#5eaef4a4 addr:MsgAddressInt = ExtendedAction; +action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; -signed_request$_ - signature: bits512 // 512 - subwallet_id: uint32 // 512+32 - valid_until: uint32 // 512+32+32 - msg_seqno: uint32 // 512+32+32+32 = 608 - inner: InnerRequest = SignedRequest; +signed_request$_ // 32 (opcode from outer) + wallet_id: WalletID // 80 + valid_until: # // 32 + msg_seqno: # // 32 + inner: InnerRequest // 1 .. (1 + 32 + 256) + ^Cell + signature: bits512 // 512 += SignedRequest; // Total: 688 .. 976 + ^Cell -internal_signed#7369676e signed:SignedRequest = InternalMsgBody; +internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; internal_extension#6578746e inner:InnerRequest = InternalMsgBody; external_signed#7369676e signed:SignedRequest = ExternalMsgBody; @@ -35,4 +27,4 @@ actions$_ {m:#} {n:#} actions:(ActionList n m) = InnerRequest; // Contract state wallet_id$_ global_id:int32 wc:int8 version:(## 8) subwallet_number:(## 32) = WalletID; -contract_state$_ seqno:# wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; +contract_state$_ seqno:int33 wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 1eca81b5..fe0c7085 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -23,7 +23,7 @@ export type WalletV5Config = { export function walletV5ConfigToCell(config: WalletV5Config): Cell { return beginCell() - .storeUint(config.seqno, 32) + .storeInt(config.seqno, 33) .storeUint(config.walletId, 80) .storeBuffer(config.publicKey, 32) .storeDict(config.extensions, Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8)) @@ -36,8 +36,10 @@ export const Opcodes = { action_extended_set_data: 0x1ff8ea0b, action_extended_add_extension: 0x1c40db9f, action_extended_remove_extension: 0x5eaef4a4, + action_extended_set_signature_auth_allowed: 0x20cbb95a, auth_extension: 0x6578746e, - auth_signed: 0x7369676e + auth_signed: 0x7369676e, + auth_signed_internal: 0x73696e74 }; export class WalletId { @@ -136,7 +138,7 @@ export class WalletV5 implements Contract { value: opts.value, sendMode: SendMode.PAY_GAS_SEPARATELY, body: beginCell() - .storeUint(Opcodes.auth_signed, 32) + // .storeUint(Opcodes.auth_signed_internal, 32) // Is signed inside message .storeSlice(opts.body.beginParse()) .endCell() }); @@ -170,7 +172,10 @@ export class WalletV5 implements Contract { async sendExternalSignedMessage(provider: ContractProvider, body: Cell) { await provider.external( - beginCell().storeUint(Opcodes.auth_signed, 32).storeSlice(body.beginParse()).endCell() + beginCell() + // .storeUint(Opcodes.auth_signed, 32) // Is signed inside message + .storeSlice(body.beginParse()) + .endCell() ); } @@ -193,6 +198,16 @@ export class WalletV5 implements Contract { } } + async getIsSignatureAuthAllowed(provider: ContractProvider) { + const state = await provider.getState(); + if (state.state.type === 'active') { + let res = await provider.get('get_is_signature_auth_allowed', []); + return res.stack.readNumber(); + } else { + return -1; + } + } + async getWalletId(provider: ContractProvider) { const result = await provider.get('get_wallet_id', []); return WalletId.deserialize(result.stack.readBigNumber());