From 6249f747cfaed4551f0a7deda0bc6f6bfb2f201b Mon Sep 17 00:00:00 2001 From: John Jannotti Date: Thu, 21 Sep 2023 13:02:15 -0400 Subject: [PATCH 1/3] Add box_splice and box_resize opcodes These opcodes simplify working with large boxes. There are simple combinations of existing codes to do the equivalent on boxes smaller than 4k. But they require moving box content into AVM values that cannot exceed 4k. So, for big boxes it is useful to have these operations. (box_splice may also be a nice convenience, even for small boxes) --- data/transactions/logic/README.md | 2 + data/transactions/logic/TEAL_opcodes_v10.md | 18 +++ data/transactions/logic/assembler_test.go | 15 ++- data/transactions/logic/box.go | 110 +++++++++++++++++++ data/transactions/logic/box_test.go | 88 ++++++++++++++- data/transactions/logic/doc.go | 4 +- data/transactions/logic/eval.go | 2 +- data/transactions/logic/langspec_v10.json | 33 ++++++ data/transactions/logic/opcodeExplain.go | 12 ++ data/transactions/logic/opcodes.go | 3 + data/transactions/logic/teal.tmLanguage.json | 2 +- 11 files changed, 279 insertions(+), 10 deletions(-) diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 3ba9037fc1..58a26d7724 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -766,10 +766,12 @@ are sure to be _available_. | `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_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 A to be of length B, adding zero bytes to end or removing bytes from the end, as needed. Fail if A is empty, is not an existing box, or B exceeds 32,768. | ### Inner Transactions diff --git a/data/transactions/logic/TEAL_opcodes_v10.md b/data/transactions/logic/TEAL_opcodes_v10.md index 4ec00a52bb..5055ec0b33 100644 --- a/data/transactions/logic/TEAL_opcodes_v10.md +++ b/data/transactions/logic/TEAL_opcodes_v10.md @@ -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 → ... +- change the size of box A to be of length B, adding zero bytes to end or removing bytes from the end, as needed. Fail if A is empty, 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) diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 19c75d201a..4fdcaae615 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -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" @@ -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, @@ -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 diff --git a/data/transactions/logic/box.go b/data/transactions/logic/box.go index ad371c6a1f..4938f95aa9 100644 --- a/data/transactions/logic/box.go +++ b/data/transactions/logic/box.go @@ -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) { @@ -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)) @@ -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) @@ -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) @@ -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 +} diff --git a/data/transactions/logic/box_test.go b/data/transactions/logic/box_test.go index 5f08878a93..205ee06d19 100644 --- a/data/transactions/logic/box_test.go +++ b/data/transactions/logic/box_test.go @@ -41,11 +41,15 @@ func TestBoxNewDel(t *testing.T) { ep, txn, ledger := MakeSampleEnv() createSelf := fmt.Sprintf(`byte "self"; int %d; box_create;`, size) + growSelf := fmt.Sprintf(`byte "self"; int %d; box_resize; int 1`, size+5) createOther := fmt.Sprintf(`byte "other"; int %d; box_create;`, size) ledger.NewApp(txn.Sender, 888, basics.AppParams{}) + TestApp(t, growSelf, ep, "no such box") + TestApp(t, createSelf, ep) + TestApp(t, growSelf, ep) ledger.DelBoxes(888, "self") TestApp(t, createSelf+`assert;`+createSelf+`!`, ep) @@ -77,10 +81,13 @@ func TestBoxNewBad(t *testing.T) { ledger.NewApp(txn.Sender, 888, basics.AppParams{}) TestApp(t, `byte "self"; int 999; box_create`, ep, "write budget") - // In test proto, you get 100 I/O budget per boxref + // In test proto, you get 100 I/O budget per boxref, and 1000 is the + // absolute biggest box. ten := [10]transactions.BoxRef{} txn.Boxes = append(txn.Boxes, ten[:]...) // write budget is now 11*100 = 1100 TestApp(t, `byte "self"; int 999; box_create`, ep) + TestApp(t, `byte "self"; int 1000; box_resize; int 1`, ep) + TestApp(t, `byte "self"; int 1001; box_resize; int 1`, ep, "box size too large") ledger.DelBoxes(888, "self") TestApp(t, `byte "self"; int 1000; box_create`, ep) ledger.DelBoxes(888, "self") @@ -141,6 +148,63 @@ func TestBoxReadWrite(t *testing.T) { "invalid Box reference") } +func TestBoxSplice(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + ep, txn, ledger := MakeSampleEnv() + + ledger.NewApp(txn.Sender, 888, basics.AppParams{}) + // extract some bytes until past the end, confirm the begin as zeros, and + // when it fails. + TestApp(t, `byte "self"; int 4; box_create;`, ep) + + // replace two bytes with two bytes. would usually use box_replace + TestApp(t, `byte "self"; int 1; int 2; byte 0x5555; box_splice; + byte "self"; box_get; assert; byte 0x00555500; ==`, ep) + + // replace first 55 with two 44s. + TestApp(t, `byte "self"; int 1; int 1; byte 0x4444; box_splice; + byte "self"; box_get; assert; byte 0x00444455; ==`, ep) + + // replace second 44 with two 33s. (loses the 55) + TestApp(t, `byte "self"; int 2; int 1; byte 0x3333; box_splice; + byte "self"; box_get; assert; byte 0x00443333; ==`, ep) + + // replace 0044 with 22. (shifts in a 0x00) + TestApp(t, `byte "self"; int 0; int 2; byte 0x22; box_splice; + byte "self"; box_get; assert; byte 0x22333300; ==`, ep) + + // dumb: try to replace 00 with 1111, but growing is illegal + TestApp(t, `byte "self"; int 3; int 1; byte 0x1111; box_splice; + byte "self"; box_get; assert; byte 0x2233331111; ==`, ep, + "inserted bytes too long") + + // dumber: try to replace 00__ with 1111, but placing outside bounds is illegal + TestApp(t, `byte "self"; int 3; int 2; byte 0x1111; box_splice; + byte "self"; box_get; assert; byte 0x2233331111; ==`, ep, + "splice end 5 beyond original length") + + // try to replace AT end (fails because it would extend) + TestApp(t, `byte "self"; int 4; int 0; byte 0x1111; box_splice; + byte "self"; box_get; assert; byte 0x223333001111; ==`, ep, + "splice inserted bytes too long") + + // so it's ok if you splice in nothing + TestApp(t, `byte "self"; int 4; int 0; byte 0x; box_splice; + byte "self"; box_get; assert; byte 0x22333300; ==`, ep) + + // try to replace BEYOND end (fails no matter what) + TestApp(t, `byte "self"; int 5; int 0; byte 0x1111; box_splice; + byte "self"; box_get; assert; byte 0x22333300001111; ==`, ep, + "replacement start 5 beyond length") + + // even doing nothing is illegal beyond the end + TestApp(t, `byte "self"; int 5; int 0; byte 0x; box_splice; + byte "self"; box_get; assert; byte 0x22333300; ==`, ep, + "replacement start 5 beyond length") +} + func TestBoxAcrossTxns(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() @@ -167,22 +231,37 @@ func TestDirtyTracking(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - ep, txn, ledger := MakeSampleEnv() + ep, txn, ledger := MakeSampleEnv() // has two box refs, "self", "other" = 200 budget ledger.NewApp(txn.Sender, 888, basics.AppParams{}) TestApp(t, `byte "self"; int 200; box_create`, ep) + TestApp(t, `byte "self"; int 201; box_resize; int 1`, ep, "write budget") TestApp(t, `byte "other"; int 201; box_create`, ep, "write budget") // deleting "self" doesn't give extra write budget to create big "other" - TestApp(t, `byte "self"; box_del; !; byte "other"; int 201; box_create`, ep, + TestApp(t, `byte "self"; box_del; assert; byte "other"; int 201; box_create`, ep, "write budget") // though it cancels out a creation that happened here TestApp(t, `byte "self"; int 200; box_create; assert byte "self"; box_del; assert - byte "self"; int 200; box_create; + byte "other"; int 200; box_create; + `, ep) + ledger.DelBoxes(888, "self", "other") + + // create 200, but shrink it, then the write budget frees up + TestApp(t, `byte "self"; int 200; box_create; assert + byte "self"; int 150; box_resize; + byte "other"; int 50; box_create; `, ep) + ledger.DelBoxes(888, "self", "other") + // confirm that the exactly right amount freed up + TestApp(t, `byte "self"; int 200; box_create; assert + byte "self"; int 150; box_resize; + byte "other"; int 51; box_create; + `, ep, "write budget") ledger.DelBoxes(888, "self", "other") + // same, but create a different box than deleted TestApp(t, `byte "self"; int 200; box_create; assert byte "self"; box_del; assert @@ -217,6 +296,7 @@ func TestBoxUnavailableWithClearState(t *testing.T) { "box_len": `byte "self"; box_len`, "box_put": `byte "put"; byte "self"; box_put`, "box_replace": `byte "self"; int 0; byte "new"; box_replace`, + "box_resize": `byte "self"; int 10; box_resize`, } for name, program := range tests { diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index c060d82623..8432f66f0e 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -289,10 +289,12 @@ var opDescByName = map[string]OpDesc{ "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", "Newly created boxes are filled with 0 bytes. `box_create` will fail if the referenced box already exists with a different size. Otherwise, existing boxes are unchanged by `box_create`.", nil}, "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.", "", nil}, "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.", "", nil}, + "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.", "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.", nil}, "box_del": {"delete box named A if it exists. Return 1 if A existed, 0 otherwise", "", nil}, "box_len": {"X is the length of box A if A exists, else 0. Y is 1 if A exists, else 0.", "", nil}, "box_get": {"X is the contents of box A if A exists, else ''. Y is 1 if A exists, else 0.", "For boxes that exceed 4,096 bytes, consider `box_create`, `box_extract`, and `box_replace`", nil}, "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", "For boxes that exceed 4,096 bytes, consider `box_create`, `box_extract`, and `box_replace`", nil}, + "box_resize": {"change the size of box A to be of length B, adding zero bytes to end or removing bytes from the end, as needed. Fail if A is empty, is not an existing box, or B exceeds 32,768.", "", nil}, } // OpDoc returns a description of the op @@ -351,7 +353,7 @@ var OpGroups = map[string][]string{ "Loading Values": {"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "pushint", "pushints", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "pushbytes", "pushbytess", "bzero", "arg", "arg_0", "arg_1", "arg_2", "arg_3", "args", "txn", "gtxn", "txna", "txnas", "gtxna", "gtxnas", "gtxns", "gtxnsa", "gtxnsas", "global", "load", "loads", "store", "stores", "gload", "gloads", "gloadss", "gaid", "gaids"}, "Flow Control": {"err", "bnz", "bz", "b", "return", "pop", "popn", "dup", "dup2", "dupn", "dig", "bury", "cover", "uncover", "frame_dig", "frame_bury", "swap", "select", "assert", "callsub", "proto", "retsub", "switch", "match"}, "State Access": {"balance", "min_balance", "app_opted_in", "app_local_get", "app_local_get_ex", "app_global_get", "app_global_get_ex", "app_local_put", "app_global_put", "app_local_del", "app_global_del", "asset_holding_get", "asset_params_get", "app_params_get", "acct_params_get", "log", "block"}, - "Box Access": {"box_create", "box_extract", "box_replace", "box_del", "box_len", "box_get", "box_put"}, + "Box Access": {"box_create", "box_extract", "box_replace", "box_splice", "box_del", "box_len", "box_get", "box_put", "box_resize"}, "Inner Transactions": {"itxn_begin", "itxn_next", "itxn_field", "itxn_submit", "itxn", "itxna", "itxnas", "gitxn", "gitxna", "gitxnas"}, } diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 2db51b24a7..0e1faec0de 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -3962,7 +3962,7 @@ func replaceCarefully(original []byte, replacement []byte, start uint64) ([]byte return nil, fmt.Errorf("replacement start %d beyond length: %d", start, len(original)) } end := start + uint64(len(replacement)) - if end < start { // impossible because it is sum of two avm value lengths + if end < start { // impossible because it is sum of two avm value (or box) lengths return nil, fmt.Errorf("replacement end exceeds uint64") } diff --git a/data/transactions/logic/langspec_v10.json b/data/transactions/logic/langspec_v10.json index 7ee54fb98d..13e89fdf08 100644 --- a/data/transactions/logic/langspec_v10.json +++ b/data/transactions/logic/langspec_v10.json @@ -4644,6 +4644,39 @@ "State Access" ] }, + { + "Opcode": 210, + "Name": "box_splice", + "Args": [ + "boxName", + "uint64", + "uint64", + "[]byte" + ], + "Size": 1, + "DocCost": "1", + "Doc": "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.", + "DocExtra": "Boxes are of constant length. If C \u003c len(D), then len(D)-C bytes will be removed from the end. If C \u003e len(D), zero bytes will be appended to the end to reach the box length.", + "IntroducedVersion": 10, + "Groups": [ + "Box Access" + ] + }, + { + "Opcode": 211, + "Name": "box_resize", + "Args": [ + "boxName", + "uint64" + ], + "Size": 1, + "DocCost": "1", + "Doc": "change the size of box A to be of length B, adding zero bytes to end or removing bytes from the end, as needed. Fail if A is empty, is not an existing box, or B exceeds 32,768.", + "IntroducedVersion": 10, + "Groups": [ + "Box Access" + ] + }, { "Opcode": 224, "Name": "ec_add", diff --git a/data/transactions/logic/opcodeExplain.go b/data/transactions/logic/opcodeExplain.go index 4b4f965a65..3643ade21f 100644 --- a/data/transactions/logic/opcodeExplain.go +++ b/data/transactions/logic/opcodeExplain.go @@ -197,6 +197,12 @@ func opBoxReplaceStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, bas return BoxState, AppStateWrite, cx.appID, basics.Address{}, string(cx.Stack[pprev].Bytes) } +func opBoxSpliceStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { + name := len(cx.Stack) - 4 // name, start, length, replacement + + return BoxState, AppStateWrite, cx.appID, basics.Address{}, string(cx.Stack[name].Bytes) +} + func opBoxDelStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { last := len(cx.Stack) - 1 // name @@ -210,6 +216,12 @@ func opBoxPutStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics. return BoxState, AppStateWrite, cx.appID, basics.Address{}, string(cx.Stack[prev].Bytes) } +func opBoxResizeStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { + name := len(cx.Stack) - 2 // name, size + + return BoxState, AppStateWrite, cx.appID, basics.Address{}, string(cx.Stack[name].Bytes) +} + func opAppLocalGetStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { last := len(cx.Stack) - 1 // state key prev := last - 1 // account diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index dc2d44bc09..fa3fd22625 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -75,6 +75,7 @@ const sharedResourcesVersion = 9 // apps can access resources from other transac // moved from vFuture to a new consensus version. If they remain unready, bump // their version, and fixup TestAssemble() in assembler_test.go. const pairingVersion = 10 // bn256 opcodes. will add bls12-381, and unify the available opcodes. +const spliceVersion = 10 // box splicing/resizing // Unlimited Global Storage opcodes const boxVersion = 8 // box_* @@ -721,6 +722,8 @@ var OpSpecs = []OpSpec{ // randomness support {0xd0, "vrf_verify", opVrfVerify, proto("b83:bT"), randomnessVersion, field("s", &VrfStandards).costs(5700)}, {0xd1, "block", opBlock, proto("i:a"), randomnessVersion, field("f", &BlockFields)}, + {0xd2, "box_splice", opBoxSplice, proto("Niib:").appStateExplain(opBoxSpliceStateChange), spliceVersion, only(ModeApp)}, + {0xd3, "box_resize", opBoxResize, proto("Ni:").appStateExplain(opBoxResizeStateChange), spliceVersion, only(ModeApp)}, {0xe0, "ec_add", opEcAdd, proto("bb:b"), pairingVersion, costByField("g", &EcGroups, []int{ diff --git a/data/transactions/logic/teal.tmLanguage.json b/data/transactions/logic/teal.tmLanguage.json index 56cf7b5dc8..ef80fd048a 100644 --- a/data/transactions/logic/teal.tmLanguage.json +++ b/data/transactions/logic/teal.tmLanguage.json @@ -72,7 +72,7 @@ }, { "name": "keyword.other.unit.teal", - "match": "^(box_create|box_del|box_extract|box_get|box_len|box_put|box_replace|acct_params_get|app_global_del|app_global_get|app_global_get_ex|app_global_put|app_local_del|app_local_get|app_local_get_ex|app_local_put|app_opted_in|app_params_get|asset_holding_get|asset_params_get|balance|block|log|min_balance)\\b" + "match": "^(box_create|box_del|box_extract|box_get|box_len|box_put|box_replace|box_resize|box_splice|acct_params_get|app_global_del|app_global_get|app_global_get_ex|app_global_put|app_local_del|app_local_get|app_local_get_ex|app_local_put|app_opted_in|app_params_get|asset_holding_get|asset_params_get|balance|block|log|min_balance)\\b" }, { "name": "keyword.operator.teal", From f7624c7b055f4ec00c909635279d00fea88609d3 Mon Sep 17 00:00:00 2001 From: John Jannotti Date: Thu, 21 Sep 2023 15:14:48 -0400 Subject: [PATCH 2/3] Coverage and spec wording improvement --- data/transactions/logic/README.md | 4 ++-- data/transactions/logic/TEAL_opcodes_v10.md | 4 ++-- data/transactions/logic/TEAL_opcodes_v8.md | 2 +- data/transactions/logic/TEAL_opcodes_v9.md | 2 +- data/transactions/logic/box_test.go | 12 ++++++++++++ data/transactions/logic/doc.go | 4 ++-- data/transactions/logic/langspec_v10.json | 4 ++-- data/transactions/logic/langspec_v8.json | 2 +- data/transactions/logic/langspec_v9.json | 2 +- 9 files changed, 24 insertions(+), 12 deletions(-) diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 58a26d7724..fe91c24216 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -763,7 +763,7 @@ 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. | @@ -771,7 +771,7 @@ are sure to be _available_. | `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 A to be of length B, adding zero bytes to end or removing bytes from the end, as needed. Fail if A is empty, is not an existing box, or B exceeds 32,768. | +| `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 diff --git a/data/transactions/logic/TEAL_opcodes_v10.md b/data/transactions/logic/TEAL_opcodes_v10.md index 5055ec0b33..7df81c33a8 100644 --- a/data/transactions/logic/TEAL_opcodes_v10.md +++ b/data/transactions/logic/TEAL_opcodes_v10.md @@ -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 @@ -1655,7 +1655,7 @@ Boxes are of constant length. If C < len(D), then len(D)-C bytes will be removed - Bytecode: 0xd3 - Stack: ..., A: boxName, B: uint64 → ... -- change the size of box A to be of length B, adding zero bytes to end or removing bytes from the end, as needed. Fail if A is empty, is not an existing box, or B exceeds 32,768. +- 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 diff --git a/data/transactions/logic/TEAL_opcodes_v8.md b/data/transactions/logic/TEAL_opcodes_v8.md index b8efb37fa3..71d756a1dc 100644 --- a/data/transactions/logic/TEAL_opcodes_v8.md +++ b/data/transactions/logic/TEAL_opcodes_v8.md @@ -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 → ..., 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 diff --git a/data/transactions/logic/TEAL_opcodes_v9.md b/data/transactions/logic/TEAL_opcodes_v9.md index 54f053686f..f2ff330591 100644 --- a/data/transactions/logic/TEAL_opcodes_v9.md +++ b/data/transactions/logic/TEAL_opcodes_v9.md @@ -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 → ..., 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 diff --git a/data/transactions/logic/box_test.go b/data/transactions/logic/box_test.go index 205ee06d19..6649627440 100644 --- a/data/transactions/logic/box_test.go +++ b/data/transactions/logic/box_test.go @@ -146,6 +146,11 @@ func TestBoxReadWrite(t *testing.T) { "no such box") TestApp(t, `byte "junk"; int 1; byte 0x3031; box_replace`, ep, "invalid Box reference") + + TestApp(t, `byte "self"; int 1; int 2; byte 0x3031; box_splice`, ep, + "no such box") + TestApp(t, `byte "junk"; int 1; int 2; byte 0x3031; box_splice`, ep, + "invalid Box reference") } func TestBoxSplice(t *testing.T) { @@ -203,6 +208,11 @@ func TestBoxSplice(t *testing.T) { TestApp(t, `byte "self"; int 5; int 0; byte 0x; box_splice; byte "self"; box_get; assert; byte 0x22333300; ==`, ep, "replacement start 5 beyond length") + + // overflow doesn't work + TestApp(t, `byte "self"; int 2; int 18446744073709551615; byte 0x; box_splice; + byte "self"; box_get; assert; byte 0x22333300; ==`, ep, + "splice end exceeds uint64") } func TestBoxAcrossTxns(t *testing.T) { @@ -603,6 +613,8 @@ func TestEarlyPanics(t *testing.T) { "box_len": `byte "%s"; box_len`, "box_put": `byte "%s"; byte "hello"; box_put`, "box_replace": `byte "%s"; int 0; byte "new"; box_replace`, + "box_splice": `byte "%s"; int 0; int 2; byte "new"; box_splice`, + "box_resize": `byte "%s"; int 2; box_resize`, } for name, program := range tests { diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index 8432f66f0e..bfecb01a67 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -286,7 +286,7 @@ var opDescByName = map[string]OpDesc{ "frame_bury": {"replace the Nth (signed) value from the frame pointer in the stack with A", "", []string{"frame slot"}}, "popn": {"remove N values from the top of the stack", "", []string{"stack depth"}}, - "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", "Newly created boxes are filled with 0 bytes. `box_create` will fail if the referenced box already exists with a different size. Otherwise, existing boxes are unchanged by `box_create`.", nil}, + "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", "Newly created boxes are filled with 0 bytes. `box_create` will fail if the referenced box already exists with a different size. Otherwise, existing boxes are unchanged by `box_create`.", nil}, "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.", "", nil}, "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.", "", nil}, "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.", "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.", nil}, @@ -294,7 +294,7 @@ var opDescByName = map[string]OpDesc{ "box_len": {"X is the length of box A if A exists, else 0. Y is 1 if A exists, else 0.", "", nil}, "box_get": {"X is the contents of box A if A exists, else ''. Y is 1 if A exists, else 0.", "For boxes that exceed 4,096 bytes, consider `box_create`, `box_extract`, and `box_replace`", nil}, "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", "For boxes that exceed 4,096 bytes, consider `box_create`, `box_extract`, and `box_replace`", nil}, - "box_resize": {"change the size of box A to be of length B, adding zero bytes to end or removing bytes from the end, as needed. Fail if A is empty, is not an existing box, or B exceeds 32,768.", "", nil}, + "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.", "", nil}, } // OpDoc returns a description of the op diff --git a/data/transactions/logic/langspec_v10.json b/data/transactions/logic/langspec_v10.json index 13e89fdf08..affe46601e 100644 --- a/data/transactions/logic/langspec_v10.json +++ b/data/transactions/logic/langspec_v10.json @@ -4242,7 +4242,7 @@ ], "Size": 1, "DocCost": "1", - "Doc": "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", + "Doc": "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", "DocExtra": "Newly created boxes are filled with 0 bytes. `box_create` will fail if the referenced box already exists with a different size. Otherwise, existing boxes are unchanged by `box_create`.", "IntroducedVersion": 8, "Groups": [ @@ -4671,7 +4671,7 @@ ], "Size": 1, "DocCost": "1", - "Doc": "change the size of box A to be of length B, adding zero bytes to end or removing bytes from the end, as needed. Fail if A is empty, is not an existing box, or B exceeds 32,768.", + "Doc": "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.", "IntroducedVersion": 10, "Groups": [ "Box Access" diff --git a/data/transactions/logic/langspec_v8.json b/data/transactions/logic/langspec_v8.json index 3b496ddcb8..4963f4c85a 100644 --- a/data/transactions/logic/langspec_v8.json +++ b/data/transactions/logic/langspec_v8.json @@ -4238,7 +4238,7 @@ ], "Size": 1, "DocCost": "1", - "Doc": "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", + "Doc": "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", "DocExtra": "Newly created boxes are filled with 0 bytes. `box_create` will fail if the referenced box already exists with a different size. Otherwise, existing boxes are unchanged by `box_create`.", "IntroducedVersion": 8, "Groups": [ diff --git a/data/transactions/logic/langspec_v9.json b/data/transactions/logic/langspec_v9.json index c52d36862d..50418be824 100644 --- a/data/transactions/logic/langspec_v9.json +++ b/data/transactions/logic/langspec_v9.json @@ -4238,7 +4238,7 @@ ], "Size": 1, "DocCost": "1", - "Doc": "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", + "Doc": "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", "DocExtra": "Newly created boxes are filled with 0 bytes. `box_create` will fail if the referenced box already exists with a different size. Otherwise, existing boxes are unchanged by `box_create`.", "IntroducedVersion": 8, "Groups": [ From ba9abf9e32295f4a0d3374a47c266975491ca570 Mon Sep 17 00:00:00 2001 From: John Jannotti Date: Mon, 4 Dec 2023 13:21:21 -0500 Subject: [PATCH 3/3] Add note about boxes and MBR. --- data/transactions/logic/README.md | 6 ++++++ data/transactions/logic/README_in.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index fe91c24216..da4fdefac9 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -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 diff --git a/data/transactions/logic/README_in.md b/data/transactions/logic/README_in.md index e98d6c2441..31f8fb05be 100644 --- a/data/transactions/logic/README_in.md +++ b/data/transactions/logic/README_in.md @@ -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