diff --git a/config/consensus.go b/config/consensus.go index c9589006ec..99cd1884df 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -1042,6 +1042,9 @@ func initConsensusProtocols() { vFuture.CompactCertWeightThreshold = (1 << 32) * 30 / 100 vFuture.CompactCertSecKQ = 128 + // Enable TEAL 6 / AVM 1.1 + vFuture.LogicSigVersion = 6 + Consensus[protocol.ConsensusFuture] = vFuture } diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index bc61b83b22..2fd6e993dd 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -134,7 +134,7 @@ intc 1 mulw ` -const v2Nonsense = ` +const v2Nonsense = v1Nonsense + ` dup2 pop pop @@ -219,7 +219,7 @@ txn FreezeAssetAccount txn FreezeAssetFrozen ` -const v3Nonsense = ` +const v3Nonsense = v2Nonsense + ` assert min_balance int 0x031337 // get bit 1, negate it, put it back @@ -248,7 +248,7 @@ pushbytes "john" // Keep in mind, only use existing int and byte constants, or else use // push* instead. The idea is to not cause the *cblocks to change. -const v4Nonsense = ` +const v4Nonsense = v3Nonsense + ` int 1 pushint 2000 int 0 @@ -294,7 +294,7 @@ gaids int 100 ` -const v5Nonsense = ` +const v5Nonsense = v4Nonsense + ` app_params_get AppExtraProgramPages cover 1 uncover 1 @@ -342,12 +342,16 @@ ecdsa_pk_recover Secp256k1 itxna Logs 3 ` +const v6Nonsense = v5Nonsense + ` +` + var nonsense = map[uint64]string{ 1: v1Nonsense, - 2: v1Nonsense + v2Nonsense, - 3: v1Nonsense + v2Nonsense + v3Nonsense, - 4: v1Nonsense + v2Nonsense + v3Nonsense + v4Nonsense, - 5: v1Nonsense + v2Nonsense + v3Nonsense + v4Nonsense + v5Nonsense, + 2: v2Nonsense, + 3: v3Nonsense, + 4: v4Nonsense, + 5: v5Nonsense, + 6: v6Nonsense, } var compiled = map[uint64]string{ @@ -356,6 +360,7 @@ var compiled = map[uint64]string{ 3: "032008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b692107210570004848210771004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f4478222105531421055427042106552105082106564c4d4b02210538212106391c0081e80780046a6f686e", 4: "042004010200b7a60c26040242420c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482a50512a632223524100034200004322602261222b634848222862482864286548482228236628226724286828692422700048482471004848361c0037001a0031183119311b311d311e311f312024221e312131223123312431253126312731283129312a312b312c312d312e312f44782522531422542b2355220823564c4d4b0222382123391c0081e80780046a6f686e2281d00f24231f880003420001892223902291922394239593a0a1a2a3a4a5a6a7a8a9aaabacadae23af3a00003b003c003d8164", 5: "052004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b53a03", + 6: "062004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b53a03", } func pseudoOp(opcode string) bool { @@ -381,6 +386,7 @@ func TestAssemble(t *testing.T) { // This doesn't have to be a sensible program to run, it just has to compile. t.Parallel() + require.Equal(t, LogicVersion, len(nonsense)) for v := uint64(2); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { for _, spec := range OpSpecs { @@ -1374,12 +1380,6 @@ func TestAssembleDisassembleCycle(t *testing.T) { // Disassembly won't necessarily perfectly recreate the source text, but assembling the result of Disassemble() should be the same program bytes. t.Parallel() - tests := map[uint64]string{ - 1: v1Nonsense, - 2: v1Nonsense + v2Nonsense, - 3: v1Nonsense + v2Nonsense + v3Nonsense, - } - // This confirms that each program compiles to the same bytes // (except the leading version indicator), when compiled under // original version, unspecified version (so it should pick up @@ -1388,7 +1388,8 @@ func TestAssembleDisassembleCycle(t *testing.T) { // optimizations in later versions that change the bytecode // emitted. But currently it is, so we test it for now to // catch any suprises. - for v, source := range tests { + require.Equal(t, LogicVersion, len(nonsense)) + for v, source := range nonsense { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { ops := testProg(t, source, v) t2, err := Disassemble(ops.Program) diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index aa2744ddc4..e02aad086e 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -3741,11 +3741,12 @@ func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs txnFieldSpec, txn *tr err = fmt.Errorf("Type arg not a byte array") return } - txType, ok := innerTxnTypes[string(sv.Bytes)] - if ok { - txn.Type = txType + txType := string(sv.Bytes) + ver, ok := innerTxnTypes[txType] + if ok && ver <= cx.version { + txn.Type = protocol.TxType(txType) } else { - err = fmt.Errorf("%s is not a valid Type for itxn_field", sv.Bytes) + err = fmt.Errorf("%s is not a valid Type for itxn_field", txType) } case TypeEnum: var i uint64 @@ -3755,9 +3756,9 @@ func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs txnFieldSpec, txn *tr } // i != 0 is so that the error reports 0 instead of Unknown if i != 0 && i < uint64(len(TxnTypeNames)) { - txType, ok := innerTxnTypes[TxnTypeNames[i]] - if ok { - txn.Type = txType + ver, ok := innerTxnTypes[TxnTypeNames[i]] + if ok && ver <= cx.version { + txn.Type = protocol.TxType(TxnTypeNames[i]) } else { err = fmt.Errorf("%s is not a valid Type for itxn_field", TxnTypeNames[i]) } @@ -3768,14 +3769,48 @@ func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs txnFieldSpec, txn *tr txn.Sender, err = cx.availableAccount(sv) case Fee: txn.Fee.Raw, err = sv.uint() - // FirstValid, LastValid unsettable: no motivation - // Note unsettable: would be strange, as this "Note" would not end up "chain-visible" + // FirstValid, LastValid unsettable: little motivation (maybe a app call + // wants to inspect?) If we set, make sure they are legal, both for current + // round, and separation by MaxLifetime (check lifetime in submit, not here) + case Note: + if len(sv.Bytes) > cx.Proto.MaxTxnNoteBytes { + err = fmt.Errorf("%s may not exceed %d bytes", fs.field, cx.Proto.MaxTxnNoteBytes) + } else { + txn.Note = make([]byte, len(sv.Bytes)) + copy(txn.Note[:], sv.Bytes) + } // GenesisID, GenesisHash unsettable: surely makes no sense // Group unsettable: Can't make groups from AVM (yet?) // Lease unsettable: This seems potentially useful. - // RekeyTo unsettable: Feels dangerous for first release. - // KeyReg not allowed yet, so no fields settable + case RekeyTo: + txn.RekeyTo, err = sv.address() + + // KeyReg + case VotePK: + if len(sv.Bytes) != 32 { + err = fmt.Errorf("%s must be 32 bytes", fs.field) + } else { + copy(txn.VotePK[:], sv.Bytes) + } + case SelectionPK: + if len(sv.Bytes) != 32 { + err = fmt.Errorf("%s must be 32 bytes", fs.field) + } else { + copy(txn.SelectionPK[:], sv.Bytes) + } + case VoteFirst: + var round uint64 + round, err = sv.uint() + txn.VoteFirst = basics.Round(round) + case VoteLast: + var round uint64 + round, err = sv.uint() + txn.VoteLast = basics.Round(round) + case VoteKeyDilution: + txn.VoteKeyDilution, err = sv.uint() + case Nonparticipation: + txn.Nonparticipation, err = sv.bool() // Payment case Receiver: @@ -3820,7 +3855,7 @@ func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs txnFieldSpec, txn *tr txn.AssetParams.URL, err = sv.string(cx.Proto.MaxAssetURLBytes) case ConfigAssetMetadataHash: if len(sv.Bytes) != 32 { - err = fmt.Errorf("ConfigAssetMetadataHash must be 32 bytes") + err = fmt.Errorf("%s must be 32 bytes", fs.field) } else { copy(txn.AssetParams.MetadataHash[:], sv.Bytes) } @@ -3857,7 +3892,8 @@ func opTxField(cx *EvalContext) { field := TxnField(cx.program[cx.pc+1]) fs, ok := txnFieldSpecByField[field] if !ok || fs.itxVersion == 0 || fs.itxVersion > cx.version { - cx.err = fmt.Errorf("invalid itxn_field field %d", field) + cx.err = fmt.Errorf("invalid itxn_field %s", field) + return } sv := cx.stack[last] cx.err = cx.stackIntoTxnField(sv, fs, &cx.subtxn.Txn) @@ -3880,15 +3916,6 @@ func opTxSubmit(cx *EvalContext) { return } - // Error out on anything unusual. Allow pay, axfer. - switch cx.subtxn.Txn.Type { - case protocol.PaymentTx, protocol.AssetTransferTx, protocol.AssetConfigTx, protocol.AssetFreezeTx: - // only pay, axfer, acfg, afrz for now - default: - cx.err = fmt.Errorf("Invalid inner transaction type %#v", cx.subtxn.Txn.Type) - return - } - // The goal is to follow the same invariants used by the // transaction pool. Namely that any transaction that makes it // to Perform (which is equivalent to eval.applyTransaction) diff --git a/data/transactions/logic/evalAppTxn_test.go b/data/transactions/logic/evalAppTxn_test.go index 693e1e82e4..60e8d83fb4 100644 --- a/data/transactions/logic/evalAppTxn_test.go +++ b/data/transactions/logic/evalAppTxn_test.go @@ -25,11 +25,18 @@ import ( "github.com/stretchr/testify/require" ) -func TestActionTypes(t *testing.T) { +func TestInnerTypesV5(t *testing.T) { + v5, _ := makeSampleEnvWithVersion(5) + // not alllowed in v5 + testApp(t, "itxn_begin; byte \"keyreg\"; itxn_field Type; itxn_submit; int 1;", v5, "keyreg is not a valid Type for itxn_field") + testApp(t, "itxn_begin; int keyreg; itxn_field TypeEnum; itxn_submit; int 1;", v5, "keyreg is not a valid Type for itxn_field") +} + +func TestCurrentInnerTypes(t *testing.T) { ep, ledger := makeSampleEnv() testApp(t, "itxn_submit; int 1;", ep, "itxn_submit without itxn_begin") testApp(t, "int pay; itxn_field TypeEnum; itxn_submit; int 1;", ep, "itxn_field without itxn_begin") - testApp(t, "itxn_begin; itxn_submit; int 1;", ep, "Invalid inner transaction type") + testApp(t, "itxn_begin; itxn_submit; int 1;", ep, "unknown tx type") // bad type testApp(t, "itxn_begin; byte \"pya\"; itxn_field Type; itxn_submit; int 1;", ep, "pya is not a valid Type") // mixed up the int form for the byte form @@ -37,11 +44,9 @@ func TestActionTypes(t *testing.T) { // or vice versa testApp(t, obfuscate("itxn_begin; byte \"pay\"; itxn_field TypeEnum; itxn_submit; int 1;"), ep, "not a uint64") - // good types, not alllowed yet - testApp(t, "itxn_begin; byte \"keyreg\"; itxn_field Type; itxn_submit; int 1;", ep, "keyreg is not a valid Type for itxn_field") + // good types, not allowed yet testApp(t, "itxn_begin; byte \"appl\"; itxn_field Type; itxn_submit; int 1;", ep, "appl is not a valid Type for itxn_field") // same, as enums - testApp(t, "itxn_begin; int keyreg; itxn_field TypeEnum; itxn_submit; int 1;", ep, "keyreg is not a valid Type for itxn_field") testApp(t, "itxn_begin; int appl; itxn_field TypeEnum; itxn_submit; int 1;", ep, "appl is not a valid Type for itxn_field") testApp(t, "itxn_begin; int 42; itxn_field TypeEnum; itxn_submit; int 1;", ep, "42 is not a valid TypeEnum") testApp(t, "itxn_begin; int 0; itxn_field TypeEnum; itxn_submit; int 1;", ep, "0 is not a valid TypeEnum") @@ -58,6 +63,10 @@ func TestActionTypes(t *testing.T) { testApp(t, "itxn_begin; int acfg; itxn_field TypeEnum; itxn_submit; int 1;", ep, "insufficient balance") testApp(t, "itxn_begin; int afrz; itxn_field TypeEnum; itxn_submit; int 1;", ep, "insufficient balance") + // alllowed since v6 + testApp(t, "itxn_begin; byte \"keyreg\"; itxn_field Type; itxn_submit; int 1;", ep, "insufficient balance") + testApp(t, "itxn_begin; int keyreg; itxn_field TypeEnum; itxn_submit; int 1;", ep, "insufficient balance") + // Establish 888 as the app id, and fund it. ledger.NewApp(ep.Txn.Txn.Receiver, 888, basics.AppParams{}) ledger.NewAccount(basics.AppIndex(888).Address(), 200000) @@ -220,6 +229,30 @@ func TestRekeyPay(t *testing.T) { // See explanation in logicLedger's Perform() } +func TestRekeyBack(t *testing.T) { + payAndUnkey := ` + itxn_begin + itxn_field Amount + itxn_field Receiver + itxn_field Sender + int pay + itxn_field TypeEnum + txn Sender + itxn_field RekeyTo + itxn_submit +` + + ep, ledger := makeSampleEnv() + ledger.NewApp(ep.Txn.Txn.Receiver, 888, basics.AppParams{}) + testApp(t, "txn Sender; balance; int 0; ==;", ep) + testApp(t, "txn Sender; txn Accounts 1; int 100"+payAndUnkey, ep, "unauthorized") + ledger.NewAccount(ep.Txn.Txn.Sender, 120+3*ep.Proto.MinTxnFee) + ledger.Rekey(ep.Txn.Txn.Sender, basics.AppIndex(888).Address()) + testApp(t, "txn Sender; txn Accounts 1; int 100"+payAndUnkey+"; int 1", ep) + // now rekeyed back to original + testApp(t, "txn Sender; txn Accounts 1; int 100"+payAndUnkey, ep, "unauthorized") +} + func TestDefaultSender(t *testing.T) { pay := ` itxn_begin @@ -320,7 +353,7 @@ func TestExtraFields(t *testing.T) { "non-zero fields for type axfer") } -func TestBadField(t *testing.T) { +func TestBadFieldV5(t *testing.T) { pay := ` itxn_begin int 7; itxn_field AssetAmount; @@ -334,12 +367,34 @@ func TestBadField(t *testing.T) { itxn_submit ` - ep, ledger := makeSampleEnv() + ep, ledger := makeSampleEnvWithVersion(5) ledger.NewApp(ep.Txn.Txn.Receiver, 888, basics.AppParams{}) testApp(t, "global CurrentApplicationAddress; txn Accounts 1; int 100"+pay, ep, "invalid itxn_field RekeyTo") } +func TestBadField(t *testing.T) { + pay := ` + itxn_begin + int 7; itxn_field AssetAmount; + itxn_field Amount + itxn_field Receiver + itxn_field Sender + int pay + itxn_field TypeEnum + txn Receiver + itxn_field RekeyTo // ALLOWED, since v6 + int 10 + itxn_field FirstValid // NOT ALLOWED + itxn_submit +` + + ep, ledger := makeSampleEnv() + ledger.NewApp(ep.Txn.Txn.Receiver, 888, basics.AppParams{}) + testApp(t, "global CurrentApplicationAddress; txn Accounts 1; int 100"+pay, ep, + "invalid itxn_field FirstValid") +} + func TestNumInner(t *testing.T) { pay := ` itxn_begin @@ -434,3 +489,32 @@ func TestAssetFreeze(t *testing.T) { require.NoError(t, err) require.Equal(t, false, holding.Frozen) } + +func TestFieldSetting(t *testing.T) { + ep, ledger := makeSampleEnv() + ledger.NewApp(ep.Txn.Txn.Receiver, 888, basics.AppParams{}) + ledger.NewAccount(ledger.ApplicationID().Address(), 10*defaultEvalProto().MinTxnFee) + testApp(t, "itxn_begin; int 500; bzero; itxn_field Note; int 1", ep) + testApp(t, "itxn_begin; int 501; bzero; itxn_field Note; int 1", ep, + "Note may not exceed") + + testApp(t, "itxn_begin; int 32; bzero; itxn_field VotePK; int 1", ep) + testApp(t, "itxn_begin; int 31; bzero; itxn_field VotePK; int 1", ep, + "VotePK must be 32") + + testApp(t, "itxn_begin; int 32; bzero; itxn_field SelectionPK; int 1", ep) + testApp(t, "itxn_begin; int 33; bzero; itxn_field SelectionPK; int 1", ep, + "SelectionPK must be 32") + + testApp(t, "itxn_begin; int 32; bzero; itxn_field RekeyTo; int 1", ep) + testApp(t, "itxn_begin; int 31; bzero; itxn_field RekeyTo; int 1", ep, + "not an address") + + testApp(t, "itxn_begin; int 6; bzero; itxn_field ConfigAssetUnitName; int 1", ep) + testApp(t, "itxn_begin; int 7; bzero; itxn_field ConfigAssetUnitName; int 1", ep, + "value is too long") + + testApp(t, "itxn_begin; int 12; bzero; itxn_field ConfigAssetName; int 1", ep) + testApp(t, "itxn_begin; int 13; bzero; itxn_field ConfigAssetName; int 1", ep, + "value is too long") +} diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index f270bd5414..5a212629c6 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -2493,7 +2493,7 @@ func TestPooledAppCallsVerifyOp(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - source := `#pragma version 5 + source := ` global CurrentApplicationID pop byte 0x01 diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 3e1fe56af3..3add52a810 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -73,6 +73,8 @@ func defaultEvalProtoWithVersion(version uint64) config.ConsensusParams { MaxAssetUnitNameBytes: 6, MaxAssetURLBytes: 32, MaxAssetDecimals: 4, + SupportRekeying: true, + MaxTxnNoteBytes: 500, } } @@ -999,6 +1001,10 @@ byte 0x0706000000000000000000000000000000000000000000000000000000000000 && ` +const globalV6TestProgram = globalV5TestProgram + ` +// No new globals in v6 +` + func TestGlobal(t *testing.T) { partitiontest.PartitionTest(t) @@ -1028,6 +1034,10 @@ func TestGlobal(t *testing.T) { GroupID, globalV5TestProgram, EvalStateful, CheckStateful, }, + 6: { + GroupID, globalV6TestProgram, + EvalStateful, CheckStateful, + }, } // tests keys are versions so they must be in a range 1..AssemblerMaxVersion plus zero version require.LessOrEqual(t, len(tests), AssemblerMaxVersion+1) @@ -1439,6 +1449,12 @@ assert int 1 ` +const testTxnProgramTextV6 = testTxnProgramTextV5 + ` +assert + +int 1 +` + func makeSampleTxn() transactions.SignedTxn { var txn transactions.SignedTxn copy(txn.Txn.Sender[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui00")) @@ -1526,7 +1542,7 @@ func TestTxn(t *testing.T) { t.Parallel() for i, txnField := range TxnFieldNames { fs := txnFieldSpecByField[TxnField(i)] - if !fs.effects && !strings.Contains(testTxnProgramTextV5, txnField) { + if !fs.effects && !strings.Contains(testTxnProgramTextV6, txnField) { if txnField != FirstValidTime.String() { t.Errorf("TestTxn missing field %v", txnField) } @@ -1539,6 +1555,7 @@ func TestTxn(t *testing.T) { 3: testTxnProgramTextV3, 4: testTxnProgramTextV4, 5: testTxnProgramTextV5, + 6: testTxnProgramTextV6, } clearOps := testProg(t, "int 1", 1) @@ -1823,11 +1840,15 @@ gtxn 0 Sender && ` + gtxnTextV6 := gtxnTextV5 + ` +` + tests := map[uint64]string{ 1: gtxnTextV1, 2: gtxnTextV2, 4: gtxnTextV4, 5: gtxnTextV5, + 6: gtxnTextV6, } for v, source := range tests { diff --git a/data/transactions/logic/fields.go b/data/transactions/logic/fields.go index 32d29fd85c..0a6cb9c96d 100644 --- a/data/transactions/logic/fields.go +++ b/data/transactions/logic/fields.go @@ -197,16 +197,16 @@ var txnFieldSpecs = []txnFieldSpec{ {FirstValid, StackUint64, 0, 0, false}, {FirstValidTime, StackUint64, 0, 0, false}, {LastValid, StackUint64, 0, 0, false}, - {Note, StackBytes, 0, 0, false}, + {Note, StackBytes, 0, 6, false}, {Lease, StackBytes, 0, 0, false}, {Receiver, StackBytes, 0, 5, false}, {Amount, StackUint64, 0, 5, false}, {CloseRemainderTo, StackBytes, 0, 5, false}, - {VotePK, StackBytes, 0, 0, false}, - {SelectionPK, StackBytes, 0, 0, false}, - {VoteFirst, StackUint64, 0, 0, false}, - {VoteLast, StackUint64, 0, 0, false}, - {VoteKeyDilution, StackUint64, 0, 0, false}, + {VotePK, StackBytes, 0, 6, false}, + {SelectionPK, StackBytes, 0, 6, false}, + {VoteFirst, StackUint64, 0, 6, false}, + {VoteLast, StackUint64, 0, 6, false}, + {VoteKeyDilution, StackUint64, 0, 6, false}, {Type, StackBytes, 0, 5, false}, {TypeEnum, StackUint64, 0, 5, false}, {XferAsset, StackUint64, 0, 5, false}, @@ -224,7 +224,7 @@ var txnFieldSpecs = []txnFieldSpec{ {NumAccounts, StackUint64, 2, 0, false}, {ApprovalProgram, StackBytes, 2, 0, false}, {ClearStateProgram, StackBytes, 2, 0, false}, - {RekeyTo, StackBytes, 2, 0, false}, + {RekeyTo, StackBytes, 2, 6, false}, {ConfigAsset, StackUint64, 2, 5, false}, {ConfigAssetTotal, StackUint64, 2, 5, false}, {ConfigAssetDecimals, StackUint64, 2, 5, false}, @@ -249,7 +249,7 @@ var txnFieldSpecs = []txnFieldSpec{ {LocalNumUint, StackUint64, 3, 0, false}, {LocalNumByteSlice, StackUint64, 3, 0, false}, {ExtraProgramPages, StackUint64, 4, 0, false}, - {Nonparticipation, StackUint64, 5, 0, false}, + {Nonparticipation, StackUint64, 5, 6, false}, {Logs, StackBytes, 5, 5, true}, {NumLogs, StackUint64, 5, 5, true}, @@ -279,11 +279,12 @@ var txnaFieldSpecByField = map[TxnField]txnFieldSpec{ Logs: {Logs, StackBytes, 5, 5, true}, } -var innerTxnTypes = map[string]protocol.TxType{ - string(protocol.PaymentTx): protocol.PaymentTx, - string(protocol.AssetTransferTx): protocol.AssetTransferTx, - string(protocol.AssetConfigTx): protocol.AssetConfigTx, - string(protocol.AssetFreezeTx): protocol.AssetFreezeTx, +var innerTxnTypes = map[string]uint64{ + string(protocol.PaymentTx): 5, + string(protocol.KeyRegistrationTx): 6, + string(protocol.AssetTransferTx): 5, + string(protocol.AssetConfigTx): 5, + string(protocol.AssetFreezeTx): 5, } // TxnTypeNames is the values of Txn.Type in enum order diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index d45dd25fba..83e608b934 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -21,7 +21,7 @@ import ( ) // LogicVersion defines default assembler and max eval versions -const LogicVersion = 5 +const LogicVersion = 6 // rekeyingEnabledVersion is the version of TEAL where RekeyTo functionality // was enabled. This is important to remember so that old TEAL accounts cannot diff --git a/data/transactions/logictest/ledger.go b/data/transactions/logictest/ledger.go index 5d75b210fc..51ed941246 100644 --- a/data/transactions/logictest/ledger.go +++ b/data/transactions/logictest/ledger.go @@ -549,6 +549,23 @@ func (l *Ledger) move(from basics.Address, to basics.Address, amount uint64) err return nil } +func (l *Ledger) rekey(tx *transactions.Transaction) error { + // rekeying: update br.auth to tx.RekeyTo if provided + if (tx.RekeyTo != basics.Address{}) { + br, ok := l.balances[tx.Sender] + if !ok { + return fmt.Errorf("no account") + } + if tx.RekeyTo == tx.Sender { + br.auth = basics.Address{} + } else { + br.auth = tx.RekeyTo + } + l.balances[tx.Sender] = br + } + return nil +} + func (l *Ledger) pay(from basics.Address, pay transactions.PaymentTxnFields) error { err := l.move(from, pay.Receiver, pay.Amount.Raw) if err != nil { @@ -706,6 +723,12 @@ func (l *Ledger) Perform(txn *transactions.Transaction, spec transactions.Specia if err != nil { return ad, err } + + err = l.rekey(txn) + if err != nil { + return ad, err + } + switch txn.Type { case protocol.PaymentTx: err = l.pay(txn.Sender, txn.PaymentTxnFields) diff --git a/ledger/applications.go b/ledger/applications.go index 9b3cb27b47..5d4e8c3b7d 100644 --- a/ledger/applications.go +++ b/ledger/applications.go @@ -273,6 +273,11 @@ func (al *logicLedger) Perform(tx *transactions.Transaction, spec transactions.S return ad, err } + err = apply.Rekey(balances, tx) + if err != nil { + return ad, err + } + // compared to eval.transaction() it may seem strange that we // increment the transaction count *before* transaction // processing, rather than after. But we need to account for the @@ -287,12 +292,18 @@ func (al *logicLedger) Perform(tx *transactions.Transaction, spec transactions.S switch tx.Type { case protocol.PaymentTx: err = apply.Payment(tx.PaymentTxnFields, tx.Header, balances, spec, &ad) - case protocol.AssetTransferTx: - err = apply.AssetTransfer(tx.AssetTransferTxnFields, tx.Header, balances, spec, &ad) + + case protocol.KeyRegistrationTx: + err = apply.Keyreg(tx.KeyregTxnFields, tx.Header, balances, spec, &ad, al.Round()) + case protocol.AssetConfigTx: err = apply.AssetConfig(tx.AssetConfigTxnFields, tx.Header, balances, spec, &ad, al.cow.txnCounter()) + + case protocol.AssetTransferTx: + err = apply.AssetTransfer(tx.AssetTransferTxnFields, tx.Header, balances, spec, &ad) case protocol.AssetFreezeTx: err = apply.AssetFreeze(tx.AssetFreezeTxnFields, tx.Header, balances, spec, &ad) + default: err = fmt.Errorf("%s tx in AVM", tx.Type) } diff --git a/ledger/apply/apply.go b/ledger/apply/apply.go index 988ae8cb90..f6a91ed8e2 100644 --- a/ledger/apply/apply.go +++ b/ledger/apply/apply.go @@ -64,3 +64,26 @@ type Balances interface { // to a ConsensusParams. This returns those parameters. ConsensusParams() config.ConsensusParams } + +// Rekey updates tx.Sender's AuthAddr to tx.RekeyTo, if provided +func Rekey(balances Balances, tx *transactions.Transaction) error { + if (tx.RekeyTo != basics.Address{}) { + acct, err := balances.Get(tx.Sender, false) + if err != nil { + return err + } + // Special case: rekeying to the account's actual address just sets acct.AuthAddr to 0 + // This saves 32 bytes in your balance record if you want to go back to using your original key + if tx.RekeyTo == tx.Sender { + acct.AuthAddr = basics.Address{} + } else { + acct.AuthAddr = tx.RekeyTo + } + + err = balances.Put(tx.Sender, acct) + if err != nil { + return err + } + } + return nil +} diff --git a/ledger/apptxn_test.go b/ledger/apptxn_test.go index 5527b385ec..800668af91 100644 --- a/ledger/apptxn_test.go +++ b/ledger/apptxn_test.go @@ -984,3 +984,202 @@ func TestAsaDuringInit(t *testing.T) { asaIndex := vb.blk.Payset[1].EvalDelta.InnerTxns[0].ConfigAsset require.Equal(t, basics.AssetIndex(3), asaIndex) } + +func TestRekey(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := newTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int pay + itxn_field TypeEnum + int 1 + itxn_field Amount + global CurrentApplicationAddress + itxn_field Receiver + int 31 + bzero + byte 0x01 + concat + itxn_field RekeyTo + itxn_submit +`), + } + + eval := l.nextBlock(t) + eval.txns(t, &app) + vb := l.endBlock(t, eval) + appIndex := vb.blk.Payset[0].ApplicationID + require.Equal(t, basics.AppIndex(1), appIndex) + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 1_000_000, + } + rekey := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + } + eval = l.nextBlock(t) + eval.txns(t, &fund, &rekey) + eval.txn(t, rekey.Noted("2"), "unauthorized") + l.endBlock(t, eval) + +} + +func TestNote(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := newTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int pay + itxn_field TypeEnum + int 0 + itxn_field Amount + global CurrentApplicationAddress + itxn_field Receiver + byte "abcdefghijklmnopqrstuvwxyz01234567890" + itxn_field Note + itxn_submit +`), + } + + eval := l.nextBlock(t) + eval.txns(t, &app) + vb := l.endBlock(t, eval) + appIndex := vb.blk.Payset[0].ApplicationID + require.Equal(t, basics.AppIndex(1), appIndex) + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 1_000_000, + } + note := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + } + eval = l.nextBlock(t) + eval.txns(t, &fund, ¬e) + vb = l.endBlock(t, eval) + alphabet := vb.blk.Payset[1].EvalDelta.InnerTxns[0].Txn.Note + require.Equal(t, "abcdefghijklmnopqrstuvwxyz01234567890", string(alphabet)) +} + +func TestKeyreg(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := newTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + txn ApplicationArgs 0 + byte "pay" + == + bz nonpart + itxn_begin + int pay + itxn_field TypeEnum + int 1 + itxn_field Amount + txn Sender + itxn_field Receiver + itxn_submit + int 1 + return +nonpart: + itxn_begin + int keyreg + itxn_field TypeEnum + int 1 + itxn_field Nonparticipation + itxn_submit +`), + } + + // Create the app + eval := l.nextBlock(t) + eval.txns(t, &app) + vb := l.endBlock(t, eval) + appIndex := vb.blk.Payset[0].ApplicationID + require.Equal(t, basics.AppIndex(1), appIndex) + + // Give the app a lot of money + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 1_000_000_000, + } + eval = l.nextBlock(t) + eval.txn(t, &fund) + vb = l.endBlock(t, eval) + + require.Equal(t, 1_000_000_000, int(l.micros(t, appIndex.Address()))) + + // Build up Residue in RewardsState so it's ready to pay + for i := 1; i < 10; i++ { + eval := l.nextBlock(t) + l.endBlock(t, eval) + } + + // pay a little + pay := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: appIndex, + ApplicationArgs: [][]byte{[]byte("pay")}, + } + eval = l.nextBlock(t) + eval.txn(t, &pay) + l.endBlock(t, eval) + // 2000 was earned in rewards (- 1000 fee, -1 pay) + require.Equal(t, 1_000_000_999, int(l.micros(t, appIndex.Address()))) + + // Go nonpart + nonpart := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: appIndex, + ApplicationArgs: [][]byte{[]byte("nonpart")}, + } + eval = l.nextBlock(t) + eval.txn(t, &nonpart) + l.endBlock(t, eval) + require.Equal(t, 999_999_999, int(l.micros(t, appIndex.Address()))) + + // Build up Residue in RewardsState so it's ready to pay AGAIN + // But expect no rewards + for i := 1; i < 100; i++ { + eval := l.nextBlock(t) + l.endBlock(t, eval) + } + eval = l.nextBlock(t) + eval.txn(t, pay.Noted("again")) + eval.txn(t, nonpart.Noted("again"), "cannot change online/offline") + l.endBlock(t, eval) + // Ppaid fee and 1. Did not get rewards + require.Equal(t, 999_998_998, int(l.micros(t, appIndex.Address()))) +} diff --git a/ledger/eval.go b/ledger/eval.go index 091a1d6ba2..1fab65242a 100644 --- a/ledger/eval.go +++ b/ledger/eval.go @@ -955,25 +955,9 @@ func (eval *BlockEvaluator) applyTransaction(tx transactions.Transaction, balanc return } - // rekeying: update balrecord.AuthAddr to tx.RekeyTo if provided - if (tx.RekeyTo != basics.Address{}) { - var acct basics.AccountData - acct, err = balances.Get(tx.Sender, false) - if err != nil { - return - } - // Special case: rekeying to the account's actual address just sets acct.AuthAddr to 0 - // This saves 32 bytes in your balance record if you want to go back to using your original key - if tx.RekeyTo == tx.Sender { - acct.AuthAddr = basics.Address{} - } else { - acct.AuthAddr = tx.RekeyTo - } - - err = balances.Put(tx.Sender, acct) - if err != nil { - return - } + err = apply.Rekey(balances, &tx) + if err != nil { + return } switch tx.Type { diff --git a/test/scripts/e2e_subs/app-rekey.py b/test/scripts/e2e_subs/app-rekey.py new file mode 100755 index 0000000000..189f7eed34 --- /dev/null +++ b/test/scripts/e2e_subs/app-rekey.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python + +import os +import sys +from goal import Goal + +from datetime import datetime + +stamp = datetime.now().strftime("%Y%m%d_%H%M%S") +print(f"{os.path.basename(sys.argv[0])} start {stamp}") + +goal = Goal(sys.argv[1], autosend=True) + +joe = goal.new_account() +flo = goal.new_account() + +txinfo, err = goal.pay(goal.account, joe, amt=500_000) +assert not err, err + +# Turn off rewards for precise balance checking +txinfo, err = goal.keyreg(joe, nonpart=True) +assert not err, err +joeb = goal.balance(joe) +print(joeb) + +txinfo, err = goal.pay(goal.account, flo, amt=500_000) +assert not err, err + +teal = """ +#pragma version 6 + txn ApplicationID + bz end + // Use the rekeyed account to make a payment, and give it back + itxn_begin + int pay + itxn_field TypeEnum + + txn Accounts 1 + itxn_field Sender + + txn Accounts 0 + itxn_field Receiver + + int 5 + itxn_field Amount + + txn Accounts 1 + itxn_field RekeyTo + itxn_submit + +end: + int 1 +""" + +txinfo, err = goal.app_create(joe, goal.assemble(teal)) +assert not err, err +joeb = joeb-1000 +app_id = txinfo['application-index'] +assert app_id + +app_addr = goal.app_address(app_id) +# flo rekeys her account to the app, app spends from it, then rekeys it back +txinfo, err = goal.pay(flo, joe, amt=1, rekey_to=app_addr) +assert not err, err +assert goal.balance(joe) == joeb+1, goal.balance(joe) + +# can no longer spend +txinfo, err = goal.pay(flo, joe, amt=1) +assert err +assert goal.balance(joe) == joeb+1, goal.balance(joe) + +txinfo, err = goal.app_call(joe, app_id, accounts=[flo]) +assert not err, err +joeb = joeb-1000 +assert goal.balance(joe) == joeb+6, goal.balance(joe) + +# can spend again +txinfo, err = goal.pay(flo, joe, amt=1) +assert not err, err +assert goal.balance(joe) == joeb+7, goal.balance(joe) + +print(f"{os.path.basename(sys.argv[0])} OK {stamp}") diff --git a/test/scripts/e2e_subs/example.py b/test/scripts/e2e_subs/example.py index 4685f57391..cc7acda2c1 100755 --- a/test/scripts/e2e_subs/example.py +++ b/test/scripts/e2e_subs/example.py @@ -5,6 +5,10 @@ from goal import Goal import algosdk.future.transaction as txn +from datetime import datetime + +stamp = datetime.now().strftime("%Y%m%d_%H%M%S") +print(f"{os.path.basename(sys.argv[0])} start {stamp}") goal = Goal(sys.argv[1]) @@ -13,8 +17,8 @@ # Pays -pay = goal.pay(goal.account, receiver=joe, amt=10000) -txid, err = goal.send(pay) +pay = goal.pay(goal.account, receiver=joe, amt=10000) # under min balance +txid, err = goal.send(pay, confirm=False) # errors early assert err pay = goal.pay(goal.account, receiver=joe, amt=500_000) @@ -65,7 +69,7 @@ # App create teal = "test/scripts/e2e_subs/tealprogs" approval = goal.assemble(os.path.join(teal, "app-escrow.teal")) -yes = goal.assemble("#pragma version 2\nint 28") # 28 is just to uniquify +yes = goal.assemble("#pragma version 2\nint 28") # 28 is just to uniquify create = goal.appl(flo, 0, local_schema=(1, 0), global_schema=(0, 4), @@ -118,3 +122,6 @@ txinfo, err = goal.send(spend) assert not err, err assert goal.balance(goal.logic_address(yes)) == 107_000, goal.balance(goal.logic_address(yes)) + +stamp = datetime.now().strftime("%Y%m%d_%H%M%S") +print(f"{os.path.basename(sys.argv[0])} OK {stamp}") diff --git a/test/scripts/e2e_subs/goal/goal.py b/test/scripts/e2e_subs/goal/goal.py index 4982d06ca2..921d2c3ea2 100755 --- a/test/scripts/e2e_subs/goal/goal.py +++ b/test/scripts/e2e_subs/goal/goal.py @@ -56,6 +56,7 @@ def __init__( algod_address=None, kmd_token=None, kmd_address=None, + autosend=None, ): self.algod = None self.kmd = None @@ -84,6 +85,8 @@ def __init__( # txns easily, even without kmd. self.internal_wallet = {} + self.autosend = autosend + def open_algod(self, algodata, algod_address=None): if algod_address: algod_token = algodata @@ -197,12 +200,12 @@ def wait_for_block(self, block): """ print(f"Waiting for block {block}.") s = self.algod.status() - last_round = s['last-round'] + last_round = s["last-round"] while last_round < block: wait_block = min(block, last_round + 3) print(f" waiting for {last_round}...") s = self.algod.status_after_block(wait_block) - last_round = s['last-round'] + last_round = s["last-round"] return s def new_account(self): @@ -215,31 +218,53 @@ def add_account(self, address, key): assert len(key) == 88, key self.internal_wallet[address] = key - def pay(self, sender, receiver, amt: int, **kwargs): + def finish(self, tx, send): + if send is None: + send = self.autosend + if send: + return self.send(tx, confirm=True) + return tx + + def keyreg(self, sender, votekey=None, selkey=None, votefst=None, + votelst=None, votekd=None, + send=None, **kwargs): params = self.algod.suggested_params() - return txn.PaymentTxn(sender, params, receiver, amt, **kwargs) + tx = txn.KeyregTxn(sender, params, + votekey, selkey, votefst, votelst, votekd, + **kwargs) + return self.finish(tx, send) - def acfg(self, sender, **kwargs): + def pay(self, sender, receiver, amt: int, send=None, **kwargs): params = self.algod.suggested_params() - return txn.AssetConfigTxn( + tx = txn.PaymentTxn(sender, params, receiver, amt, **kwargs) + return self.finish(tx, send) + + def acfg(self, sender, send=None, **kwargs): + params = self.algod.suggested_params() + tx = txn.AssetConfigTxn( sender, params, **kwargs, strict_empty_address_check=False ) + return self.finish(tx, send) def asset_create(self, sender, **kwargs): assert not kwargs.pop("index", None) return self.acfg(sender, **kwargs) - def axfer(self, sender, receiver, amt: int, index: int, **kwargs): + def axfer(self, sender, receiver, amt: int, index: int, send=None, **kwargs): params = self.algod.suggested_params() - return txn.AssetTransferTxn(sender, params, receiver, amt, index, **kwargs) + tx = txn.AssetTransferTxn( + sender, params, receiver, amt, index, **kwargs + ) + return self.finish(tx, send) def asset_optin(self, sender, index: int, **kwargs): assert not kwargs.pop("receiver", None) return self.axfer(sender, sender, 0, index, **kwargs) - def afrz(self, sender, index: int, target, frozen, **kwargs): + def afrz(self, sender, index: int, target, frozen, send=None, **kwargs): params = self.algod.suggested_params() - return txn.AssetFreezeTxn(sender, params, index, target, frozen, **kwargs) + tx = txn.AssetFreezeTxn(sender, params, index, target, frozen, **kwargs) + return self.finish(tx, send) def coerce_schema(self, values): if not values: @@ -248,11 +273,12 @@ def coerce_schema(self, values): return values return txn.StateSchema(num_uints=values[0], num_byte_slices=values[1]) - def appl(self, sender, index: int, on_complete=txn.OnComplete.NoOpOC, **kwargs): + def appl(self, sender, index: int, on_complete=txn.OnComplete.NoOpOC, + send=None, **kwargs): params = self.algod.suggested_params() local_schema = self.coerce_schema(kwargs.pop("local_schema", None)) global_schema = self.coerce_schema(kwargs.pop("global_schema", None)) - return txn.ApplicationCallTxn( + tx = txn.ApplicationCallTxn( sender, params, index, @@ -261,6 +287,7 @@ def appl(self, sender, index: int, on_complete=txn.OnComplete.NoOpOC, **kwargs): global_schema=global_schema, **kwargs, ) + return self.finish(tx, send) def app_create( self, @@ -326,7 +353,7 @@ def assemble(self, source): def assemble_with_rest(self, source): compile_response = self.algod.compile(source) - return base64.b64decode(compile_response['result']) + return base64.b64decode(compile_response["result"]) def app_info(self, index: int) -> dict: return self.algod.application_info(index)["params"]