From efb7bf6aaa721b3c102042116cbbb62537f0b653 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Thu, 21 Dec 2023 16:15:39 -0500 Subject: [PATCH] More box work and docs --- docs/state.rst | 61 +++++++++++++++++++++++++++++++++++++++++++---- pyteal/ast/app.py | 39 ++++++++++++++++++++++++++++++ pyteal/ast/box.py | 12 ++++++---- 3 files changed, 102 insertions(+), 10 deletions(-) diff --git a/docs/state.rst b/docs/state.rst index b45a5a0c6..884312b94 100644 --- a/docs/state.rst +++ b/docs/state.rst @@ -30,6 +30,8 @@ Other App Global :any:`App.gl Other App Local :any:`App.localGetEx` :any:`App.localGetEx` Current App Boxes :any:`App.box_create` :any:`App.box_put` :any:`App.box_extract` :any:`App.box_delete` :any:`App.box_length` :any:`App.box_put` :any:`App.box_replace` :any:`App.box_get` :any:`App.box_get` + :any:`App.box_splice` + :any:`App.box_resize` ================== ======================= ======================== ======================== ===================== ======================= Global State @@ -267,9 +269,10 @@ The app account's minimum balance requirement (MBR) is increased with each addit If one deletes an application with outstanding boxes, the MBR is not recoverable from the deleted app account. It is recommended that *before* app deletion, all box storage be deleted, and funds previously allocated to the MBR be withdrawn. -Box sizes and names cannot be changed after initial allocation, but they can be deleted and re-allocated. Boxes are only visible to the application itself; in other words, an application cannot read from or write to another application's boxes on-chain. +Boxes are fixed-length structures, though they can be resized with the :any:`App.box_resize` method (or by deleting and recreating the box). + The following sections explain how to work with boxes. .. _Creating Boxes: @@ -311,19 +314,67 @@ For :any:`App.box_put`, the first argument is the box name to create or to write # write to box `poemLine` with new value App.box_put(Bytes("poemLine"), Bytes("The lone and level sands stretch far away.")) +Resizing Boxes +~~~~~~~~~~~~~~ + +Boxes that already exist can be resized using the :any:`App.box_resize` method. This is the only way to resize a box, besides deleting it and recreating it. + +For :any:`App.box_resize`, the first argument is the box name to resize, and the second argument is the new byte size to be allocated. + +.. note:: + If the new size is smaller than the existing box's byte size, then the box will lose the bytes at the end. + If the new size is larger than the existing box's byte size, then the box will be padded with zeros at the end. + + For all size changes, the app account's minimum balance requirement (MBR) will be updated accordingly. + +For example: + +.. code-block:: python + + # resize a box called "BoxA" to byte size 200 + App.box_resize(Bytes("BoxA"), Int(200)) + Writing to a Box ~~~~~~~~~~~~~~~~ -To write to a box, use :any:`App.box_replace`, or :any:`App.box_put` method. +To write to a box, use :any:`App.box_replace`, :any:`App.box_splice` , or :any:`App.box_put` method. -:any:`App.box_replace` writes bytes of certain length from a start index in a box. +:any:`App.box_replace` replaces a range of bytes in a box. The first argument is the box name to write into, the second argument is the starting index to write, and the third argument is the replacement bytes. For example: .. code-block:: python - # replace 2 bytes starting from the 0'th byte by `Ne` in the box named `wordleBox` - App.box_replace(Bytes("wordleBox"), Int(0), Bytes("Ne")) + # Assume the box named "wordleBox" initially contains the bytes "cones" + + # Replace 2 bytes starting from index 1 with "ap" in the box named "wordleBox" + App.box_replace(Bytes("wordleBox"), Int(1), Bytes("ap")) + + # The result is that the box named "wordleBox" now contains the bytes "capes" + +:any:`App.box_splice` is a more general version of :any:`App.box_replace`. This operation takes an +additional argument, which is the length of the bytes in the box to be replaced. By specifying a +different length than the bytes you are inserting, you can shift contents of the box instead of just +replacing a range of bytes. + +For example: + +.. code-block:: python + + # Assume the box named "flavors" initially contains the bytes "banana_apple_cherry_______" + + # Insert "grape_" at index 7 in the box named "flavors". By specifying a length of 0, the + # following bytes will be shifted to the right. + App.box_splice(Bytes("flavors"), Int(7), Int(0), Bytes("grape_")) + + # The result is that the box named "flavors" now contains the bytes "banana_grape_apple_cherry_" + + # If we want to zero the box, we can replace the entire contents with an empty string. + App.box_splice(Bytes("flavors"), Int(0), Int(26), Bytes("")) + # The "flavors" box now contains "00000000000000000000000000". Ready for reuse! + +Recall that boxes are fixed length, so shifting bytes can cause the box to truncate or pad with zeros. +More information is available in the docstring for :any:`App.box_splice`. :any:`App.box_put` writes the full contents to a pre-existing box, as is mentioned in `Creating Boxes`_. diff --git a/pyteal/ast/app.py b/pyteal/ast/app.py index 1e01e6abc..04aa06f5f 100644 --- a/pyteal/ast/app.py +++ b/pyteal/ast/app.py @@ -2,9 +2,11 @@ from enum import Enum from pyteal.ast.box import ( BoxCreate, + BoxResize, BoxDelete, BoxExtract, BoxReplace, + BoxSplice, BoxLen, BoxGet, BoxPut, @@ -237,6 +239,19 @@ def box_create(cls, name: Expr, size: Expr) -> Expr: """ return BoxCreate(name, size) + @classmethod + def box_resize(cls, name: Expr, size: Expr) -> Expr: + """Resize an existing box. + + If the new size is larger than the old size, zero bytes will be added to the end of the box. + If the new size is smaller than the old size, the box will be truncated from the end. + + Args: + name: The key used to reference this box. Must evaluate to a bytes. + size: The new number of bytes to reserve for this box. Must evaluate to a uint64. + """ + return BoxResize(name, size) + @classmethod def box_delete(cls, name: Expr) -> Expr: """Deletes a box given it's name. @@ -272,6 +287,30 @@ def box_replace(cls, name: Expr, start: Expr, value: Expr) -> Expr: """ return BoxReplace(name, start, value) + @classmethod + def box_splice( + cls, name: Expr, start: Expr, length: Expr, new_content: Expr + ) -> Expr: + """ + Replaces the range of bytes from `start` through `start + length` with `new_content`. + + Bytes after `start + length` will be shifted to the right. + + Recall that boxes are constant length, and this operation will not change the length of the + box. Instead content may be adjusted as so: + + * If the length of the new content is less than `length`, the bytes following `start + length` will be shifted to the left, and the end of the box will be padded with zeros. + + * If the length of the new content is greater than `length`, the bytes following `start + length` will be shifted to the right and bytes exceeding the length of the box will be truncated. + + Args: + name: The name of the box to modify. Must evaluate to bytes. + start: The byte index into the box to start writing. Must evaluate to uint64. + length: The length of the bytes to be replaced. Must evaluate to uint64. + new_content: The new content to write into the box. Must evaluate to bytes. + """ + return BoxSplice(name, start, length, new_content) + @classmethod def box_length(cls, name: Expr) -> MaybeValue: """Get the byte length of the box specified by its name. diff --git a/pyteal/ast/box.py b/pyteal/ast/box.py index 3e37cd604..5977d0512 100644 --- a/pyteal/ast/box.py +++ b/pyteal/ast/box.py @@ -220,12 +220,14 @@ def __init__( """ Replaces the range of bytes from `start` through `start + length` with `new_content`. - Recall that boxes are constant length, so this operation will not change the length of the + Bytes after `start + length` will be shifted to the right. + + Recall that boxes are constant length, and this operation will not change the length of the box. Instead content may be adjusted as so: - * If the length of the new content is less than `length`, zero bytes will be added to the - end of the box to make up the difference. - * If the length of the new content is greater than `length`, bytes will be truncated from - the end of the box to make up the difference. + + * If the length of the new content is less than `length`, the bytes following `start + length` will be shifted to the left, and the end of the box will be padded with zeros. + + * If the length of the new content is greater than `length`, the bytes following `start + length` will be shifted to the right and bytes exceeding the length of the box will be truncated. Args: name: The name of the box to modify. Must evaluate to bytes.