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

Goal: Introduce new command for simulate #5213

Merged
merged 12 commits into from
Mar 21, 2023
2 changes: 1 addition & 1 deletion cmd/goal/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,7 @@ var infoAppCmd = &cobra.Command{
}

// populateMethodCallTxnArgs parses and loads transactions from the files indicated by the values
// slice. An error will occur if the transaction does not matched the expected type, it has a nonzero
// slice. An error will occur if the transaction does not match the expected type, it has a nonzero
// group ID, or if it is signed by a normal signature or Msig signature (but not Lsig signature)
func populateMethodCallTxnArgs(types []string, values []string) ([]transactions.SignedTxn, error) {
loadedTxns := make([]transactions.SignedTxn, len(values))
Expand Down
58 changes: 45 additions & 13 deletions cmd/goal/clerk.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func init() {
clerkCmd.AddCommand(compileCmd)
clerkCmd.AddCommand(dryrunCmd)
clerkCmd.AddCommand(dryrunRemoteCmd)
clerkCmd.AddCommand(simulateCmd)

// Wallet to be used for the clerk operation
clerkCmd.PersistentFlags().StringVarP(&walletName, "wallet", "w", "", "Set the wallet to be used for the selected operation")
Expand All @@ -88,7 +89,7 @@ func init() {
sendCmd.Flags().StringVar(&rekeyToAddress, "rekey-to", "", "Rekey account to the given spending key/address. (Future transactions from this account will need to be signed with the new key.)")
sendCmd.Flags().StringVarP(&programSource, "from-program", "F", "", "Program source to use as account logic")
sendCmd.Flags().StringVarP(&progByteFile, "from-program-bytes", "P", "", "Program binary to use as account logic")
sendCmd.Flags().StringSliceVar(&argB64Strings, "argb64", nil, "base64 encoded args to pass to transaction logic")
sendCmd.Flags().StringSliceVar(&argB64Strings, "argb64", nil, "Base64 encoded args to pass to transaction logic")
sendCmd.Flags().StringVarP(&logicSigFile, "logic-sig", "L", "", "LogicSig to apply to transaction")
sendCmd.Flags().StringVar(&msigParams, "msig-params", "", "Multisig preimage parameters - [threshold] [Address 1] [Address 2] ...\nUsed to add the necessary fields in case the account was rekeyed to a multisig account")
sendCmd.MarkFlagRequired("to")
Expand All @@ -108,8 +109,8 @@ func init() {
signCmd.Flags().StringVarP(&signerAddress, "signer", "S", "", "Address of key to sign with, if different from transaction \"from\" address due to rekeying")
signCmd.Flags().StringVarP(&programSource, "program", "p", "", "Program source to use as account logic")
signCmd.Flags().StringVarP(&logicSigFile, "logic-sig", "L", "", "LogicSig to apply to transaction")
signCmd.Flags().StringSliceVar(&argB64Strings, "argb64", nil, "base64 encoded args to pass to transaction logic")
signCmd.Flags().StringVarP(&protoVersion, "proto", "P", "", "consensus protocol version id string")
signCmd.Flags().StringSliceVar(&argB64Strings, "argb64", nil, "Base64 encoded args to pass to transaction logic")
signCmd.Flags().StringVarP(&protoVersion, "proto", "P", "", "Consensus protocol version id string")
signCmd.MarkFlagRequired("infile")
signCmd.MarkFlagRequired("outfile")

Expand All @@ -123,26 +124,29 @@ func init() {
splitCmd.MarkFlagRequired("infile")
splitCmd.MarkFlagRequired("outfile")

compileCmd.Flags().BoolVarP(&disassemble, "disassemble", "D", false, "disassemble a compiled program")
compileCmd.Flags().BoolVarP(&noProgramOutput, "no-out", "n", false, "don't write contract program binary")
compileCmd.Flags().BoolVarP(&writeSourceMap, "map", "m", false, "write out source map")
compileCmd.Flags().BoolVarP(&signProgram, "sign", "s", false, "sign program, output is a binary signed LogicSig record")
compileCmd.Flags().BoolVarP(&disassemble, "disassemble", "D", false, "Disassemble a compiled program")
compileCmd.Flags().BoolVarP(&noProgramOutput, "no-out", "n", false, "Don't write contract program binary")
compileCmd.Flags().BoolVarP(&writeSourceMap, "map", "m", false, "Write out source map")
compileCmd.Flags().BoolVarP(&signProgram, "sign", "s", false, "Sign program, output is a binary signed LogicSig record")
compileCmd.Flags().StringVarP(&outFilename, "outfile", "o", "", "Filename to write program bytes or signed LogicSig to")
compileCmd.Flags().StringVarP(&account, "account", "a", "", "Account address to sign the program (If not specified, uses default account)")

dryrunCmd.Flags().StringVarP(&txFilename, "txfile", "t", "", "transaction or transaction-group to test")
dryrunCmd.Flags().StringVarP(&protoVersion, "proto", "P", "", "consensus protocol version id string")
dryrunCmd.Flags().StringVarP(&txFilename, "txfile", "t", "", "Transaction or transaction-group to test")
dryrunCmd.Flags().StringVarP(&protoVersion, "proto", "P", "", "Consensus protocol version id string")
dryrunCmd.Flags().BoolVar(&dumpForDryrun, "dryrun-dump", false, "Dump in dryrun format acceptable by dryrun REST api instead of running")
dryrunCmd.Flags().Var(&dumpForDryrunFormat, "dryrun-dump-format", "Dryrun dump format: "+dumpForDryrunFormat.AllowedString())
dryrunCmd.Flags().StringSliceVar(&dumpForDryrunAccts, "dryrun-accounts", nil, "additional accounts to include into dryrun request obj")
dryrunCmd.Flags().StringSliceVar(&dumpForDryrunAccts, "dryrun-accounts", nil, "Additional accounts to include into dryrun request obj")
dryrunCmd.Flags().StringVarP(&outFilename, "outfile", "o", "", "Filename for writing dryrun state object")
dryrunCmd.MarkFlagRequired("txfile")

dryrunRemoteCmd.Flags().StringVarP(&txFilename, "dryrun-state", "D", "", "dryrun request object to run")
dryrunRemoteCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "print more info")
dryrunRemoteCmd.Flags().BoolVarP(&rawOutput, "raw", "r", false, "output raw response from algod")
dryrunRemoteCmd.Flags().StringVarP(&txFilename, "dryrun-state", "D", "", "Dryrun request object to run")
dryrunRemoteCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Print more info")
dryrunRemoteCmd.Flags().BoolVarP(&rawOutput, "raw", "r", false, "Output raw response from algod")
dryrunRemoteCmd.MarkFlagRequired("dryrun-state")

simulateCmd.Flags().StringVarP(&txFilename, "txfile", "t", "", "Transaction or transaction-group to test")
simulateCmd.Flags().StringVarP(&outFilename, "outfile", "o", "", "Filename for writing simulation result")
panicIfErr(simulateCmd.MarkFlagRequired("txfile"))
}

var clerkCmd = &cobra.Command{
Expand Down Expand Up @@ -1253,6 +1257,34 @@ var dryrunRemoteCmd = &cobra.Command{
},
}

var simulateCmd = &cobra.Command{
Use: "simulate",
Short: "Simulate a transaction or transaction group offline",
Long: `Simulate a transaction or transaction group offline under various conditions and verbosity.`,
jasonpaulos marked this conversation as resolved.
Show resolved Hide resolved
Run: func(cmd *cobra.Command, args []string) {
data, err := readFile(txFilename)
if err != nil {
reportErrorf(fileReadError, txFilename, err)
}

dataDir := datadir.EnsureSingleDataDir()
algochoi marked this conversation as resolved.
Show resolved Hide resolved
client := ensureFullClient(dataDir)
resp, err := client.TransactionSimulation(data)
if err != nil {
reportErrorf("simulation error: %s", err.Error())
}

if outFilename != "" {
err = writeFile(outFilename, protocol.EncodeJSON(&resp), 0600)
if err != nil {
reportErrorf("write file error: %s", err.Error())
}
} else {
fmt.Println(string(protocol.EncodeJSON(&resp)))
}
},
}

// unmarshalSlice converts string addresses to basics.Address
func unmarshalSlice(accts []string) ([]basics.Address, error) {
result := make([]basics.Address, 0, len(accts))
Expand Down
18 changes: 12 additions & 6 deletions daemon/algod/api/client/restClient.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ const (

// rawRequestPaths is a set of paths where the body should not be urlencoded
var rawRequestPaths = map[string]bool{
"/v2/transactions": true,
"/v2/teal/dryrun": true,
"/v2/teal/compile": true,
"/v2/participation": true,
"/v2/transactions": true,
"/v2/teal/dryrun": true,
"/v2/teal/compile": true,
"/v2/participation": true,
"/v2/transactions/simulate": true,
}

// unauthorizedRequestError is generated when we receive 401 error from the server. This error includes the inner error
Expand Down Expand Up @@ -284,7 +285,7 @@ func (client RestClient) WaitForBlock(round basics.Round) (response model.NodeSt
return
}

// HealthCheck does a health check on the the potentially running node,
// HealthCheck does a health check on the potentially running node,
// returning an error if the API is down
func (client RestClient) HealthCheck() error {
return client.get(nil, "/health", nil)
Expand Down Expand Up @@ -629,6 +630,12 @@ func (client RestClient) RawDryrun(data []byte) (response []byte, err error) {
return
}

// RawTransactionSimulate gets the raw transaction or raw transaction group, and returns relevant simulation results.
func (client RestClient) RawTransactionSimulate(data []byte) (response model.SimulateResponse, err error) {
jasonpaulos marked this conversation as resolved.
Show resolved Hide resolved
err = client.submitForm(&response, "/v2/transactions/simulate", data, "POST", false /* encodeJSON */, true /* decodeJSON */, false)
return
}

// StateProofs gets a state proof that covers a given round
func (client RestClient) StateProofs(round uint64) (response model.StateProofResponse, err error) {
err = client.get(&response, fmt.Sprintf("/v2/stateproofs/%d", round), nil)
Expand Down Expand Up @@ -670,7 +677,6 @@ func (client RestClient) GetParticipationKeyByID(participationID string) (respon
func (client RestClient) RemoveParticipationKeyByID(participationID string) (err error) {
err = client.delete(nil, fmt.Sprintf("/v2/participation/%s", participationID), nil, true)
return

}

/* Endpoint registered for follower nodes */
Expand Down
19 changes: 14 additions & 5 deletions libgoal/libgoal.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,11 +504,10 @@ func (c *Client) signAndBroadcastTransactionWithWallet(walletHandle, pw []byte,
//
// validRounds | lastValid | result (lastValid)
// -------------------------------------------------
// 0 | 0 | firstValid + maxTxnLife
// 0 | N | lastValid
// M | 0 | first + validRounds - 1
// M | M | error
//
// 0 | 0 | firstValid + maxTxnLife
// 0 | N | lastValid
// M | 0 | first + validRounds - 1
// M | M | error
func (c *Client) ComputeValidityRounds(firstValid, lastValid, validRounds uint64) (first, last, latest uint64, err error) {
params, err := c.cachedSuggestedParams()
if err != nil {
Expand Down Expand Up @@ -1270,6 +1269,16 @@ func (c *Client) Dryrun(data []byte) (resp model.DryrunResponse, err error) {
return
}

// TransactionSimulation takes raw transaction or raw transaction group, and returns relevant simulation results.
func (c *Client) TransactionSimulation(data []byte) (resp model.SimulateResponse, err error) {
algod, err := c.ensureAlgodClient()
if err != nil {
return
}
resp, err = algod.RawTransactionSimulate(data)
return
}

// TransactionProof returns a Merkle proof for a transaction in a block.
func (c *Client) TransactionProof(txid string, round uint64, hashType crypto.HashType) (resp model.TransactionProofResponse, err error) {
algod, err := c.ensureAlgodClient()
Expand Down
139 changes: 139 additions & 0 deletions test/scripts/e2e_subs/e2e-app-simulate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/bin/bash

date '+app-simple-test start %Y%m%d_%H%M%S'

set -e
set -x
set -o pipefail
set -o nounset
export SHELLOPTS

WALLET=$1

# Directory of this bash program
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

gcmd="goal -w ${WALLET}"

ACCOUNT=$(${gcmd} account list|awk '{ print $3 }')

CONST_TRUE="true"
CONST_FALSE="false"

##############################################
# WE FIRST TEST TRANSACTION GROUP SIMULATION #
##############################################

${gcmd} clerk send -a 10000 -f ${ACCOUNT} -t ${ACCOUNT} -o pay1.tx
${gcmd} clerk send -a 10000 -f ${ACCOUNT} -t ${ACCOUNT} -o pay2.tx

cat pay1.tx pay2.tx | ${gcmd} clerk group -i - -o grouped.tx

# We first test transaction group simulation WITHOUT signatures
RES=$(${gcmd} clerk simulate -t grouped.tx)

if [[ $(echo "$RES" | jq '."would-succeed"') != $CONST_FALSE ]]; then
date '+app-simulate-test FAIL the simulation transaction group without signatures should not succeed %Y%m%d_%H%M%S'
false
fi

# check the simulation failing reason, first transaction has no signature
if [[ $(echo "$RES" | jq '."txn-groups"[0]."txn-results"[0]."missing-signature"') != $CONST_TRUE ]]; then
date '+app-simulate-test FAIL the simulation transaction group FAIL for first transaction has NO signature %Y%m%d_%H%M%S'
false
fi

# check the simulation failing reason, second transaction has no signature
if [[ $(echo "$RES" | jq '."txn-groups"[0]."txn-results"[1]."missing-signature"') != $CONST_TRUE ]]; then
date '+app-simulate-test FAIL the simulation transaction group FAIL for second transaction has NO signature %Y%m%d_%H%M%S'
false
fi

# We then test transaction group simulation WITH signatures
${gcmd} clerk split -i grouped.tx -o grouped.tx

${gcmd} clerk sign -i grouped-0.tx -o grouped-0.stx
${gcmd} clerk sign -i grouped-1.tx -o grouped-1.stx

cat grouped-0.stx grouped-1.stx > grouped.stx

RES=$(${gcmd} clerk simulate -t grouped.stx | jq '."would-succeed"')

if [[ $RES != $CONST_TRUE ]]; then
date '+app-simulate-test FAIL should pass to simulate self pay transaction group %Y%m%d_%H%M%S'
false
fi

###############################################
# WE ALSO TEST OVERSPEND IN TRANSACTION GROUP #
###############################################

${gcmd} clerk send -a 1000000000000 -f ${ACCOUNT} -t ${ACCOUNT} -o pay1.tx
${gcmd} clerk send -a 10000 -f ${ACCOUNT} -t ${ACCOUNT} -o pay2.tx

cat pay1.tx pay2.tx | ${gcmd} clerk group -i - -o grouped.tx

${gcmd} clerk split -i grouped.tx -o grouped.tx

${gcmd} clerk sign -i grouped-0.tx -o grouped-0.stx
${gcmd} clerk sign -i grouped-1.tx -o grouped-1.stx

cat grouped-0.stx grouped-1.stx > grouped.stx

RES=$(${gcmd} clerk simulate -t grouped.stx)

if [[ $(echo "$RES" | jq '."would-succeed"') != $CONST_FALSE ]]; then
data '+app-simulate-test FAIL should FAIL for overspending in simulate self pay transaction group %Y%m%d_%H%M%S'
false
fi

OVERSPEND_INFO="overspend"

if [[ $(echo "$RES" | jq '."txn-groups"[0]."failure-message"') != *"$OVERSPEND_INFO"* ]]; then
data '+app-simulate-test FAIL first overspending transaction in transaction group should contain message OVERSPEND %Y%m%d_%H%M%S'
false
fi

#######################################################
# NOW WE TRY TO TEST SIMULATION WITH ABI METHOD CALLS #
#######################################################

printf '#pragma version 2\nint 1' > "${TEMPDIR}/simple-v2.teal"

# Real Create
RES=$(${gcmd} app method --method "create(uint64)uint64" --arg "1234" --create --approval-prog ${DIR}/tealprogs/app-abi-method-example.teal --clear-prog ${TEMPDIR}/simple-v2.teal --global-byteslices 0 --global-ints 0 --local-byteslices 1 --local-ints 0 --extra-pages 0 --from $ACCOUNT 2>&1 || true)
EXPECTED="method create(uint64)uint64 succeeded with output: 2468"
if [[ $RES != *"${EXPECTED}"* ]]; then
date '+app-simulate-test FAIL the method call to create(uint64)uint64 should not fail %Y%m%d_%H%M%S'
false
fi

APPID=$(echo "$RES" | grep Created | awk '{ print $6 }')

# SIMULATION! empty()void
${gcmd} app method --method "empty()void" --app-id $APPID --from $ACCOUNT 2>&1 -o empty.tx

# SIMULATE without a signature first
RES=$(${gcmd} clerk simulate -t empty.tx)

# confirm that without signature, the simulation should fail
if [[ $(echo "$RES" | jq '."would-succeed"') != $CONST_FALSE ]]; then
date '+app-simulate-test FAIL the simulation call to empty()void without signature should not succeed %Y%m%d_%H%M%S'
false
fi

# check again the simulation failing reason
if [[ $(echo "$RES" | jq '."txn-groups"[0]."txn-results"[0]."missing-signature"') != $CONST_TRUE ]]; then
date '+app-simulate-test FAIL the simulation call to empty()void without signature should fail with missing-signature %Y%m%d_%H%M%S'
false
fi

# SIMULATE with a signature
${gcmd} clerk sign -i empty.tx -o empty.stx
RES=$(${gcmd} clerk simulate -t empty.stx | jq '."would-succeed"')

# with signature, simulation app-call should succeed
if [[ $RES != $CONST_TRUE ]]; then
date '+app-simulate-test FAIL the simulation call to empty()void should succeed %Y%m%d_%H%M%S'
false
fi