Skip to content

Commit

Permalink
AVM: Add box_splice and box_resize opcodes (#5750)
Browse files Browse the repository at this point in the history
  • Loading branch information
jannotti committed Dec 4, 2023
1 parent ae133b8 commit ed278b8
Show file tree
Hide file tree
Showing 16 changed files with 311 additions and 18 deletions.
10 changes: 9 additions & 1 deletion data/transactions/logic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,12 @@ Account fields used in the `acct_params_get` opcode.

### Box Access

Box opcodes that create, delete, or resize boxes affect the minimum
balance requirement of the calling application's account. The change
is immediate, and can be observed after exection by using
`min_balance`. If the account does not possess the new minimum
balance, the opcode fails.

All box related opcodes fail immediately if used in a
ClearStateProgram. This behavior is meant to discourage Smart Contract
authors from depending upon the availability of boxes in a ClearState
Expand All @@ -763,13 +769,15 @@ are sure to be _available_.

| Opcode | Description |
| - | -- |
| `box_create` | create a box named A, of length B. Fail if A is empty or B exceeds 32,768. Returns 0 if A already existed, else 1 |
| `box_create` | create a box named A, of length B. Fail if the name A is empty or B exceeds 32,768. Returns 0 if A already existed, else 1 |
| `box_extract` | read C bytes from box A, starting at offset B. Fail if A does not exist, or the byte range is outside A's size. |
| `box_replace` | write byte-array C into box A, starting at offset B. Fail if A does not exist, or the byte range is outside A's size. |
| `box_splice` | set box A to contain its previous bytes up to index B, followed by D, followed by the original bytes of A that began at index B+C. |
| `box_del` | delete box named A if it exists. Return 1 if A existed, 0 otherwise |
| `box_len` | X is the length of box A if A exists, else 0. Y is 1 if A exists, else 0. |
| `box_get` | X is the contents of box A if A exists, else ''. Y is 1 if A exists, else 0. |
| `box_put` | replaces the contents of box A with byte-array B. Fails if A exists and len(B) != len(box A). Creates A if it does not exist |
| `box_resize` | change the size of box named A to be of length B, adding zero bytes to end or removing bytes from the end, as needed. Fail if the name A is empty, A is not an existing box, or B exceeds 32,768. |

### Inner Transactions

Expand Down
6 changes: 6 additions & 0 deletions data/transactions/logic/README_in.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,12 @@ Account fields used in the `acct_params_get` opcode.

### Box Access

Box opcodes that create, delete, or resize boxes affect the minimum
balance requirement of the calling application's account. The change
is immediate, and can be observed after exection by using
`min_balance`. If the account does not possess the new minimum
balance, the opcode fails.

All box related opcodes fail immediately if used in a
ClearStateProgram. This behavior is meant to discourage Smart Contract
authors from depending upon the availability of boxes in a ClearState
Expand Down
20 changes: 19 additions & 1 deletion data/transactions/logic/TEAL_opcodes_v10.md
Original file line number Diff line number Diff line change
Expand Up @@ -1487,7 +1487,7 @@ The notation A,B indicates that A and B are interpreted as a uint128 value, with

- Bytecode: 0xb9
- Stack: ..., A: boxName, B: uint64 → ..., bool
- create a box named A, of length B. Fail if A is empty or B exceeds 32,768. Returns 0 if A already existed, else 1
- create a box named A, of length B. Fail if the name A is empty or B exceeds 32,768. Returns 0 if A already existed, else 1
- Availability: v8
- Mode: Application

Expand Down Expand Up @@ -1641,6 +1641,24 @@ Fields
| 1 | BlkTimestamp | uint64 | |


## box_splice

- Bytecode: 0xd2
- Stack: ..., A: boxName, B: uint64, C: uint64, D: []byte → ...
- set box A to contain its previous bytes up to index B, followed by D, followed by the original bytes of A that began at index B+C.
- Availability: v10
- Mode: Application

Boxes are of constant length. If C < len(D), then len(D)-C bytes will be removed from the end. If C > len(D), zero bytes will be appended to the end to reach the box length.

## box_resize

- Bytecode: 0xd3
- Stack: ..., A: boxName, B: uint64 &rarr; ...
- change the size of box named A to be of length B, adding zero bytes to end or removing bytes from the end, as needed. Fail if the name A is empty, A is not an existing box, or B exceeds 32,768.
- Availability: v10
- Mode: Application

## ec_add

- Syntax: `ec_add G` ∋ G: [EC](#field-group-ec)
Expand Down
2 changes: 1 addition & 1 deletion data/transactions/logic/TEAL_opcodes_v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -1485,7 +1485,7 @@ The notation A,B indicates that A and B are interpreted as a uint128 value, with

- Bytecode: 0xb9
- Stack: ..., A: boxName, B: uint64 &rarr; ..., bool
- create a box named A, of length B. Fail if A is empty or B exceeds 32,768. Returns 0 if A already existed, else 1
- create a box named A, of length B. Fail if the name A is empty or B exceeds 32,768. Returns 0 if A already existed, else 1
- Availability: v8
- Mode: Application

Expand Down
2 changes: 1 addition & 1 deletion data/transactions/logic/TEAL_opcodes_v9.md
Original file line number Diff line number Diff line change
Expand Up @@ -1485,7 +1485,7 @@ The notation A,B indicates that A and B are interpreted as a uint128 value, with

- Bytecode: 0xb9
- Stack: ..., A: boxName, B: uint64 &rarr; ..., bool
- create a box named A, of length B. Fail if A is empty or B exceeds 32,768. Returns 0 if A already existed, else 1
- create a box named A, of length B. Fail if the name A is empty or B exceeds 32,768. Returns 0 if A already existed, else 1
- Availability: v8
- Mode: Application

Expand Down
15 changes: 12 additions & 3 deletions data/transactions/logic/assembler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,13 @@ pushbytess "1" "2" "1"
const v8Nonsense = v7Nonsense + switchNonsense + frameNonsense + matchNonsense + boxNonsense

const v9Nonsense = v8Nonsense
const v10Nonsense = v9Nonsense + pairingNonsense

const spliceNonsence = `
box_splice
box_resize
`

const v10Nonsense = v9Nonsense + pairingNonsense + spliceNonsence

const v6Compiled = "2004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f2310231123122313231418191a1b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b400b53a03b6b7043cb8033a0c2349c42a9631007300810881088120978101c53a8101c6003a"

Expand All @@ -447,7 +453,10 @@ const matchCompiled = "83030102018e02fff500008203013101320131"
const v8Compiled = v7Compiled + switchCompiled + frameCompiled + matchCompiled + boxCompiled

const v9Compiled = v8Compiled
const v10Compiled = v9Compiled + pairingCompiled

const spliceCompiled = "d2d3"

const v10Compiled = v9Compiled + pairingCompiled + spliceCompiled

var nonsense = map[uint64]string{
1: v1Nonsense,
Expand Down Expand Up @@ -527,7 +536,7 @@ func TestAssemble(t *testing.T) {
}
}

var experiments = []uint64{pairingVersion}
var experiments = []uint64{pairingVersion, spliceVersion}

// TestExperimental forces a conscious choice to promote "experimental" opcode
// groups. This will fail when we increment vFuture's LogicSigVersion. If we had
Expand Down
110 changes: 110 additions & 0 deletions data/transactions/logic/box.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const (
BoxWriteOperation
// BoxDeleteOperation deletes a box
BoxDeleteOperation
// BoxResizeOperation resizes a box
BoxResizeOperation
)

func (cx *EvalContext) availableBox(name string, operation BoxOperation, createSize uint64) ([]byte, bool, error) {
Expand Down Expand Up @@ -81,6 +83,13 @@ func (cx *EvalContext) availableBox(name string, operation BoxOperation, createS
cx.available.dirtyBytes += writeSize
}
dirty = true
case BoxResizeOperation:
newSize := createSize
if dirty {
cx.available.dirtyBytes -= uint64(len(content))
}
cx.available.dirtyBytes += newSize
dirty = true
case BoxDeleteOperation:
if dirty {
cx.available.dirtyBytes -= uint64(len(content))
Expand Down Expand Up @@ -199,6 +208,34 @@ func opBoxReplace(cx *EvalContext) error {
return cx.Ledger.SetBox(cx.appID, name, bytes)
}

func opBoxSplice(cx *EvalContext) error {
last := len(cx.Stack) - 1 // replacement
replacement := cx.Stack[last].Bytes
length := cx.Stack[last-1].Uint
start := cx.Stack[last-2].Uint
name := string(cx.Stack[last-3].Bytes)

err := argCheck(cx, name, 0)
if err != nil {
return err
}

contents, exists, err := cx.availableBox(name, BoxWriteOperation, 0 /* size is already known */)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("no such box %#x", name)
}

bytes, err := spliceCarefully(contents, replacement, start, length)
if err != nil {
return err
}
cx.Stack = cx.Stack[:last-3]
return cx.Ledger.SetBox(cx.appID, name, bytes)
}

func opBoxDel(cx *EvalContext) error {
last := len(cx.Stack) - 1 // name
name := string(cx.Stack[last].Bytes)
Expand All @@ -222,6 +259,48 @@ func opBoxDel(cx *EvalContext) error {
return nil
}

func opBoxResize(cx *EvalContext) error {
last := len(cx.Stack) - 1 // size
prev := last - 1 // name

name := string(cx.Stack[prev].Bytes)
size := cx.Stack[last].Uint

err := argCheck(cx, name, size)
if err != nil {
return err
}

contents, exists, err := cx.availableBox(name, BoxResizeOperation, size)
if err != nil {
return err
}

if !exists {
return fmt.Errorf("no such box %#x", name)
}
appAddr := cx.GetApplicationAddress(cx.appID)
_, err = cx.Ledger.DelBox(cx.appID, name, appAddr)
if err != nil {
return err
}
var resized []byte
if size > uint64(len(contents)) {
resized = make([]byte, size)
copy(resized, contents)
} else {
resized = contents[:size]
}
err = cx.Ledger.NewBox(cx.appID, name, resized, appAddr)
if err != nil {
return err
}

cx.Stack = cx.Stack[:prev]
return err

}

func opBoxLen(cx *EvalContext) error {
last := len(cx.Stack) - 1 // name
name := string(cx.Stack[last].Bytes)
Expand Down Expand Up @@ -292,3 +371,34 @@ func opBoxPut(cx *EvalContext) error {
appAddr := cx.GetApplicationAddress(cx.appID)
return cx.Ledger.NewBox(cx.appID, name, value, appAddr)
}

// spliceCarefully is used to make a NEW byteslice copy of original, with
// replacement written over the bytes from start to start+length. Returned slice
// is always the same size as original. Zero bytes are "shifted in" or high
// bytes are "shifted out" as needed.
func spliceCarefully(original []byte, replacement []byte, start uint64, olen uint64) ([]byte, error) {
if start > uint64(len(original)) {
return nil, fmt.Errorf("replacement start %d beyond length: %d", start, len(original))
}
oend := start + olen
if oend < start {
return nil, fmt.Errorf("splice end exceeds uint64")
}

if oend > uint64(len(original)) {
return nil, fmt.Errorf("splice end %d beyond original length: %d", oend, len(original))
}

// Do NOT use the append trick to make a copy here.
// append(nil, []byte{}...) would return a nil, which means "not a bytearray" to AVM.
clone := make([]byte, len(original))
copy(clone[:start], original)
copied := copy(clone[start:], replacement)
if copied != len(replacement) {
return nil, fmt.Errorf("splice inserted bytes too long")
}
// If original is "too short" we get zeros at the end. If original is "too
// long" we lose some bytes. Fortunately, that's what we want.
copy(clone[int(start)+copied:], original[oend:])
return clone, nil
}

0 comments on commit ed278b8

Please sign in to comment.