From 2a34ac898dcbc7ad1cb0ca46b5ee33355a4f605d Mon Sep 17 00:00:00 2001 From: giulio Date: Sat, 14 Sep 2024 09:35:19 +0200 Subject: [PATCH] Add integration test for lsig size pooling --- data/transactions/verify/txn.go | 6 +- .../cli/goal/expect/tealConsensusTest.exp | 25 --- .../features/transactions/logicsig_test.go | 167 ++++++++++++++++++ .../nettemplates/TwoNodes50EachV18.json | 36 ++++ 4 files changed, 205 insertions(+), 29 deletions(-) create mode 100644 test/e2e-go/features/transactions/logicsig_test.go create mode 100644 test/testdata/nettemplates/TwoNodes50EachV18.json diff --git a/data/transactions/verify/txn.go b/data/transactions/verify/txn.go index eb08ddec5e..ab3a339219 100644 --- a/data/transactions/verify/txn.go +++ b/data/transactions/verify/txn.go @@ -231,13 +231,11 @@ func txnGroupBatchPrep(stxs []transactions.SignedTxn, contextHdr *bookkeeping.Bl if groupCtx.consensusParams.EnableLogicSigSizePooling { lSigMaxPooledSize := len(stxs) * int(groupCtx.consensusParams.LogicSigMaxSize) if lSigPooledSize > lSigMaxPooledSize { - errorMsg := fmt.Sprintf( + errorMsg := fmt.Errorf( "txgroup had %d bytes of LogicSigs, more than the available pool of %d bytes", lSigPooledSize, lSigMaxPooledSize, ) - err = &TxGroupError{err: errors.New(errorMsg), GroupIndex: -1, Reason: TxGroupErrorReasonNotWellFormed} - return nil, err - } + return nil, &TxGroupError{err: errorMsg, GroupIndex: -1, Reason: TxGroupErrorReasonNotWellFormed} } feeNeeded, overflow := basics.OMul(groupCtx.consensusParams.MinTxnFee, minFeeCount) if overflow { diff --git a/test/e2e-go/cli/goal/expect/tealConsensusTest.exp b/test/e2e-go/cli/goal/expect/tealConsensusTest.exp index a6abb862d4..9df3d4abe9 100644 --- a/test/e2e-go/cli/goal/expect/tealConsensusTest.exp +++ b/test/e2e-go/cli/goal/expect/tealConsensusTest.exp @@ -54,18 +54,6 @@ if { [catch { "\n" { ::AlgorandGoal::Abort $expect_out(buffer) } } - # we do not test anymore for a max lsig size because we let goal compile any program size, - # even if they will be rejected by the nodes - - #teal "$TEST_ROOT_DIR/big-sig.teal" 2 16001 1 - #spawn goal clerk compile "$TEST_ROOT_DIR/big-sig.teal" - #expect { - # -re {[A-Z2-9]{58}} { ::AlgorandGoal::Abort "hash" } - # -re {.*logicsig program size too large} { puts "bigsigcheck: pass" } - # eof { ::AlgorandGoal::Abort $expect_out(buffer) } - # "\n" { ::AlgorandGoal::Abort $expect_out(buffer) } - #} - teal "$TEST_ROOT_DIR/barely-fits-app.teal" 2 4090 1 "int 0\nbalance\npop\n" spawn goal clerk compile "$TEST_ROOT_DIR/barely-fits-app.teal" expect { @@ -74,19 +62,6 @@ if { [catch { "\n" { ::AlgorandGoal::Abort $expect_out(buffer) } } - # we do not test anymore for a max app program size because we let goal compile any program size, - # even if they will be rejected by the nodes - - # MaxAppProgramLen = 2K * (1 + 3 pages max) - #teal "$TEST_ROOT_DIR/big-app.teal" 2 8192 1 "int 0\nbalance\npop\n" - #spawn goal clerk compile "$TEST_ROOT_DIR/big-app.teal" - #expect { - # -re {[A-Z2-9]{58}} { ::AlgorandGoal::Abort "hash" } - # -re {.*app program size too large} { puts "bigappcheck: pass" } - # eof { ::AlgorandGoal::Abort $expect_out(buffer) } - # "\n" { ::AlgorandGoal::Abort $expect_out(buffer) } - #} - # Test cost limits during dryrun exec goal clerk send -F "$TEST_ROOT_DIR/small-sig.teal" -t GXBNLU4AXQABPLHXJDMTG2YXSDT4EWUZACT7KTPFXDQW52XPTIUS5OZ5HQ -a 100 -d $TEST_PRIMARY_NODE_DIR -o $TEST_ROOT_DIR/small-sig.tx spawn goal clerk dryrun -t $TEST_ROOT_DIR/small-sig.tx diff --git a/test/e2e-go/features/transactions/logicsig_test.go b/test/e2e-go/features/transactions/logicsig_test.go new file mode 100644 index 0000000000..087fed198d --- /dev/null +++ b/test/e2e-go/features/transactions/logicsig_test.go @@ -0,0 +1,167 @@ +// Copyright (C) 2019-2024 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package transactions + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/test/framework/fixtures" + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/stretchr/testify/require" +) + +// CreateTealOfSize return a TEAL bytecode of `size` bytes which always succeeds. +// `size` must be at least 9 bytes +func CreateTealOfSize(size uint, pragma uint) ([]byte, error) { + if size < 9 { + return nil, fmt.Errorf("size must be at least 9 bytes; got %d", size) + } + ls := fmt.Sprintf("#pragma version %d\n", pragma) + if size%2 == 0 { + ls += "int 10\npop\nint 1\npop\n" + } else { + ls += "int 1\npop\nint 1\npop\n" + } + for i := uint(11); i <= size; i += 2 { + ls = ls + "int 1\npop\n" + } + ls = ls + "int 1" + code, err := logic.AssembleString(ls) + if err != nil { + return nil, err + } + // panic if the function is not working as expected and needs to be updated + if len(code.Program) != int(size) { + panic(fmt.Sprintf("wanted to create a program of size %d but got a program of size %d", + size, len(code.Program))) + } + return code.Program, nil +} + +func TestLogicSigSizeBeforePooling(t *testing.T) { + partitiontest.PartitionTest(t) + defer fixtures.ShutdownSynchronizedTest(t) + a := require.New(fixtures.SynchronizedTest(t)) + + // From consensus version 18, we have lsigs with a maximum size of 1000 bytes. + // We need to use pragma 1 for teal in v18 + pragma := uint(1) + tealOK, err := CreateTealOfSize(1000, pragma) + a.NoError(err) + tealTooLong, err := CreateTealOfSize(1001, pragma) + a.NoError(err) + + testLogicSize(t, tealOK, tealTooLong, filepath.Join("nettemplates", "TwoNodes50EachV18.json")) +} + +func TestLogicSigSizeAfterPooling(t *testing.T) { + partitiontest.PartitionTest(t) + defer fixtures.ShutdownSynchronizedTest(t) + a := require.New(fixtures.SynchronizedTest(t)) + + pragma := uint(1) + tealOK, err := CreateTealOfSize(2000, pragma) + a.NoError(err) + tealTooLong, err := CreateTealOfSize(2001, pragma) + a.NoError(err) + + // TODO: Update this when lsig pooling graduates from vFuture + testLogicSize(t, tealOK, tealTooLong, filepath.Join("nettemplates", "TwoNodes50EachFuture.json")) +} + +// testLogicSize takes two TEAL programs, one expected to be ok and one expected to be too long +// and thus to be rejected, and tests that's indeed the case. +func testLogicSize(t *testing.T, tealOK, tealTooLong []byte, + networkTemplate string) { + + t.Parallel() + a := require.New(fixtures.SynchronizedTest(t)) + + var fixture fixtures.RestClientFixture + fixture.Setup(t, networkTemplate) + defer fixture.Shutdown() + + client := fixture.LibGoalClient + accountList, err := fixture.GetWalletsSortedByBalance() + a.NoError(err) + baseAcct := accountList[0].Address + + walletHandler, err := client.GetUnencryptedWalletHandle() + a.NoError(err) + + signatureOK, err := client.SignProgramWithWallet(walletHandler, nil, baseAcct, tealOK) + a.NoError(err) + lsigOK := transactions.LogicSig{Logic: tealOK, Sig: signatureOK} + + signatureTooLong, err := client.SignProgramWithWallet(walletHandler, nil, baseAcct, tealTooLong) + a.NoError(err) + lsigTooLong := transactions.LogicSig{Logic: tealTooLong, Sig: signatureTooLong} + + // We build two transaction groups of two transactions each. + // The first txn will be either signed by an ok lsig or a too long one. + // The second is a vanilla payment transaction to complete the group. + var txn1Success, txn2Success, txn1Fail, txn2Fail transactions.Transaction + for i, txn := range []*transactions.Transaction{&txn1Success, &txn2Success, &txn1Fail, &txn2Fail} { + *txn, err = client.ConstructPayment(baseAcct, baseAcct, 0, uint64(i), nil, "", [32]byte{}, 0, 0) + a.NoError(err) + } + + // success group + gidSuccess, err := client.GroupID([]transactions.Transaction{txn1Success, txn2Success}) + a.NoError(err) + + txn1Success.Group = gidSuccess + stxn1Success := transactions.SignedTxn{Txn: txn1Success, Lsig: lsigOK} + + txn2Success.Group = gidSuccess + stxn2Success, err := client.SignTransactionWithWallet(walletHandler, nil, txn2Success) + a.NoError(err) + + err = client.BroadcastTransactionGroup([]transactions.SignedTxn{stxn1Success, stxn2Success}) + a.NoError(err) + + // fail group + gidFail, err := client.GroupID([]transactions.Transaction{txn1Fail, txn2Fail}) + a.NoError(err) + + txn1Fail.Group = gidFail + stxn1Fail := transactions.SignedTxn{Txn: txn1Fail, Lsig: lsigTooLong} + + txn2Fail.Group = gidFail + stxn2Fail, err := client.SignTransactionWithWallet(walletHandler, nil, txn2Fail) + a.NoError(err) + + cp, err := client.ConsensusParams(0) + a.NoError(err) + + err = client.BroadcastTransactionGroup([]transactions.SignedTxn{stxn1Fail, stxn2Fail}) + if cp.EnableLogicSigSizePooling { + a.Contains(err.Error(), "more than the available pool") + } else { + a.Contains(err.Error(), "LogicSig too long") + } + + // wait for the second transaction in the successful group to confirm + txn2SuccessId := txn2Success.ID().String() + _, curRound := fixture.GetBalanceAndRound(baseAcct) + confirmed := fixture.WaitForTxnConfirmation(curRound+5, txn2SuccessId) + a.True(confirmed) +} diff --git a/test/testdata/nettemplates/TwoNodes50EachV18.json b/test/testdata/nettemplates/TwoNodes50EachV18.json new file mode 100644 index 0000000000..198a4f0f99 --- /dev/null +++ b/test/testdata/nettemplates/TwoNodes50EachV18.json @@ -0,0 +1,36 @@ +{ + "Genesis": { + "NetworkName": "tbd", + "ConsensusProtocol": "https://github.com/algorandfoundation/specs/tree/6c6bd668be0ab14098e51b37e806c509f7b7e31f", + "LastPartKeyRound": 3000, + "Wallets": [ + { + "Name": "Wallet1", + "Stake": 50, + "Online": true + }, + { + "Name": "Wallet2", + "Stake": 50, + "Online": true + } + ] + }, + "Nodes": [ + { + "Name": "Primary", + "IsRelay": true, + "Wallets": [ + { "Name": "Wallet1", + "ParticipationOnly": false } + ] + }, + { + "Name": "Node", + "Wallets": [ + { "Name": "Wallet2", + "ParticipationOnly": false } + ] + } + ] +}