Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AVM: Track Scratch Slot Types #4064

Merged
merged 12 commits into from
Jun 9, 2022
110 changes: 83 additions & 27 deletions data/transactions/logic/assembler.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ type ProgramKnowledge struct {
// deadcode indicates that the program is in deadcode, so no type checking
// errors should be reported.
deadcode bool

scratchSpace [256]StackType
}

func (pgm *ProgramKnowledge) pop() StackType {
Expand Down Expand Up @@ -312,6 +314,9 @@ func (pgm *ProgramKnowledge) reset() {
pgm.stack = nil
pgm.bottom = StackAny
pgm.deadcode = false
for i := range pgm.scratchSpace {
pgm.scratchSpace[i] = StackAny
}
}

// createLabel inserts a label to point to the next instruction, reporting an
Expand All @@ -334,7 +339,7 @@ func (ops *OpStream) referToLabel(pc int, label string) {
ops.labelReferences = append(ops.labelReferences, labelReference{ops.sourceLine, pc, label})
}

type refineFunc func(pgm ProgramKnowledge, immediates []string) (StackTypes, StackTypes)
type refineFunc func(pgm *ProgramKnowledge, immediates []string) (StackTypes, StackTypes)

// returns allows opcodes like `txn` to be specific about their return value
// types, based on the field requested, rather than use Any as specified by
Expand Down Expand Up @@ -969,7 +974,19 @@ func asmDefault(ops *OpStream, spec *OpSpec, args []string) error {
return nil
}

func typeSwap(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
// Interprets the arg at index argIndex as byte-long uint immediate
func getUintImm(args []string, argIndex int) (byte, bool) {
jannotti marked this conversation as resolved.
Show resolved Hide resolved
if len(args) <= argIndex {
return 0, false
}
n, err := strconv.ParseUint(args[argIndex], 0, 8)
if err != nil {
return 0, false
}
return byte(n), true
}

func typeSwap(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
topTwo := StackTypes{StackAny, StackAny}
top := len(pgm.stack) - 1
if top >= 0 {
Expand All @@ -982,12 +999,9 @@ func typeSwap(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
return nil, reversed
}

func typeDig(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
if len(args) == 0 {
return nil, nil
}
n, err := strconv.ParseUint(args[0], 0, 64)
if err != nil {
func typeDig(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
n, ok := getUintImm(args, 0)
if !ok {
return nil, nil
}
depth := int(n) + 1
Expand All @@ -1008,7 +1022,7 @@ func typeDig(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
return anys, returns
}

func typeEquals(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
func typeEquals(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
top := len(pgm.stack) - 1
if top >= 0 {
//Require arg0 and arg1 to have same type
Expand All @@ -1017,15 +1031,15 @@ func typeEquals(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
return nil, nil
}

func typeDup(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
func typeDup(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
top := len(pgm.stack) - 1
if top >= 0 {
return StackTypes{pgm.stack[top]}, StackTypes{pgm.stack[top], pgm.stack[top]}
}
return nil, nil
}

func typeDupTwo(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
func typeDupTwo(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
topTwo := StackTypes{StackAny, StackAny}
top := len(pgm.stack) - 1
if top >= 0 {
Expand All @@ -1037,7 +1051,7 @@ func typeDupTwo(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
return nil, append(topTwo, topTwo...)
}

func typeSelect(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
func typeSelect(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
top := len(pgm.stack) - 1
if top >= 2 {
if pgm.stack[top-1] == pgm.stack[top-2] {
Expand All @@ -1047,20 +1061,17 @@ func typeSelect(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
return nil, nil
}

func typeSetBit(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
func typeSetBit(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
top := len(pgm.stack) - 1
if top >= 2 {
return nil, StackTypes{pgm.stack[top-2]}
}
return nil, nil
}

func typeCover(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
if len(args) == 0 {
return nil, nil
}
n, err := strconv.ParseUint(args[0], 0, 64)
if err != nil {
func typeCover(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
n, ok := getUintImm(args, 0)
if !ok {
return nil, nil
}
depth := int(n) + 1
Expand All @@ -1086,12 +1097,9 @@ func typeCover(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
return anys, returns
}

func typeUncover(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
if len(args) == 0 {
return nil, nil
}
n, err := strconv.ParseUint(args[0], 0, 64)
if err != nil {
func typeUncover(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
n, ok := getUintImm(args, 0)
if !ok {
return nil, nil
}
depth := int(n) + 1
Expand All @@ -1114,7 +1122,7 @@ func typeUncover(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
return anys, returns
}

func typeTxField(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
func typeTxField(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
if len(args) != 1 {
return nil, nil
}
Expand All @@ -1125,6 +1133,51 @@ func typeTxField(pgm ProgramKnowledge, args []string) (StackTypes, StackTypes) {
return StackTypes{fs.ftype}, nil
}

func typeStore(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
scratchIndex, ok := getUintImm(args, 0)
if !ok {
return nil, nil
}
top := len(pgm.stack) - 1
if top >= 0 {
pgm.scratchSpace[scratchIndex] = pgm.stack[top]
}
return nil, nil
}

func typeStores(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
top := len(pgm.stack) - 1
if top < 0 {
return nil, nil
}
for i := range pgm.scratchSpace {
// We can't know what slot stacktop is being stored in, but we can at least keep the slots that are the same type as stacktop
if pgm.scratchSpace[i] != pgm.stack[top] {
pgm.scratchSpace[i] = StackAny
iten-alg marked this conversation as resolved.
Show resolved Hide resolved
}
}
return nil, nil
}

func typeLoad(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
scratchIndex, ok := getUintImm(args, 0)
if !ok {
return nil, nil
}
return nil, StackTypes{pgm.scratchSpace[scratchIndex]}
}

func typeLoads(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) {
scratchType := pgm.scratchSpace[0]
for _, item := range pgm.scratchSpace {
// If all the scratch slots are one type, then we can say we are loading that type
if item != scratchType {
return nil, nil
}
}
return nil, StackTypes{scratchType}
}

// keywords or "pseudo-ops" handle parsing and assembling special asm language
// constructs like 'addr' We use an OpSpec here, but it's somewhat degenerate,
// since they don't have opcodes or eval functions. But it does need a lot of
Expand Down Expand Up @@ -1302,6 +1355,9 @@ func (ops *OpStream) assemble(text string) error {
return ops.errorf("Can not assemble version %d", ops.Version)
}
scanner := bufio.NewScanner(fin)
for i := range ops.known.scratchSpace {
iten-alg marked this conversation as resolved.
Show resolved Hide resolved
ops.known.scratchSpace[i] = StackUint64
}
for scanner.Scan() {
ops.sourceLine++
line := scanner.Text()
Expand Down Expand Up @@ -1366,7 +1422,7 @@ func (ops *OpStream) assemble(text string) error {
}
args, returns := spec.Arg.Types, spec.Return.Types
if spec.OpDetails.refine != nil {
nargs, nreturns := spec.OpDetails.refine(ops.known, fields[1:])
nargs, nreturns := spec.OpDetails.refine(&ops.known, fields[1:])
if nargs != nil {
args = nargs
}
Expand Down
21 changes: 21 additions & 0 deletions data/transactions/logic/assembler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2400,6 +2400,27 @@ func TestSetBitTypeCheck(t *testing.T) {
testProg(t, "byte 0x1234; int 2; int 3; setbit; !", AssemblerMaxVersion, Expect{5, "! arg 0..."})
}

func TestScratchTypeCheck(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()
// All scratch slots should start as uint64
testProg(t, "load 0; int 1; +", AssemblerMaxVersion)
// Check load and store accurately using the scratch space
testProg(t, "byte 0x01; store 0; load 0; int 1; +", AssemblerMaxVersion, Expect{5, "+ arg 0..."})
// Loads should know the type it's loading if all the slots are the same type
testProg(t, "int 0; loads; btoi", AssemblerMaxVersion, Expect{3, "btoi arg 0..."})
iten-alg marked this conversation as resolved.
Show resolved Hide resolved
// Stores should only set slots to StackAny if they are not the same type as what is being stored
testProg(t, "byte 0x01; store 0; int 3; byte 0x01; stores; load 0; int 1; +", AssemblerMaxVersion, Expect{8, "+ arg 0..."})
// ScratchSpace should reset after hitting label in deadcode
testProg(t, "byte 0x01; store 0; b label1; label1:; load 0; int 1; +", AssemblerMaxVersion)
// But it should reset to StackAny not uint64
testProg(t, "int 1; store 0; b label1; label1:; load 0; btoi", AssemblerMaxVersion)
// Callsubs should also reset the scratch space
testProg(t, "callsub A; load 0; btoi; return; A: byte 0x01; store 0; retsub", AssemblerMaxVersion)
// But the scratchspace should still be tracked after the callsub
testProg(t, "callsub A; int 1; store 0; load 0; btoi; return; A: retsub", AssemblerMaxVersion, Expect{5, "btoi arg 0..."})
}

func TestCoverAsm(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()
Expand Down
15 changes: 11 additions & 4 deletions data/transactions/logic/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4534,6 +4534,8 @@ func TestLog(t *testing.T) {
source string
runMode runMode
errContains string
// For cases where assembly errors, we manually put in the bytes
assembledBytes []byte
}{
{
source: fmt.Sprintf(`byte "%s"; log; int 1`, strings.Repeat("a", maxLogSize+1)),
Expand Down Expand Up @@ -4561,9 +4563,10 @@ func TestLog(t *testing.T) {
runMode: modeApp,
},
{
source: `load 0; log`,
errContains: "log arg 0 wanted []byte but got uint64",
runMode: modeApp,
source: `load 0; log`,
errContains: "log arg 0 wanted []byte but got uint64",
runMode: modeApp,
assembledBytes: []byte{byte(ep.Proto.LogicSigVersion), 0x34, 0x00, 0xb0},
},
{
source: `byte "a logging message"; log; int 1`,
Expand All @@ -4575,7 +4578,11 @@ func TestLog(t *testing.T) {
for _, c := range failCases {
switch c.runMode {
case modeApp:
testApp(t, c.source, ep, c.errContains)
if c.assembledBytes == nil {
testApp(t, c.source, ep, c.errContains)
} else {
testAppBytes(t, c.assembledBytes, ep, c.errContains)
}
default:
testLogic(t, c.source, AssemblerMaxVersion, ep, c.errContains, c.errContains)
}
Expand Down
8 changes: 4 additions & 4 deletions data/transactions/logic/opcodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,8 @@ var OpSpecs = []OpSpec{
{0x32, "global", opGlobal, proto(":a"), 1, field("f", &GlobalFields)},
{0x33, "gtxn", opGtxn, proto(":a"), 1, immediates("t", "f").field("f", &TxnScalarFields)},
{0x33, "gtxn", opGtxn, proto(":a"), 2, immediates("t", "f").field("f", &TxnFields).assembler(asmGtxn2)},
{0x34, "load", opLoad, proto(":a"), 1, immediates("i")},
{0x35, "store", opStore, proto("a:"), 1, immediates("i")},
{0x34, "load", opLoad, proto(":a"), 1, stacky(typeLoad, "i")},
{0x35, "store", opStore, proto("a:"), 1, stacky(typeStore, "i")},
{0x36, "txna", opTxna, proto(":a"), 2, immediates("f", "i").field("f", &TxnArrayFields)},
{0x37, "gtxna", opGtxna, proto(":a"), 2, immediates("t", "f", "i").field("f", &TxnArrayFields)},
// Like gtxn, but gets txn index from stack, rather than immediate arg
Expand All @@ -458,8 +458,8 @@ var OpSpecs = []OpSpec{
{0x3d, "gaids", opGaids, proto("i:i"), 4, only(modeApp)},

// Like load/store, but scratch slot taken from TOS instead of immediate
{0x3e, "loads", opLoads, proto("i:a"), 5, opDefault()},
{0x3f, "stores", opStores, proto("ia:"), 5, opDefault()},
{0x3e, "loads", opLoads, proto("i:a"), 5, stacky(typeLoads)},
{0x3f, "stores", opStores, proto("ia:"), 5, stacky(typeStores)},

{0x40, "bnz", opBnz, proto("i:"), 1, opBranch()},
{0x41, "bz", opBz, proto("i:"), 2, opBranch()},
Expand Down