diff --git a/.circleci/config.yml b/.circleci/config.yml index b17ac198..5fb9a5b9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ workflows: name: 'test_go_<< matrix.go_version >>' matrix: parameters: - go_version: ['1.17'] + go_version: ['1.20.5'] jobs: test: diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml index 33c3183e..197fae69 100644 --- a/.github/workflows/codegen.yml +++ b/.github/workflows/codegen.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v3 + uses: actions/checkout@v3.5.3 - name: Generate and PR uses: algorand/generator/.github/actions/sdk-codegen/@master with: diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index 640c8159..3705b630 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -9,7 +9,7 @@ on: required: true type: string pre_release_version: - description: "Pre-Release version, e.g. 'beta.1', will be added behind the release_version as the tag." + description: "(Optional) Pre-Release version, e.g. 'beta.1'. Used mainly to support consensus release on betanet." required: false type: string @@ -43,7 +43,7 @@ jobs: fi - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v3.5.3 with: fetch-depth: 0 @@ -78,52 +78,25 @@ jobs: fi - name: Build Changelog - uses: mikepenz/release-changelog-builder-action@v3.7.2 id: build-changelog env: PREVIOUS_VERSION: ${{ steps.get-release.outputs.latest-tag }} - with: - fromTag: ${{ env.PREVIOUS_VERSION }} - toTag: ${{ env.RELEASE_BRANCH }} - failOnError: true - configurationJson: | - { - "categories": [ - { - "title": "## New Features", - "labels": [ - "New Feature" - ] - }, - { - "title": "## Enhancements", - "labels": [ - "Enhancement" - ] - }, - { - "title": "## Bug Fixes", - "labels": [ - "Bug-Fix" - ] - }, - { - "title": "## Not Yet Enabled", - "labels": [ - "Not-Yet-Enabled" - ] - } - ], - "ignore_labels": [ - "Skip-Release-Notes" - ], - "sort": { - "order": "ASC", - "on_property": "mergedAt" - }, - "template": "#{{CHANGELOG}}", - "pr_template": "- #{{TITLE}} by @#{{AUTHOR}} in ##{{NUMBER}}" - } + run: | + CHANGELOG=$(curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ github.token }}"\ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/${{ github.repository }}/releases/generate-notes \ + -d '{"tag_name":"${{ env.RELEASE_VERSION }}","target_commitish":"${{ env.RELEASE_BRANCH }}","previous_tag_name":"${{ env.PREVIOUS_VERSION }}","configuration_file_path":".github/release.yml"}' \ + | jq -r '.body') + + # The EOF steps are used to save multiline string in github: + # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#example-of-a-multiline-string + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "changelog<<$EOF" >> $GITHUB_OUTPUT + echo -e "${CHANGELOG}" >> $GITHUB_OUTPUT + echo "$EOF" >> $GITHUB_OUTPUT - name: Update Changelog if: ${{ env.PRE_RELEASE_VERSION == '' }} @@ -131,7 +104,7 @@ jobs: CHANGELOG_CONTENT: ${{ steps.build-changelog.outputs.changelog }} PREVIOUS_VERSION: ${{ steps.get-release.outputs.latest-tag }} run: | - echo -e "# ${RELEASE_VERSION}\n\n${CHANGELOG_CONTENT}**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_VERSION}...${RELEASE_VERSION}\n" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md + echo -e "# ${RELEASE_VERSION}\n\n${CHANGELOG_CONTENT}\n" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md - name: Commit Changes uses: EndBug/add-and-commit@v9.1.3 @@ -147,15 +120,12 @@ jobs: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ steps.set-release.outputs.release-tag }} run: | - echo -e "# What's Changed\n\n${CHANGELOG_CONTENT}**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_VERSION}...${RELEASE_TAG}" > tmp_msg_body.txt - export msg_body=$(cat tmp_msg_body.txt) - rm tmp_msg_body.txt # Note: There's an issue adding teams as reviewers, see https://github.com/cli/cli/issues/6395 PULL_REQUEST_URL=$(gh pr create --base "master" \ --title "FOR REVIEW ONLY: ${{ github.event.repository.name }} $RELEASE_TAG" \ --label "Skip-Release-Notes" \ --label "Team Hyper Flow" \ - --body "$msg_body" | tail -n 1) + --body "${CHANGELOG_CONTENT}" | tail -n 1) if [[ $PULL_REQUEST_URL =~ ^https://github.com/${{ github.repository }}/pull/[0-9]+$ ]]; then PULL_REQUEST_NUM=$(echo $PULL_REQUEST_URL | sed 's:.*/::') echo "pull-request-master=$PULL_REQUEST_URL" >> $GITHUB_ENV diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index afe5fb36..2f3845da 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -6,23 +6,23 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v3.5.3 with: fetch-depth: 0 # required for new-from-rev option in .golangci.yml - name: Install specific golang - uses: actions/setup-go@v2 + uses: actions/setup-go@v4.0.1 with: - go-version: '1.17.13' + go-version: '1.20.5' - name: Check format run: test -z `go fmt ./...` - name: Vet run: go vet ./... - name: reviewdog-golangci-lint - uses: reviewdog/action-golangci-lint@v2 + uses: reviewdog/action-golangci-lint@v2.3.1 with: - golangci_lint_version: "v1.47.3" + golangci_lint_version: "v1.53.2" golangci_lint_flags: "-c .golangci.yml --allow-parallel-runners" - go_version: "1.17.13" + go_version: "1.20.5" reporter: "github-pr-review" tool_name: "Lint Errors" level: "error" diff --git a/.golangci.yml b/.golangci.yml index 30061ae5..a62927aa 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,7 +8,6 @@ run: linters: disable-all: true enable: - - deadcode - errcheck - exportloopref - gci @@ -21,10 +20,8 @@ linters: - nolintlint - revive - staticcheck - - structcheck - typecheck - unused - - varcheck linters-settings: gci: @@ -61,6 +58,8 @@ issues: - "exported method (.*).Unwrap` should have comment or be unexported" # ignore issues about the way we use _struct fields to define encoding settings - "`_struct` is unused" + # we are not enforcing package-comments at this point + - "^package-comments: should have a package comment" # Enable some golangci-lint default exception rules: # "EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok" diff --git a/.test-env b/.test-env index 049289d7..a321a7f1 100644 --- a/.test-env +++ b/.test-env @@ -1,6 +1,6 @@ # Configs for testing repo download: SDK_TESTING_URL="https://github.com/algorand/algorand-sdk-testing" -SDK_TESTING_BRANCH="master" +SDK_TESTING_BRANCH="V2" SDK_TESTING_HARNESS="test-harness" INSTALL_ONLY=0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 069559a6..d7315c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# v2.3.0 + +## What's Changed +### Bugfixes +* bug-fix: include currency-greater-than param for 0 value by @shiqizng in https://github.com/algorand/go-algorand-sdk/pull/584 +* types: Reject non canonical addresses in DecodeAddress by @algochoi in https://github.com/algorand/go-algorand-sdk/pull/595 +### New Features +* Build: Go 1.20 and golint-ci 1.52 support by @gmalouf in https://github.com/algorand/go-algorand-sdk/pull/566 +* chore: Add new consensus params to vFuture by @Eric-Warehime in https://github.com/algorand/go-algorand-sdk/pull/577 +### Enhancements +* consensus config: add dynamic filter timeout parameter by @yossigi in https://github.com/algorand/go-algorand-sdk/pull/603 +* Simulate: Fix simulate request error & support ATC simulation by @jasonpaulos in https://github.com/algorand/go-algorand-sdk/pull/611 +* api: Sync client object with latest spec. by @winder in https://github.com/algorand/go-algorand-sdk/pull/613 +### Other +* Regenerate code with the latest specification file (7276a1b2) by @github-actions in https://github.com/algorand/go-algorand-sdk/pull/582 +* Regenerate code with the latest specification file (fb8a5ede) by @github-actions in https://github.com/algorand/go-algorand-sdk/pull/585 + +## New Contributors +* @gmalouf made their first contribution in https://github.com/algorand/go-algorand-sdk/pull/566 +* @yossigi made their first contribution in https://github.com/algorand/go-algorand-sdk/pull/603 + +**Full Changelog**: https://github.com/algorand/go-algorand-sdk/compare/v2.2.0...v2.3.0 + # v2.2.0 ## Enhancements diff --git a/Makefile b/Makefile index 15ee842c..ad6b9b40 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ TEST_SOURCES := $(shell cd $(SRCPATH) && go list ./...) TEST_SOURCES_NO_CUCUMBER := $(shell cd $(SRCPATH) && go list ./... | grep -v test) UNIT_TAGS := "$(shell awk '{print $2}' test/unit.tags | paste -s -d, -)" INTEGRATIONS_TAGS := "$(shell awk '{print $2}' test/integration.tags | paste -s -d, -)" -GO_IMAGE := golang:$(subst go,,$(shell go version | cut -d' ' -f 3 | cut -d'.' -f 1,2))-stretch +GO_IMAGE := golang:$(subst go,,$(shell go version | cut -d' ' -f 3 | cut -d'.' -f 1,2))-bookworm lint: golangci-lint run -c .golangci.yml diff --git a/client/v2/algod/algod.go b/client/v2/algod/algod.go index 2914f2f9..48deecf4 100644 --- a/client/v2/algod/algod.go +++ b/client/v2/algod/algod.go @@ -98,6 +98,10 @@ func (c *Client) Block(round uint64) *Block { return &Block{c: c, round: round} } +func (c *Client) GetBlockTxids(round uint64) *GetBlockTxids { + return &GetBlockTxids{c: c, round: round} +} + func (c *Client) GetBlockHash(round uint64) *GetBlockHash { return &GetBlockHash{c: c, round: round} } diff --git a/client/v2/algod/getBlockTxids.go b/client/v2/algod/getBlockTxids.go new file mode 100644 index 00000000..1297d741 --- /dev/null +++ b/client/v2/algod/getBlockTxids.go @@ -0,0 +1,23 @@ +package algod + +import ( + "context" + "fmt" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/common" + "github.com/algorand/go-algorand-sdk/v2/client/v2/common/models" +) + +// GetBlockTxids get the top level transaction IDs for the block on the given +// round. +type GetBlockTxids struct { + c *Client + + round uint64 +} + +// Do performs the HTTP request +func (s *GetBlockTxids) Do(ctx context.Context, headers ...*common.Header) (response models.BlockTxidsResponse, err error) { + err = s.c.get(ctx, &response, fmt.Sprintf("/v2/blocks/%s/txids", common.EscapeParams(s.round)...), nil, headers) + return +} diff --git a/client/v2/algod/simulateTransaction.go b/client/v2/algod/simulateTransaction.go index ef5fdae6..cec0944f 100644 --- a/client/v2/algod/simulateTransaction.go +++ b/client/v2/algod/simulateTransaction.go @@ -5,6 +5,7 @@ import ( "github.com/algorand/go-algorand-sdk/v2/client/v2/common" "github.com/algorand/go-algorand-sdk/v2/client/v2/common/models" + "github.com/algorand/go-algorand-sdk/v2/encoding/msgpack" ) // SimulateTransactionParams contains all of the query parameters for url serialization. @@ -28,6 +29,6 @@ type SimulateTransaction struct { // Do performs the HTTP request func (s *SimulateTransaction) Do(ctx context.Context, headers ...*common.Header) (response models.SimulateResponse, err error) { - err = s.c.post(ctx, &response, "/v2/transactions/simulate", s.p, headers, s.request) + err = s.c.post(ctx, &response, "/v2/transactions/simulate", s.p, headers, msgpack.Encode(&s.request)) return } diff --git a/client/v2/algod/waitForBlock.go b/client/v2/algod/waitForBlock.go index 70435f39..0c26ec97 100644 --- a/client/v2/algod/waitForBlock.go +++ b/client/v2/algod/waitForBlock.go @@ -9,7 +9,9 @@ import ( ) // StatusAfterBlock waits for a block to appear after round {round} and returns the -// node's status at the time. +// node's status at the time. There is a 1 minute timeout, when reached the current +// status is returned regardless of whether or not it is the round after the given +// round. type StatusAfterBlock struct { c *Client diff --git a/client/v2/common/common.go b/client/v2/common/common.go index 538fc63a..6b9b3f5c 100644 --- a/client/v2/common/common.go +++ b/client/v2/common/common.go @@ -16,10 +16,11 @@ import ( // rawRequestPaths is a set of paths where the body should not be urlencoded var rawRequestPaths = map[string]bool{ - "/v2/transactions": true, - "/v2/teal/compile": true, - "/v2/teal/disassemble": true, - "/v2/teal/dryrun": true, + "/v2/transactions": true, + "/v2/teal/compile": true, + "/v2/teal/disassemble": true, + "/v2/teal/dryrun": true, + "/v2/transactions/simulate": true, } // Header is a struct for custom headers. diff --git a/client/v2/common/models/application_initial_states.go b/client/v2/common/models/application_initial_states.go new file mode 100644 index 00000000..f4bbf885 --- /dev/null +++ b/client/v2/common/models/application_initial_states.go @@ -0,0 +1,17 @@ +package models + +// ApplicationInitialStates an application's initial global/local/box states that +// were accessed during simulation. +type ApplicationInitialStates struct { + // AppBoxes an application's global/local/box state. + AppBoxes ApplicationKVStorage `json:"app-boxes,omitempty"` + + // AppGlobals an application's global/local/box state. + AppGlobals ApplicationKVStorage `json:"app-globals,omitempty"` + + // AppLocals an application's initial local states tied to different accounts. + AppLocals []ApplicationKVStorage `json:"app-locals,omitempty"` + + // Id application index. + Id uint64 `json:"id"` +} diff --git a/client/v2/common/models/application_k_v_storage.go b/client/v2/common/models/application_k_v_storage.go new file mode 100644 index 00000000..c4092b43 --- /dev/null +++ b/client/v2/common/models/application_k_v_storage.go @@ -0,0 +1,10 @@ +package models + +// ApplicationKVStorage an application's global/local/box state. +type ApplicationKVStorage struct { + // Account the address of the account associated with the local state. + Account string `json:"account,omitempty"` + + // Kvs key-Value pairs representing application states. + Kvs []AvmKeyValue `json:"kvs"` +} diff --git a/client/v2/common/models/application_local_reference.go b/client/v2/common/models/application_local_reference.go new file mode 100644 index 00000000..a740ba0a --- /dev/null +++ b/client/v2/common/models/application_local_reference.go @@ -0,0 +1,11 @@ +package models + +// ApplicationLocalReference references an account's local state for an +// application. +type ApplicationLocalReference struct { + // Account address of the account with the local state. + Account string `json:"account"` + + // App application ID of the local state application. + App uint64 `json:"app"` +} diff --git a/client/v2/common/models/application_state_operation.go b/client/v2/common/models/application_state_operation.go new file mode 100644 index 00000000..c25411bc --- /dev/null +++ b/client/v2/common/models/application_state_operation.go @@ -0,0 +1,22 @@ +package models + +// ApplicationStateOperation an operation against an application's global/local/box +// state. +type ApplicationStateOperation struct { + // Account for local state changes, the address of the account associated with the + // local state. + Account string `json:"account,omitempty"` + + // AppStateType type of application state. Value `g` is **global state**, `l` is + // **local state**, `b` is **boxes**. + AppStateType string `json:"app-state-type"` + + // Key the key (name) of the global/local/box state. + Key []byte `json:"key"` + + // NewValue represents an AVM value. + NewValue AvmValue `json:"new-value,omitempty"` + + // Operation operation type. Value `w` is **write**, `d` is **delete**. + Operation string `json:"operation"` +} diff --git a/client/v2/common/models/asset_holding_reference.go b/client/v2/common/models/asset_holding_reference.go new file mode 100644 index 00000000..0ef882a8 --- /dev/null +++ b/client/v2/common/models/asset_holding_reference.go @@ -0,0 +1,10 @@ +package models + +// AssetHoldingReference references an asset held by an account. +type AssetHoldingReference struct { + // Account address of the account holding the asset. + Account string `json:"account"` + + // Asset asset ID of the holding. + Asset uint64 `json:"asset"` +} diff --git a/client/v2/common/models/avm_key_value.go b/client/v2/common/models/avm_key_value.go new file mode 100644 index 00000000..9cabff16 --- /dev/null +++ b/client/v2/common/models/avm_key_value.go @@ -0,0 +1,10 @@ +package models + +// AvmKeyValue represents an AVM key-value pair in an application store. +type AvmKeyValue struct { + // Key + Key []byte `json:"key"` + + // Value represents an AVM value. + Value AvmValue `json:"value"` +} diff --git a/client/v2/common/models/avm_value.go b/client/v2/common/models/avm_value.go new file mode 100644 index 00000000..28e5a47c --- /dev/null +++ b/client/v2/common/models/avm_value.go @@ -0,0 +1,13 @@ +package models + +// AvmValue represents an AVM value. +type AvmValue struct { + // Bytes bytes value. + Bytes []byte `json:"bytes,omitempty"` + + // Type value type. Value `1` refers to **bytes**, value `2` refers to **uint64** + Type uint64 `json:"type"` + + // Uint uint value. + Uint uint64 `json:"uint,omitempty"` +} diff --git a/client/v2/common/models/block_txids_response.go b/client/v2/common/models/block_txids_response.go new file mode 100644 index 00000000..8830b265 --- /dev/null +++ b/client/v2/common/models/block_txids_response.go @@ -0,0 +1,7 @@ +package models + +// BlockTxidsResponse top level transaction IDs in a block. +type BlockTxidsResponse struct { + // Blocktxids block transaction IDs. + Blocktxids []string `json:"blockTxids"` +} diff --git a/client/v2/common/models/box_reference.go b/client/v2/common/models/box_reference.go new file mode 100644 index 00000000..1a0a27f4 --- /dev/null +++ b/client/v2/common/models/box_reference.go @@ -0,0 +1,10 @@ +package models + +// BoxReference references a box of an application. +type BoxReference struct { + // App application ID which this box belongs to + App uint64 `json:"app"` + + // Name base64 encoded box name + Name []byte `json:"name"` +} diff --git a/client/v2/common/models/scratch_change.go b/client/v2/common/models/scratch_change.go new file mode 100644 index 00000000..851f69ee --- /dev/null +++ b/client/v2/common/models/scratch_change.go @@ -0,0 +1,10 @@ +package models + +// ScratchChange a write operation into a scratch slot. +type ScratchChange struct { + // NewValue represents an AVM value. + NewValue AvmValue `json:"new-value"` + + // Slot the scratch slot written. + Slot uint64 `json:"slot"` +} diff --git a/client/v2/common/models/simulate_initial_states.go b/client/v2/common/models/simulate_initial_states.go new file mode 100644 index 00000000..7449eff2 --- /dev/null +++ b/client/v2/common/models/simulate_initial_states.go @@ -0,0 +1,9 @@ +package models + +// SimulateInitialStates initial states of resources that were accessed during +// simulation. +type SimulateInitialStates struct { + // AppInitialStates the initial states of accessed application before simulation. + // The order of this array is arbitrary. + AppInitialStates []ApplicationInitialStates `json:"app-initial-states,omitempty"` +} diff --git a/client/v2/common/models/simulate_request.go b/client/v2/common/models/simulate_request.go index a212ad8a..ad26db77 100644 --- a/client/v2/common/models/simulate_request.go +++ b/client/v2/common/models/simulate_request.go @@ -2,13 +2,16 @@ package models // SimulateRequest request type for simulation endpoint. type SimulateRequest struct { - // AllowEmptySignatures allow transactions without signatures to be simulated as if - // they had correct signatures. + // AllowEmptySignatures allows transactions without signatures to be simulated as + // if they had correct signatures. AllowEmptySignatures bool `json:"allow-empty-signatures,omitempty"` // AllowMoreLogging lifts limits on log opcode usage during simulation. AllowMoreLogging bool `json:"allow-more-logging,omitempty"` + // AllowUnnamedResources allows access to unnamed resources during simulation. + AllowUnnamedResources bool `json:"allow-unnamed-resources,omitempty"` + // ExecTraceConfig an object that configures simulation execution trace. ExecTraceConfig SimulateTraceConfig `json:"exec-trace-config,omitempty"` @@ -16,6 +19,12 @@ type SimulateRequest struct { // transaction group. ExtraOpcodeBudget uint64 `json:"extra-opcode-budget,omitempty"` + // Round if provided, specifies the round preceding the simulation. State changes + // through this round will be used to run this simulation. Usually only the 4 most + // recent rounds will be available (controlled by the node config value + // MaxAcctLookback). If not specified, defaults to the latest available round. + Round uint64 `json:"round,omitempty"` + // TxnGroups the transaction groups to simulate. TxnGroups []SimulateRequestTransactionGroup `json:"txn-groups"` } diff --git a/client/v2/common/models/simulate_response.go b/client/v2/common/models/simulate_response.go index 98e4034a..ac109fe2 100644 --- a/client/v2/common/models/simulate_response.go +++ b/client/v2/common/models/simulate_response.go @@ -10,6 +10,9 @@ type SimulateResponse struct { // ExecTraceConfig an object that configures simulation execution trace. ExecTraceConfig SimulateTraceConfig `json:"exec-trace-config,omitempty"` + // InitialStates initial states of resources that were accessed during simulation. + InitialStates SimulateInitialStates `json:"initial-states,omitempty"` + // LastRound the round immediately preceding this simulation. State changes through // this round were used to run this simulation. LastRound uint64 `json:"last-round"` diff --git a/client/v2/common/models/simulate_trace_config.go b/client/v2/common/models/simulate_trace_config.go index 27e4166b..448c9bb5 100644 --- a/client/v2/common/models/simulate_trace_config.go +++ b/client/v2/common/models/simulate_trace_config.go @@ -5,4 +5,16 @@ type SimulateTraceConfig struct { // Enable a boolean option for opting in execution trace features simulation // endpoint. Enable bool `json:"enable,omitempty"` + + // ScratchChange a boolean option enabling returning scratch slot changes together + // with execution trace during simulation. + ScratchChange bool `json:"scratch-change,omitempty"` + + // StackChange a boolean option enabling returning stack changes together with + // execution trace during simulation. + StackChange bool `json:"stack-change,omitempty"` + + // StateChange a boolean option enabling returning application state changes + // (global, local, and box changes) with the execution trace during simulation. + StateChange bool `json:"state-change,omitempty"` } diff --git a/client/v2/common/models/simulate_transaction_group_result.go b/client/v2/common/models/simulate_transaction_group_result.go index 43429fc5..9c77de03 100644 --- a/client/v2/common/models/simulate_transaction_group_result.go +++ b/client/v2/common/models/simulate_transaction_group_result.go @@ -22,4 +22,15 @@ type SimulateTransactionGroupResult struct { // TxnResults simulation result for individual transactions TxnResults []SimulateTransactionResult `json:"txn-results"` + + // UnnamedResourcesAccessed these are resources that were accessed by this group + // that would normally have caused failure, but were allowed in simulation. + // Depending on where this object is in the response, the unnamed resources it + // contains may or may not qualify for group resource sharing. If this is a field + // in SimulateTransactionGroupResult, the resources do qualify, but if this is a + // field in SimulateTransactionResult, they do not qualify. In order to make this + // group valid for actual submission, resources that qualify for group sharing can + // be made available by any transaction of the group; otherwise, resources must be + // placed in the same transaction which accessed them. + UnnamedResourcesAccessed SimulateUnnamedResourcesAccessed `json:"unnamed-resources-accessed,omitempty"` } diff --git a/client/v2/common/models/simulate_transaction_result.go b/client/v2/common/models/simulate_transaction_result.go index 56cf5118..b5034e9f 100644 --- a/client/v2/common/models/simulate_transaction_result.go +++ b/client/v2/common/models/simulate_transaction_result.go @@ -16,4 +16,15 @@ type SimulateTransactionResult struct { // TxnResult details about a pending transaction. If the transaction was recently // confirmed, includes confirmation details like the round and reward details. TxnResult PendingTransactionResponse `json:"txn-result"` + + // UnnamedResourcesAccessed these are resources that were accessed by this group + // that would normally have caused failure, but were allowed in simulation. + // Depending on where this object is in the response, the unnamed resources it + // contains may or may not qualify for group resource sharing. If this is a field + // in SimulateTransactionGroupResult, the resources do qualify, but if this is a + // field in SimulateTransactionResult, they do not qualify. In order to make this + // group valid for actual submission, resources that qualify for group sharing can + // be made available by any transaction of the group; otherwise, resources must be + // placed in the same transaction which accessed them. + UnnamedResourcesAccessed SimulateUnnamedResourcesAccessed `json:"unnamed-resources-accessed,omitempty"` } diff --git a/client/v2/common/models/simulate_unnamed_resources_accessed.go b/client/v2/common/models/simulate_unnamed_resources_accessed.go new file mode 100644 index 00000000..ca888f0a --- /dev/null +++ b/client/v2/common/models/simulate_unnamed_resources_accessed.go @@ -0,0 +1,41 @@ +package models + +// SimulateUnnamedResourcesAccessed these are resources that were accessed by this +// group that would normally have caused failure, but were allowed in simulation. +// Depending on where this object is in the response, the unnamed resources it +// contains may or may not qualify for group resource sharing. If this is a field +// in SimulateTransactionGroupResult, the resources do qualify, but if this is a +// field in SimulateTransactionResult, they do not qualify. In order to make this +// group valid for actual submission, resources that qualify for group sharing can +// be made available by any transaction of the group; otherwise, resources must be +// placed in the same transaction which accessed them. +type SimulateUnnamedResourcesAccessed struct { + // Accounts the unnamed accounts that were referenced. The order of this array is + // arbitrary. + Accounts []string `json:"accounts,omitempty"` + + // AppLocals the unnamed application local states that were referenced. The order + // of this array is arbitrary. + AppLocals []ApplicationLocalReference `json:"app-locals,omitempty"` + + // Apps the unnamed applications that were referenced. The order of this array is + // arbitrary. + Apps []uint64 `json:"apps,omitempty"` + + // AssetHoldings the unnamed asset holdings that were referenced. The order of this + // array is arbitrary. + AssetHoldings []AssetHoldingReference `json:"asset-holdings,omitempty"` + + // Assets the unnamed assets that were referenced. The order of this array is + // arbitrary. + Assets []uint64 `json:"assets,omitempty"` + + // Boxes the unnamed boxes that were referenced. The order of this array is + // arbitrary. + Boxes []BoxReference `json:"boxes,omitempty"` + + // ExtraBoxRefs the number of extra box references used to increase the IO budget. + // This is in addition to the references defined in the input transaction group and + // any referenced to unnamed boxes. + ExtraBoxRefs uint64 `json:"extra-box-refs,omitempty"` +} diff --git a/client/v2/common/models/simulation_eval_overrides.go b/client/v2/common/models/simulation_eval_overrides.go index dfa9c3d9..3a49df7f 100644 --- a/client/v2/common/models/simulation_eval_overrides.go +++ b/client/v2/common/models/simulation_eval_overrides.go @@ -8,6 +8,10 @@ type SimulationEvalOverrides struct { // simulated as if they were properly signed. AllowEmptySignatures bool `json:"allow-empty-signatures,omitempty"` + // AllowUnnamedResources if true, allows access to unnamed resources during + // simulation. + AllowUnnamedResources bool `json:"allow-unnamed-resources,omitempty"` + // ExtraOpcodeBudget the extra opcode budget added to each transaction group during // simulation ExtraOpcodeBudget uint64 `json:"extra-opcode-budget,omitempty"` diff --git a/client/v2/common/models/simulation_opcode_trace_unit.go b/client/v2/common/models/simulation_opcode_trace_unit.go index 1b256458..9618d0c0 100644 --- a/client/v2/common/models/simulation_opcode_trace_unit.go +++ b/client/v2/common/models/simulation_opcode_trace_unit.go @@ -6,7 +6,19 @@ type SimulationOpcodeTraceUnit struct { // Pc the program counter of the current opcode being evaluated. Pc uint64 `json:"pc"` + // ScratchChanges the writes into scratch slots. + ScratchChanges []ScratchChange `json:"scratch-changes,omitempty"` + // SpawnedInners the indexes of the traces for inner transactions spawned by this // opcode, if any. SpawnedInners []uint64 `json:"spawned-inners,omitempty"` + + // StackAdditions the values added by this opcode to the stack. + StackAdditions []AvmValue `json:"stack-additions,omitempty"` + + // StackPopCount the number of deleted stack values by this opcode. + StackPopCount uint64 `json:"stack-pop-count,omitempty"` + + // StateChanges the operations against the current application's states. + StateChanges []ApplicationStateOperation `json:"state-changes,omitempty"` } diff --git a/client/v2/common/models/simulation_transaction_exec_trace.go b/client/v2/common/models/simulation_transaction_exec_trace.go index c37196bb..ca300a5f 100644 --- a/client/v2/common/models/simulation_transaction_exec_trace.go +++ b/client/v2/common/models/simulation_transaction_exec_trace.go @@ -3,10 +3,18 @@ package models // SimulationTransactionExecTrace the execution trace of calling an app or a logic // sig, containing the inner app call trace in a recursive way. type SimulationTransactionExecTrace struct { + // ApprovalProgramHash sHA512_256 hash digest of the approval program executed in + // transaction. + ApprovalProgramHash []byte `json:"approval-program-hash,omitempty"` + // ApprovalProgramTrace program trace that contains a trace of opcode effects in an // approval program. ApprovalProgramTrace []SimulationOpcodeTraceUnit `json:"approval-program-trace,omitempty"` + // ClearStateProgramHash sHA512_256 hash digest of the clear state program executed + // in transaction. + ClearStateProgramHash []byte `json:"clear-state-program-hash,omitempty"` + // ClearStateProgramTrace program trace that contains a trace of opcode effects in // a clear state program. ClearStateProgramTrace []SimulationOpcodeTraceUnit `json:"clear-state-program-trace,omitempty"` @@ -15,6 +23,9 @@ type SimulationTransactionExecTrace struct { // trace of any inner transactions executed. InnerTrace []SimulationTransactionExecTrace `json:"inner-trace,omitempty"` + // LogicSigHash sHA512_256 hash digest of the logic sig executed in transaction. + LogicSigHash []byte `json:"logic-sig-hash,omitempty"` + // LogicSigTrace program trace that contains a trace of opcode effects in a logic // sig. LogicSigTrace []SimulationOpcodeTraceUnit `json:"logic-sig-trace,omitempty"` diff --git a/client/v2/indexer/lookupAccountTransactions.go b/client/v2/indexer/lookupAccountTransactions.go index e1bb6fbf..c8200aed 100644 --- a/client/v2/indexer/lookupAccountTransactions.go +++ b/client/v2/indexer/lookupAccountTransactions.go @@ -27,7 +27,7 @@ type LookupAccountTransactionsParams struct { // CurrencyGreaterThan results should have an amount greater than this value. // MicroAlgos are the default currency unless an asset-id is provided, in which // case the asset will be used. - CurrencyGreaterThan uint64 `url:"currency-greater-than,omitempty"` + CurrencyGreaterThan *uint64 `url:"currency-greater-than,omitempty"` // CurrencyLessThan results should have an amount less than this value. MicroAlgos // are the default currency unless an asset-id is provided, in which case the asset @@ -123,7 +123,7 @@ func (s *LookupAccountTransactions) BeforeTime(BeforeTime time.Time) *LookupAcco // MicroAlgos are the default currency unless an asset-id is provided, in which // case the asset will be used. func (s *LookupAccountTransactions) CurrencyGreaterThan(CurrencyGreaterThan uint64) *LookupAccountTransactions { - s.p.CurrencyGreaterThan = CurrencyGreaterThan + s.p.CurrencyGreaterThan = &CurrencyGreaterThan return s } diff --git a/client/v2/indexer/lookupAssetBalances.go b/client/v2/indexer/lookupAssetBalances.go index 33595ec3..edd55e63 100644 --- a/client/v2/indexer/lookupAssetBalances.go +++ b/client/v2/indexer/lookupAssetBalances.go @@ -14,7 +14,7 @@ type LookupAssetBalancesParams struct { // CurrencyGreaterThan results should have an amount greater than this value. // MicroAlgos are the default currency unless an asset-id is provided, in which // case the asset will be used. - CurrencyGreaterThan uint64 `url:"currency-greater-than,omitempty"` + CurrencyGreaterThan *uint64 `url:"currency-greater-than,omitempty"` // CurrencyLessThan results should have an amount less than this value. MicroAlgos // are the default currency unless an asset-id is provided, in which case the asset @@ -48,7 +48,7 @@ type LookupAssetBalances struct { // MicroAlgos are the default currency unless an asset-id is provided, in which // case the asset will be used. func (s *LookupAssetBalances) CurrencyGreaterThan(CurrencyGreaterThan uint64) *LookupAssetBalances { - s.p.CurrencyGreaterThan = CurrencyGreaterThan + s.p.CurrencyGreaterThan = &CurrencyGreaterThan return s } diff --git a/client/v2/indexer/lookupAssetTransactions.go b/client/v2/indexer/lookupAssetTransactions.go index 5890dd49..071addc2 100644 --- a/client/v2/indexer/lookupAssetTransactions.go +++ b/client/v2/indexer/lookupAssetTransactions.go @@ -32,7 +32,7 @@ type LookupAssetTransactionsParams struct { // CurrencyGreaterThan results should have an amount greater than this value. // MicroAlgos are the default currency unless an asset-id is provided, in which // case the asset will be used. - CurrencyGreaterThan uint64 `url:"currency-greater-than,omitempty"` + CurrencyGreaterThan *uint64 `url:"currency-greater-than,omitempty"` // CurrencyLessThan results should have an amount less than this value. MicroAlgos // are the default currency unless an asset-id is provided, in which case the asset @@ -142,7 +142,7 @@ func (s *LookupAssetTransactions) BeforeTime(BeforeTime time.Time) *LookupAssetT // MicroAlgos are the default currency unless an asset-id is provided, in which // case the asset will be used. func (s *LookupAssetTransactions) CurrencyGreaterThan(CurrencyGreaterThan uint64) *LookupAssetTransactions { - s.p.CurrencyGreaterThan = CurrencyGreaterThan + s.p.CurrencyGreaterThan = &CurrencyGreaterThan return s } diff --git a/client/v2/indexer/searchForAccounts.go b/client/v2/indexer/searchForAccounts.go index 175600e4..5d1edf5b 100644 --- a/client/v2/indexer/searchForAccounts.go +++ b/client/v2/indexer/searchForAccounts.go @@ -22,7 +22,7 @@ type SearchAccountsParams struct { // CurrencyGreaterThan results should have an amount greater than this value. // MicroAlgos are the default currency unless an asset-id is provided, in which // case the asset will be used. - CurrencyGreaterThan uint64 `url:"currency-greater-than,omitempty"` + CurrencyGreaterThan *uint64 `url:"currency-greater-than,omitempty"` // CurrencyLessThan results should have an amount less than this value. MicroAlgos // are the default currency unless an asset-id is provided, in which case the asset @@ -88,7 +88,7 @@ func (s *SearchAccounts) AuthAddress(AuthAddress string) *SearchAccounts { // MicroAlgos are the default currency unless an asset-id is provided, in which // case the asset will be used. func (s *SearchAccounts) CurrencyGreaterThan(CurrencyGreaterThan uint64) *SearchAccounts { - s.p.CurrencyGreaterThan = CurrencyGreaterThan + s.p.CurrencyGreaterThan = &CurrencyGreaterThan return s } diff --git a/client/v2/indexer/searchForTransactions.go b/client/v2/indexer/searchForTransactions.go index 04a18556..fa3b6bd0 100644 --- a/client/v2/indexer/searchForTransactions.go +++ b/client/v2/indexer/searchForTransactions.go @@ -37,7 +37,7 @@ type SearchForTransactionsParams struct { // CurrencyGreaterThan results should have an amount greater than this value. // MicroAlgos are the default currency unless an asset-id is provided, in which // case the asset will be used. - CurrencyGreaterThan uint64 `url:"currency-greater-than,omitempty"` + CurrencyGreaterThan *uint64 `url:"currency-greater-than,omitempty"` // CurrencyLessThan results should have an amount less than this value. MicroAlgos // are the default currency unless an asset-id is provided, in which case the asset @@ -160,7 +160,7 @@ func (s *SearchForTransactions) BeforeTime(BeforeTime time.Time) *SearchForTrans // MicroAlgos are the default currency unless an asset-id is provided, in which // case the asset will be used. func (s *SearchForTransactions) CurrencyGreaterThan(CurrencyGreaterThan uint64) *SearchForTransactions { - s.p.CurrencyGreaterThan = CurrencyGreaterThan + s.p.CurrencyGreaterThan = &CurrencyGreaterThan return s } diff --git a/go.mod b/go.mod index 98f11d2a..37b32c8f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/algorand/go-algorand-sdk/v2 -go 1.17 +go 1.20 require ( github.com/algorand/avm-abi v0.1.1 diff --git a/go.sum b/go.sum index e8b1fb2b..960ca0a0 100644 --- a/go.sum +++ b/go.sum @@ -3,7 +3,6 @@ github.com/algorand/avm-abi v0.1.1/go.mod h1:+CgwM46dithy850bpTeHh9MC99zpn2Snirb github.com/algorand/go-codec/codec v1.1.10 h1:zmWYU1cp64jQVTOG8Tw8wa+k0VfwgXIPbnDfiVa+5QA= github.com/algorand/go-codec/codec v1.1.10/go.mod h1:YkEx5nmr/zuCeaDYOIhlDg92Lxju8tj2d2NrYqP7g7k= github.com/chrismcguire/gobberish v0.0.0-20150821175641-1d8adb509a0e h1:CHPYEbz71w8DqJ7DRIq+MXyCQsdibK08vdcQTY4ufas= -github.com/chrismcguire/gobberish v0.0.0-20150821175641-1d8adb509a0e/go.mod h1:6Xhs0ZlsRjXLIiSMLKafbZxML/j30pg9Z1priLuha5s= github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8= github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/protocol/config/consensus.go b/protocol/config/consensus.go index 80cc8652..90174faa 100644 --- a/protocol/config/consensus.go +++ b/protocol/config/consensus.go @@ -93,6 +93,10 @@ type ConsensusParams struct { // rather than check each individual app call is within the budget. EnableAppCostPooling bool + // EnableLogicSigCostPooling specifies LogicSig budgets are pooled across a + // group. The total available is len(group) * LogicSigMaxCost) + EnableLogicSigCostPooling bool + // RewardUnit specifies the number of MicroAlgos corresponding to one reward // unit. // @@ -493,6 +497,12 @@ type ConsensusParams struct { // used by agreement for Circulation, and updates the calculation of StateProofOnlineTotalWeight used // by state proofs to use the same method (rather than excluding stake from the top N stakeholders as before). ExcludeExpiredCirculation bool + + // DynamicFilterTimeout indicates whether the filter timeout is set + // dynamically, at run time, according to the recent history of credential + // arrival times or is set to a static value. Even if this flag disables the + // dynamic filter, it will be calculated and logged (but not used). + DynamicFilterTimeout bool } // PaysetCommitType enumerates possible ways for the block header to commit to @@ -564,7 +574,7 @@ var MaxBytesKeyValueLen int var MaxExtraAppProgramLen int // MaxAvailableAppProgramLen is the largest supported app program size include the extra pages -//supported supported by any of the consensus protocols. used for decoding purposes. +// supported supported by any of the consensus protocols. used for decoding purposes. var MaxAvailableAppProgramLen int // MaxProposedExpiredOnlineAccounts is the maximum number of online accounts, which need @@ -1230,6 +1240,7 @@ func initConsensusProtocols() { vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} vFuture.LogicSigVersion = 10 // When moving this to a release, put a new higher LogicSigVersion here + vFuture.EnableLogicSigCostPooling = true Consensus[protocol.ConsensusFuture] = vFuture diff --git a/test/algodclientv2_test.go b/test/algodclientv2_test.go index 910d935a..21b26eab 100644 --- a/test/algodclientv2_test.go +++ b/test/algodclientv2_test.go @@ -64,6 +64,7 @@ func AlgodClientV2Context(s *godog.Suite) { s.Step(`^we make a GetLedgerStateDelta call against round (\d+)$`, weMakeAGetLedgerStateDeltaCallAgainstRound) s.Step(`^we make a LedgerStateDeltaForTransactionGroupResponse call for ID "([^"]*)"$`, weMakeALedgerStateDeltaForTransactionGroupResponseCallForID) s.Step(`^we make a TransactionGroupLedgerStateDeltaForRoundResponse call for round (\d+)$`, weMakeATransactionGroupLedgerStateDeltaForRoundResponseCallForRound) + s.Step(`^we make a GetBlockTxids call against block number (\d+)$`, weMakeAGetBlockTxidsCallAgainstBlockNumber) s.BeforeScenario(func(interface{}) { globalErrForExamination = nil @@ -394,3 +395,12 @@ func weMakeATransactionGroupLedgerStateDeltaForRoundResponseCallForRound(round i algodClient.GetTransactionGroupLedgerStateDeltasForRound(uint64(round)).Do(context.Background()) return nil } + +func weMakeAGetBlockTxidsCallAgainstBlockNumber(round int) error { + algodClient, err := algod.MakeClient(mockServer.URL, "") + if err != nil { + return err + } + algodClient.GetBlockTxids(uint64(round)).Do(context.Background()) + return nil +} diff --git a/test/applications_integration_test.go b/test/applications_integration_test.go index df33926b..88a4e6da 100644 --- a/test/applications_integration_test.go +++ b/test/applications_integration_test.go @@ -9,7 +9,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/algorand/go-algorand-sdk/v2/transaction" "reflect" "regexp" "sort" @@ -17,6 +16,8 @@ import ( "strings" "time" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/cucumber/godog" "github.com/algorand/go-algorand-sdk/v2/abi" @@ -34,7 +35,7 @@ var tx types.Transaction var transientAccount crypto.Account var applicationId uint64 var applicationIds []uint64 -var txComposerResult transaction.ExecuteResult +var txComposerMethodResults []transaction.ABIMethodResult func anAlgodVClientConnectedToPortWithToken(v int, host string, port int, token string) error { var err error @@ -485,16 +486,29 @@ func iCloneTheComposer() error { } func iExecuteTheCurrentTransactionGroupWithTheComposer() error { - var err error - txComposerResult, err = txComposer.Execute(algodV2client, context.Background(), 10) - return err + txComposerResult, err := txComposer.Execute(algodV2client, context.Background(), 10) + if err != nil { + return err + } + txComposerMethodResults = txComposerResult.MethodResults + return nil +} + +func iSimulateTheCurrentTransactionGroupWithTheComposer() error { + result, err := txComposer.Simulate(context.Background(), algodV2client, models.SimulateRequest{}) + if err != nil { + return err + } + simulateResponse = result.SimulateResponse + txComposerMethodResults = result.MethodResults + return nil } func theAppShouldHaveReturned(commaSeparatedB64Results string) error { b64ExpectedResults := strings.Split(commaSeparatedB64Results, ",") - if len(b64ExpectedResults) != len(txComposerResult.MethodResults) { - return fmt.Errorf("length of expected results doesn't match actual: %d != %d", len(b64ExpectedResults), len(txComposerResult.MethodResults)) + if len(b64ExpectedResults) != len(txComposerMethodResults) { + return fmt.Errorf("length of expected results doesn't match actual: %d != %d", len(b64ExpectedResults), len(txComposerMethodResults)) } for i, b64ExpectedResult := range b64ExpectedResults { @@ -503,7 +517,7 @@ func theAppShouldHaveReturned(commaSeparatedB64Results string) error { return err } - actualResult := txComposerResult.MethodResults[i] + actualResult := txComposerMethodResults[i] method := actualResult.Method if actualResult.DecodeError != nil { @@ -542,12 +556,12 @@ func theAppShouldHaveReturned(commaSeparatedB64Results string) error { func theAppShouldHaveReturnedABITypes(colonSeparatedExpectedTypeStrings string) error { expectedTypeStrings := strings.Split(colonSeparatedExpectedTypeStrings, ":") - if len(expectedTypeStrings) != len(txComposerResult.MethodResults) { - return fmt.Errorf("length of expected results doesn't match actual: %d != %d", len(expectedTypeStrings), len(txComposerResult.MethodResults)) + if len(expectedTypeStrings) != len(txComposerMethodResults) { + return fmt.Errorf("length of expected results doesn't match actual: %d != %d", len(expectedTypeStrings), len(txComposerMethodResults)) } for i, expectedTypeString := range expectedTypeStrings { - actualResult := txComposerResult.MethodResults[i] + actualResult := txComposerMethodResults[i] if actualResult.DecodeError != nil { return actualResult.DecodeError @@ -584,7 +598,7 @@ func theAppShouldHaveReturnedABITypes(colonSeparatedExpectedTypeStrings string) func checkAtomicResultAgainstValue(resultIndex int, path, expectedValue string) error { keys := strings.Split(path, ".") - info := txComposerResult.MethodResults[resultIndex].TransactionInfo + info := txComposerMethodResults[resultIndex].TransactionInfo jsonBytes := sdkJson.Encode(&info) @@ -677,7 +691,7 @@ func checkInnerTxnGroupIDs(colonSeparatedPathsString string) error { var current models.PendingTransactionResponse for pathIndex, innerTxnIndex := range path { if pathIndex == 0 { - current = models.PendingTransactionResponse(txComposerResult.MethodResults[innerTxnIndex].TransactionInfo) + current = models.PendingTransactionResponse(txComposerMethodResults[innerTxnIndex].TransactionInfo) } else { current = current.InnerTxns[innerTxnIndex] } @@ -704,7 +718,7 @@ func checkSpinResult(resultIndex int, method, r string) error { return fmt.Errorf("Incorrect method name, expected 'spin()', got '%s'", method) } - result := txComposerResult.MethodResults[resultIndex] + result := txComposerMethodResults[resultIndex] decodedResult := result.ReturnValue.([]interface{}) spin := decodedResult[0].([]interface{}) @@ -731,7 +745,7 @@ func sha512_256AsUint64(preimage []byte) uint64 { } func checkRandomIntResult(resultIndex, input int) error { - result := txComposerResult.MethodResults[resultIndex] + result := txComposerMethodResults[resultIndex] decodedResult := result.ReturnValue.([]interface{}) randInt := decodedResult[0].(uint64) @@ -752,7 +766,7 @@ func checkRandomIntResult(resultIndex, input int) error { } func checkRandomElementResult(resultIndex int, input string) error { - result := txComposerResult.MethodResults[resultIndex] + result := txComposerMethodResults[resultIndex] decodedResult := result.ReturnValue.([]interface{}) randElt := decodedResult[0].(byte) @@ -932,6 +946,7 @@ func ApplicationsContext(s *godog.Suite) { s.Step(`^I add the current transaction with signer to the composer\.$`, iAddTheCurrentTransactionWithSignerToTheComposer) s.Step(`^I clone the composer\.$`, iCloneTheComposer) s.Step(`^I execute the current transaction group with the composer\.$`, iExecuteTheCurrentTransactionGroupWithTheComposer) + s.Step(`^I simulate the current transaction group with the composer$`, iSimulateTheCurrentTransactionGroupWithTheComposer) s.Step(`^The app should have returned "([^"]*)"\.$`, theAppShouldHaveReturned) s.Step(`^The app should have returned ABI types "([^"]*)"\.$`, theAppShouldHaveReturnedABITypes) diff --git a/test/helpers.go b/test/helpers.go index 6b465445..f362be89 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -4,7 +4,7 @@ import ( "container/ring" "encoding/base64" "fmt" - "io/ioutil" + "io" "net/http" "net/http/httptest" "os" @@ -21,7 +21,7 @@ func loadMockJsons(commaDelimitedFilenames, pathToJsons string) ([][]byte, error if err != nil { return nil, err } - fileBytes, err := ioutil.ReadAll(jsonfile) + fileBytes, err := io.ReadAll(jsonfile) if err != nil { return nil, err } @@ -105,5 +105,5 @@ func expectErrorStringToContain(contains string) error { } func loadResource(filepath string) ([]byte, error) { - return ioutil.ReadFile(path.Join("features", "resources", filepath)) + return os.ReadFile(path.Join("features", "resources", filepath)) } diff --git a/test/integration.tags b/test/integration.tags index f8b49f9f..d73c5c74 100644 --- a/test/integration.tags +++ b/test/integration.tags @@ -13,3 +13,4 @@ @rekey_v1 @send @send.keyregtxn +@simulate diff --git a/test/responses_unit_test.go b/test/responses_unit_test.go index 0d838f5b..ad299696 100644 --- a/test/responses_unit_test.go +++ b/test/responses_unit_test.go @@ -193,6 +193,9 @@ func weMakeAnyCallTo(client /* algod/indexer */, endpoint string) (err error) { case "GetLedgerStateDeltaForTransactionGroup": response, err = algodC.GetLedgerStateDeltaForTransactionGroup("someID").Do(context.Background()) + case "GetBlockTxids": + response, err = + algodC.GetBlockTxids(123).Do(context.Background()) case "any": // This is an error case // pickup the error as the response diff --git a/test/steps_test.go b/test/steps_test.go index 2badecf2..85779912 100644 --- a/test/steps_test.go +++ b/test/steps_test.go @@ -104,6 +104,7 @@ var sourceMap logic.SourceMap var srcMapping map[string]interface{} var seeminglyProgram []byte var sanityCheckError error +var simulateResponse modelsV2.SimulateResponse var assetTestFixture struct { Creator string @@ -335,6 +336,7 @@ func FeatureContext(s *godog.Suite) { s.Step(`^the base64 encoded signed transactions should equal "([^"]*)"$`, theBaseEncodedSignedTransactionsShouldEqual) s.Step(`^I build a payment transaction with sender "([^"]*)", receiver "([^"]*)", amount (\d+), close remainder to "([^"]*)"$`, iBuildAPaymentTransactionWithSenderReceiverAmountCloseRemainderTo) s.Step(`^I create a transaction with signer with the current transaction\.$`, iCreateATransactionWithSignerWithTheCurrentTransaction) + s.Step(`^I create a transaction with an empty signer with the current transaction\.$`, iCreateATransactionWithAnEmptySignerWithTheCurrentTransaction) s.Step(`^I append the current transaction with signer to the method arguments array\.$`, iAppendTheCurrentTransactionWithSignerToTheMethodArgumentsArray) s.Step(`^a dryrun response file "([^"]*)" and a transaction at index "([^"]*)"$`, aDryrunResponseFileAndATransactionAtIndex) s.Step(`^calling app trace produces "([^"]*)"$`, callingAppTraceProduces) @@ -354,6 +356,10 @@ func FeatureContext(s *godog.Suite) { s.Step(`^I start heuristic sanity check over the bytes$`, heuristicCheckOverBytes) s.Step(`^if the heuristic sanity check throws an error, the error contains "([^"]*)"$`, checkErrorIfMatching) s.Step(`^disassembly of "([^"]*)" matches "([^"]*)"$`, disassemblyMatches) + s.Step(`^I simulate the transaction$`, iSimulateTheTransaction) + s.Step(`^the simulation should succeed without any failure message$`, theSimulationShouldSucceedWithoutAnyFailureMessage) + s.Step(`^I prepare the transaction without signatures for simulation$`, iPrepareTheTransactionWithoutSignaturesForSimulation) + s.Step(`^the simulation should report a failure at group "([^"]*)", path "([^"]*)" with message "([^"]*)"$`, theSimulationShouldReportAFailureAtGroupPathWithMessage) s.BeforeScenario(func(interface{}) { stxObj = types.SignedTxn{} @@ -2371,6 +2377,14 @@ func iCreateATransactionWithSignerWithTheCurrentTransaction() error { return nil } +func iCreateATransactionWithAnEmptySignerWithTheCurrentTransaction() error { + accountTxAndSigner = transaction.TransactionWithSigner{ + Signer: transaction.EmptyTransactionSigner{}, + Txn: txn, + } + return nil +} + func iAppendTheCurrentTransactionWithSignerToTheMethodArgumentsArray() error { methodArgs = append(methodArgs, accountTxAndSigner) return nil @@ -2581,3 +2595,76 @@ func disassemblyMatches(bytecodeFilename, sourceFilename string) error { } return nil } + +func iSimulateTheTransaction() error { + var signedTxn types.SignedTxn + if err := msgpack.Decode(stx, &signedTxn); err != nil { + return err + } + + resp, err := algodV2client.SimulateTransaction(modelsV2.SimulateRequest{ + TxnGroups: []modelsV2.SimulateRequestTransactionGroup{ + { + Txns: []types.SignedTxn{signedTxn}, + }, + }, + }).Do(context.Background()) + if err != nil { + return err + } + + simulateResponse = resp + return nil +} + +func theSimulationShouldSucceedWithoutAnyFailureMessage() error { + for i, groupResult := range simulateResponse.TxnGroups { + if groupResult.FailureMessage != "" { + return fmt.Errorf("Simulation group %d failed with message: %s", i, groupResult.FailureMessage) + } + } + return nil +} + +func iPrepareTheTransactionWithoutSignaturesForSimulation() error { + stx = msgpack.Encode(&types.SignedTxn{Txn: txn}) + return nil +} + +func theSimulationShouldReportAFailureAtGroupPathWithMessage(txnGroupIndex, failAt, expectedFailureMsg string) error { + // Parse transaction group number + groupIndex, err := strconv.Atoi(txnGroupIndex) + if err != nil { + return err + } + + // Parse the path ("0,0") into a list of numbers ([0, 0]) + path := strings.Split(failAt, ",") + expectedPath := make([]uint64, len(path)) + for i, pathStr := range path { + pathNumber, err := strconv.ParseUint(pathStr, 10, 64) + if err != nil { + return err + } + expectedPath[i] = pathNumber + } + + actualFailureMsg := simulateResponse.TxnGroups[groupIndex].FailureMessage + if expectedFailureMsg == "" && actualFailureMsg != "" { + return fmt.Errorf("Expected no failure message, but got: '%s'", actualFailureMsg) + } else if expectedFailureMsg != "" && !strings.Contains(actualFailureMsg, expectedFailureMsg) { + return fmt.Errorf("Expected failure message '%s', but got: '%s'", expectedFailureMsg, actualFailureMsg) + } + + actualPath := simulateResponse.TxnGroups[groupIndex].FailedAt + if len(expectedPath) != len(actualPath) { + return fmt.Errorf("Expected failure path %v, but got: %v", expectedPath, actualPath) + } + for i := range expectedPath { + if expectedPath[i] != actualPath[i] { + return fmt.Errorf("Expected failure path %v, but got: %v", expectedPath, actualPath) + } + } + + return nil +} diff --git a/test/unit.tags b/test/unit.tags index 6b27ad93..29d3b813 100644 --- a/test/unit.tags +++ b/test/unit.tags @@ -6,6 +6,7 @@ @unit.applications.boxes @unit.atomic_transaction_composer @unit.blocksummary +@unit.blocktxids @unit.dryrun @unit.dryrun.trace.application @unit.feetest @@ -26,6 +27,7 @@ @unit.responses.participationupdates @unit.responses.sync @unit.responses.timestamp +@unit.responses.txid.json @unit.responses.unlimited_assets @unit.sourcemap @unit.statedelta diff --git a/test/utilities.go b/test/utilities.go index 8d9ffd89..fb68cc35 100644 --- a/test/utilities.go +++ b/test/utilities.go @@ -7,7 +7,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "log" "os" "sort" @@ -24,7 +24,7 @@ func VerifyResponse(expectedFile string, actual string) error { if err != nil { return err } - fileBytes, err := ioutil.ReadAll(jsonfile) + fileBytes, err := io.ReadAll(jsonfile) if err != nil { return err } diff --git a/transaction/atomicTransactionComposer.go b/transaction/atomicTransactionComposer.go index 8565d33b..2a1a9eb5 100644 --- a/transaction/atomicTransactionComposer.go +++ b/transaction/atomicTransactionComposer.go @@ -10,6 +10,7 @@ import ( "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" "github.com/algorand/go-algorand-sdk/v2/client/v2/common/models" "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/encoding/msgpack" "github.com/algorand/go-algorand-sdk/v2/types" ) @@ -102,6 +103,16 @@ type AddMethodCallParams struct { BoxReferences []types.AppBoxReference } +// SimulateResult contains the results of calling the Simulate method on an +// AtomicTransactionComposer object. +type SimulateResult struct { + // The result of the transaction group simulation + SimulateResponse models.SimulateResponse + // For each ABI method call in the executed group (created by the AddMethodCall method), this + // slice contains information about the method call's return value + MethodResults []ABIMethodResult +} + // ExecuteResult contains the results of successfully calling the Execute method on an // AtomicTransactionComposer object. type ExecuteResult struct { @@ -573,6 +584,64 @@ func (atc *AtomicTransactionComposer) Submit(client *algod.Client, ctx context.C return atc.getTxIDs(), nil } +// Simulate simulates the transaction group against the network. +// +// The composer's status must be SUBMITTED or lower before calling this method. Simulation will not +// advance the status of the composer beyond SIGNED. +// +// The `request` argument can be used to customize the characteristics of the simulation. +// +// Returns a models.SimulateResponse and an ABIResult for each method call in this group. +func (atc *AtomicTransactionComposer) Simulate(ctx context.Context, client *algod.Client, request models.SimulateRequest) (SimulateResult, error) { + if atc.status > SUBMITTED { + return SimulateResult{}, errors.New("status must be SUBMITTED or lower in order to call Simulate()") + } + + stxs, err := atc.GatherSignatures() + if err != nil { + return SimulateResult{}, err + } + + txnObjects := make([]types.SignedTxn, len(stxs)) + for i, stx := range stxs { + var txnObject types.SignedTxn + err = msgpack.Decode(stx, &txnObject) + if err != nil { + return SimulateResult{}, err + } + txnObjects[i] = txnObject + } + + request.TxnGroups = []models.SimulateRequestTransactionGroup{ + {Txns: txnObjects}, + } + + simulateResponse, err := client.SimulateTransaction(request).Do(ctx) + if err != nil { + return SimulateResult{}, err + } + + result := SimulateResult{ + SimulateResponse: simulateResponse, + } + + for i, txContext := range atc.txContexts { + // Verify method call is available. This may not be the case if the App Call Tx wasn't created + // by AddMethodCall(). + if !txContext.isMethodCallTx() { + continue + } + + methodResult := ABIMethodResult{TxID: txContext.txID(), Method: *txContext.method} + txnInfo := models.PendingTransactionInfoResponse(simulateResponse.TxnGroups[0].TxnResults[i].TxnResult) + + methodResult = prepareMethodResult(methodResult, txnInfo) + result.MethodResults = append(result.MethodResults, methodResult) + } + + return result, nil +} + // Execute sends the transaction group to the network and waits until it's committed to a block. An // error will be thrown if submission or execution fails. // @@ -642,39 +711,42 @@ func (atc *AtomicTransactionComposer) Execute(client *algod.Client, ctx context. result.TransactionInfo = methodCallInfo } - if txContext.method.Returns.IsVoid() { - result.RawReturnValue = []byte{} - executeResponse.MethodResults = append(executeResponse.MethodResults, result) - continue - } + result = prepareMethodResult(result, result.TransactionInfo) + executeResponse.MethodResults = append(executeResponse.MethodResults, result) + } - if len(result.TransactionInfo.Logs) == 0 { - result.DecodeError = errors.New("method call did not log a return value") - executeResponse.MethodResults = append(executeResponse.MethodResults, result) - continue - } + return executeResponse, nil +} - lastLog := result.TransactionInfo.Logs[len(result.TransactionInfo.Logs)-1] - if !bytes.HasPrefix(lastLog, abiReturnHash) { - result.DecodeError = errors.New("method call did not log a return value") - executeResponse.MethodResults = append(executeResponse.MethodResults, result) - continue - } +func prepareMethodResult(result ABIMethodResult, transactionInfo models.PendingTransactionInfoResponse) ABIMethodResult { + result.TransactionInfo = transactionInfo - result.RawReturnValue = lastLog[len(abiReturnHash):] + if result.Method.Returns.IsVoid() { + result.RawReturnValue = []byte{} + return result + } - abiType, err := txContext.method.Returns.GetTypeObject() - if err != nil { - result.DecodeError = err - executeResponse.MethodResults = append(executeResponse.MethodResults, result) - break - } + if len(result.TransactionInfo.Logs) == 0 { + result.DecodeError = errors.New("method call did not log a return value") + return result + } - result.ReturnValue, result.DecodeError = abiType.Decode(result.RawReturnValue) - executeResponse.MethodResults = append(executeResponse.MethodResults, result) + lastLog := result.TransactionInfo.Logs[len(result.TransactionInfo.Logs)-1] + if !bytes.HasPrefix(lastLog, abiReturnHash) { + result.DecodeError = errors.New("method call did not log a return value") + return result } - return executeResponse, nil + result.RawReturnValue = lastLog[len(abiReturnHash):] + + abiType, err := result.Method.Returns.GetTypeObject() + if err != nil { + result.DecodeError = err + return result + } + + result.ReturnValue, result.DecodeError = abiType.Decode(result.RawReturnValue) + return result } // marshallAbiUint64 converts any value used to represent an ABI "uint64" into diff --git a/transaction/transaction.go b/transaction/transaction.go index b79aa548..b7d40715 100644 --- a/transaction/transaction.go +++ b/transaction/transaction.go @@ -317,10 +317,11 @@ func MakeAssetCreateTxn(account string, note []byte, params types.SuggestedParam // MakeAssetConfigTxn creates a tx template for changing the // key configuration of an existing asset. // Important notes - -// * Every asset config transaction is a fresh one. No parameters will be inherited from the current config. -// * Once an address is set to to the empty string, IT CAN NEVER BE CHANGED AGAIN. For example, if you want to keep -// The current manager, you must specify its address again. -// Parameters - +// - Every asset config transaction is a fresh one. No parameters will be inherited from the current config. +// - Once an address is set to to the empty string, IT CAN NEVER BE CHANGED AGAIN. For example, if you want to keep +// The current manager, you must specify its address again. +// Parameters - +// // - account is a checksummed, human-readable address that will send the transaction // - note is an arbitrary byte array // - params is typically received from algod, it defines common-to-all-txns arguments like fee and validity period diff --git a/transaction/transactionSigner.go b/transaction/transactionSigner.go index d23e79b7..951d945d 100644 --- a/transaction/transactionSigner.go +++ b/transaction/transactionSigner.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/encoding/msgpack" "github.com/algorand/go-algorand-sdk/v2/types" ) @@ -11,8 +12,9 @@ import ( // @param txnGroup - The atomic group containing transactions to be signed // @param indexesToSign - An array of indexes in the atomic transaction group that should be signed // @returns An array of encoded signed transactions. The length of the -// array will be the same as the length of indexesToSign, and each index i in the array -// corresponds to the signed transaction from txnGroup[indexesToSign[i]] +// +// array will be the same as the length of indexesToSign, and each index i in the array +// corresponds to the signed transaction from txnGroup[indexesToSign[i]] type TransactionSigner interface { //nolint:revive // Ignore stuttering for backwards compatibility SignTransactions(txGroup []types.Transaction, indexesToSign []int) ([][]byte, error) Equals(other TransactionSigner) bool @@ -148,3 +150,25 @@ func (txSigner MultiSigAccountTransactionSigner) Equals(other TransactionSigner) } return false } + +// EmptyTransactionSigner is a TransactionSigner that produces signed transaction objects without +// signatures. This is useful for simulating transactions, but it won't work for actual submission. +type EmptyTransactionSigner struct{} + +// SignTransactions returns SignedTxn bytes but does not sign them. +func (txSigner EmptyTransactionSigner) SignTransactions(txGroup []types.Transaction, indexesToSign []int) ([][]byte, error) { + stxs := make([][]byte, len(indexesToSign)) + for i, pos := range indexesToSign { + stx := types.SignedTxn{ + Txn: txGroup[pos], + } + stxs[i] = msgpack.Encode(&stx) + } + return stxs, nil +} + +// Equals returns true if the other TransactionSigner equals this one. +func (txSigner EmptyTransactionSigner) Equals(other TransactionSigner) bool { + _, ok := other.(EmptyTransactionSigner) + return ok +} diff --git a/transaction/waitForConfirmation.go b/transaction/waitForConfirmation.go index 9725a6cc..af445a76 100644 --- a/transaction/waitForConfirmation.go +++ b/transaction/waitForConfirmation.go @@ -41,10 +41,9 @@ func WaitForConfirmation(c *algod.Client, txid string, waitRounds uint64, ctx co return } } - // ignore errors from PendingTransactionInformation, since it may return 404 if the algod - // instance is behind a load balancer and the request goes to a different algod than the - // one we submitted the transaction to - err = nil + // Note that we intentionally ignore errors from PendingTransactionInformation, since it + // may return 404 if the algod instance is behind a load balancer and the request goes + // to a different algod than the one we submitted the transaction to // Wait until the block for the `currentRound` is confirmed response, err = c.StatusAfterBlock(currentRound).Do(ctx, headers...) diff --git a/types/address.go b/types/address.go index f2d33019..ea02a72d 100644 --- a/types/address.go +++ b/types/address.go @@ -5,6 +5,7 @@ import ( "crypto/sha512" "encoding/base32" "encoding/base64" + "fmt" ) const ( @@ -64,7 +65,8 @@ func (a *Address) UnmarshalText(text []byte) error { } // DecodeAddress turns a checksum address string into an Address object. It -// checks that the checksum is correct, and returns an error if it's not. +// checks that the checksum is correct and whether the address is canonical, +// and returns an error if it's not. func DecodeAddress(addr string) (a Address, err error) { // Interpret the address as base32 decoded, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(addr) @@ -94,6 +96,13 @@ func DecodeAddress(addr string) (a Address, err error) { // Checksum is good, copy address bytes into output copy(a[:], addressBytes) + + // Check if address is canonical + if a.String() != addr { + err = fmt.Errorf("address %s is non-canonical", addr) + return + } + return a, nil } diff --git a/types/address_test.go b/types/address_test.go index dee6c278..11205a0e 100644 --- a/types/address_test.go +++ b/types/address_test.go @@ -92,3 +92,18 @@ func TestUnmarshalAddress(t *testing.T) { }) } } + +func TestDecodeNonCanonicalAddress(t *testing.T) { + // Canonical addresses must end with one of the following: "AEIMQUY4", + // e.g. "7HJBGRIWI7GDL42SOJNIAZ7LJ7EBEGKGE5S52QZXAWDXOHDKMDFR6AUXDE" + addrs := []string{ + "7HJBGRIWI7GDL42SOJNIAZ7LJ7EBEGKGE5S52QZXAWDXOHDKMDFR6AUXDF", + "7HJBGRIWI7GDL42SOJNIAZ7LJ7EBEGKGE5S52QZXAWDXOHDKMDFR6AUXDG", + "7HJBGRIWI7GDL42SOJNIAZ7LJ7EBEGKGE5S52QZXAWDXOHDKMDFR6AUXDH", + } + for _, addr := range addrs { + _, err := DecodeAddress(addr) + require.Error(t, err) + require.ErrorContains(t, err, fmt.Sprintf("address %s is non-canonical", addr)) + } +} diff --git a/types/applications.go b/types/applications.go index 984dc326..fb9a5a94 100644 --- a/types/applications.go +++ b/types/applications.go @@ -66,6 +66,7 @@ const ( // OnCompletion is an enum representing some layer 1 side effect that an // ApplicationCall transaction will have if it is included in a block. +// //go:generate stringer -type=OnCompletion -output=application_string.go type OnCompletion uint64 diff --git a/types/block.go b/types/block.go index e15cfc9f..2e2a1f6c 100644 --- a/types/block.go +++ b/types/block.go @@ -276,6 +276,7 @@ type EvalDelta struct { // StateDelta is a map from key/value store keys to ValueDeltas, indicating // what should happen for that key +// //msgp:allocbound StateDelta config.MaxStateDeltaKeys type StateDelta map[string]ValueDelta diff --git a/types/statedelta.go b/types/statedelta.go index 22a1b45f..54c71fb5 100644 --- a/types/statedelta.go +++ b/types/statedelta.go @@ -38,6 +38,7 @@ type TealValue struct { // TealKeyValue represents a key/value store for use in an application's // LocalState or GlobalState +// //msgp:allocbound TealKeyValue EncodedMaxKeyValueEntries type TealKeyValue map[string]TealValue diff --git a/types/stateproof.go b/types/stateproof.go index 808ae1f5..c46ce40e 100644 --- a/types/stateproof.go +++ b/types/stateproof.go @@ -34,6 +34,7 @@ const ( ) // GenericDigest is a digest that implements CustomSizeDigest, and can be used as hash output. +// //msgp:allocbound GenericDigest MaxHashDigestSize type GenericDigest []byte @@ -72,6 +73,7 @@ const ( ) // HashFactory is responsible for generating new hashes accordingly to the type it stores. +// //msgp:postunmarshalcheck HashFactory Validate type HashFactory struct { _struct struct{} `codec:",omitempty,omitemptyarray"` @@ -129,6 +131,7 @@ type Participant struct { } // MerkleSignature represents a Falcon signature in a compressed-form +// //msgp:allocbound MerkleSignature FalconMaxSignatureSize type MerkleSignature []byte