diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 3ba9037fc1..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 @@ -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 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 diff --git a/data/transactions/logic/TEAL_opcodes_v10.md b/data/transactions/logic/TEAL_opcodes_v10.md index 4ec00a52bb..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 @@ -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 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) 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/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..6649627440 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") @@ -139,6 +146,73 @@ 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) { + 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") + + // 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) { @@ -167,22 +241,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 +306,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 { @@ -523,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 c060d82623..bfecb01a67 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -286,13 +286,15 @@ 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}, "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 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 @@ -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..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": [ @@ -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 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" + ] + }, { "Opcode": 224, "Name": "ec_add", 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": [ 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",