Skip to content

Commit 0447a6c

Browse files
authored
Merge pull request #1727 from onetechnical/onetechnical/relbeta2.3.0
go-algorand 2.3.0-beta
2 parents 566405e + 65d5a6a commit 0447a6c

File tree

85 files changed

+4550
-1487
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+4550
-1487
lines changed
File renamed without changes.

Makefile

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
UNAME := $(shell uname)
2-
ifneq (, $(findstring MINGW,$(UNAME)))
2+
ifneq (,$(findstring MINGW,$(UNAME)))
33
#Gopath is not saved across sessions, probably existing Windows env vars, override them
4-
export GOPATH := ${HOME}/go
4+
export GOPATH := $(HOME)/go
55
GOPATH1 := $(GOPATH)
66
export PATH := $(PATH):$(GOPATH)/bin
77
else
@@ -26,6 +26,9 @@ DEFAULT_DEADLOCK ?= $(shell ./scripts/compute_branch_deadlock_default.sh $(BUILD
2626

2727
GOTAGSLIST := sqlite_unlock_notify sqlite_omit_load_extension
2828

29+
# e.g. make GOTAGSCUSTOM=msgtrace
30+
GOTAGSLIST += ${GOTAGSCUSTOM}
31+
2932
ifeq ($(UNAME), Linux)
3033
EXTLDFLAGS := -static-libstdc++ -static-libgcc
3134
ifeq ($(ARCH), amd64)
@@ -46,12 +49,12 @@ endif
4649
endif
4750

4851
ifneq (, $(findstring MINGW,$(UNAME)))
49-
EXTLDFLAGS := -static-libstdc++ -static-libgcc
52+
EXTLDFLAGS := -static -static-libstdc++ -static-libgcc
5053
export GOBUILDMODE := -buildmode=exe
5154
endif
5255

5356
GOTAGS := --tags "$(GOTAGSLIST)"
54-
GOTRIMPATH := $(shell go help build | grep -q .-trimpath && echo -trimpath)
57+
GOTRIMPATH := $(shell GOPATH=$(GOPATH) && go help build | grep -q .-trimpath && echo -trimpath)
5558

5659
GOLDFLAGS_BASE := -X github.com/algorand/go-algorand/config.BuildNumber=$(BUILDNUMBER) \
5760
-X github.com/algorand/go-algorand/config.CommitHash=$(COMMITHASH) \
@@ -62,8 +65,8 @@ GOLDFLAGS_BASE := -X github.com/algorand/go-algorand/config.BuildNumber=$(BUILD
6265
GOLDFLAGS := $(GOLDFLAGS_BASE) \
6366
-X github.com/algorand/go-algorand/config.Channel=$(CHANNEL)
6467

65-
UNIT_TEST_SOURCES := $(sort $(shell GO111MODULE=off go list ./... | grep -v /go-algorand/test/ ))
66-
ALGOD_API_PACKAGES := $(sort $(shell GO111MODULE=off cd daemon/algod/api; go list ./... ))
68+
UNIT_TEST_SOURCES := $(sort $(shell GOPATH=$(GOPATH) && GO111MODULE=off && go list ./... | grep -v /go-algorand/test/ ))
69+
ALGOD_API_PACKAGES := $(sort $(shell GOPATH=$(GOPATH) && GO111MODULE=off && cd daemon/algod/api; go list ./... ))
6770

6871
MSGP_GENERATE := ./protocol ./crypto ./crypto/compactcert ./data/basics ./data/transactions ./data/committee ./data/bookkeeping ./data/hashable ./auction ./agreement ./rpcs ./node ./ledger
6972

@@ -302,5 +305,4 @@ install: build
302305
include ./scripts/release/mule/Makefile.mule
303306

304307
archive:
305-
CHANNEL=$(CHANNEL) \
306-
aws s3 cp tmp/node_pkgs s3://algorand-internal/channel/${CHANNEL}/$(FULLBUILDNUMBER) --recursive --exclude "*" --include "*${CHANNEL}*$(FULLBUILDNUMBER)*"
308+
aws s3 cp tmp/node_pkgs s3://algorand-internal/channel/$(CHANNEL)/$(FULLBUILDNUMBER) --recursive --exclude "*" --include "*$(FULLBUILDNUMBER)*"

agreement/gossip/network.go

+12
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/algorand/go-algorand/agreement"
2626
"github.com/algorand/go-algorand/logging"
2727
"github.com/algorand/go-algorand/network"
28+
"github.com/algorand/go-algorand/network/messagetracer"
2829
"github.com/algorand/go-algorand/protocol"
2930
"github.com/algorand/go-algorand/util/metrics"
3031
)
@@ -50,6 +51,8 @@ type networkImpl struct {
5051

5152
net network.GossipNode
5253
log logging.Logger
54+
55+
trace messagetracer.MessageTracer
5356
}
5457

5558
// WrapNetwork adapts a network.GossipNode into an agreement.Network.
@@ -66,6 +69,12 @@ func WrapNetwork(net network.GossipNode, log logging.Logger) agreement.Network {
6669
return i
6770
}
6871

72+
// SetTrace modifies the result of WrapNetwork to add network propagation tracing
73+
func SetTrace(net agreement.Network, trace messagetracer.MessageTracer) {
74+
i := net.(*networkImpl)
75+
i.trace = trace
76+
}
77+
6978
func (i *networkImpl) Start() {
7079
handlers := []network.TaggedMessageHandler{
7180
{Tag: protocol.AgreementVoteTag, MessageHandler: network.HandlerFunc(i.processVoteMessage)},
@@ -87,6 +96,9 @@ func (i *networkImpl) processVoteMessage(raw network.IncomingMessage) network.Ou
8796
}
8897

8998
func (i *networkImpl) processProposalMessage(raw network.IncomingMessage) network.OutgoingMessage {
99+
if i.trace != nil {
100+
i.trace.HashTrace(messagetracer.Proposal, raw.Data)
101+
}
90102
return i.processMessage(raw, i.proposalCh)
91103
}
92104

catchup/catchpointService.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type CatchpointCatchupStats struct {
4343
CatchpointLabel string
4444
TotalAccounts uint64
4545
ProcessedAccounts uint64
46+
VerifiedAccounts uint64
4647
TotalBlocks uint64
4748
AcquiredBlocks uint64
4849
VerifiedBlocks uint64
@@ -262,7 +263,7 @@ func (cs *CatchpointCatchupService) processStageLedgerDownload() (err error) {
262263
}
263264

264265
// download balances file.
265-
ledgerFetcher := makeLedgerFetcher(cs.net, cs.ledgerAccessor, cs.log, cs)
266+
ledgerFetcher := makeLedgerFetcher(cs.net, cs.ledgerAccessor, cs.log, cs, cs.config)
266267
attemptsCount := 0
267268

268269
for {
@@ -277,11 +278,15 @@ func (cs *CatchpointCatchupService) processStageLedgerDownload() (err error) {
277278
}
278279
err = ledgerFetcher.downloadLedger(cs.ctx, round)
279280
if err == nil {
280-
break
281+
err = cs.ledgerAccessor.BuildMerkleTrie(cs.ctx, cs.updateVerifiedAccounts)
282+
if err == nil {
283+
break
284+
}
285+
// failed to build the merkle trie for the above catchpoint file.
281286
}
282287
// instead of testing for err == cs.ctx.Err() , we'll check on the context itself.
283288
// this is more robust, as the http client library sometimes wrap the context canceled
284-
// error with other erros.
289+
// error with other errors.
285290
if cs.ctx.Err() != nil {
286291
return cs.stopOrAbort()
287292
}
@@ -300,6 +305,13 @@ func (cs *CatchpointCatchupService) processStageLedgerDownload() (err error) {
300305
return nil
301306
}
302307

308+
// updateVerifiedAccounts update the user's statistics for the given verified accounts
309+
func (cs *CatchpointCatchupService) updateVerifiedAccounts(verifiedAccounts uint64) {
310+
cs.statsMu.Lock()
311+
defer cs.statsMu.Unlock()
312+
cs.stats.VerifiedAccounts = verifiedAccounts
313+
}
314+
303315
// processStageLastestBlockDownload is the third catchpoint catchup stage. It downloads the latest block and verify that against the previously downloaded ledger.
304316
func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err error) {
305317
blockRound, err := cs.ledgerAccessor.GetCatchupBlockRound(cs.ctx)
@@ -328,6 +340,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro
328340
if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts {
329341
// try again.
330342
blk = nil
343+
cs.log.Infof("processStageLastestBlockDownload: block %d download failed, another attempt will be made; err = %v", blockRound, err)
331344
continue
332345
}
333346
return cs.abort(fmt.Errorf("processStageLastestBlockDownload failed to get block %d : %v", blockRound, err))
@@ -369,6 +382,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro
369382
if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts {
370383
// try again.
371384
blk = nil
385+
cs.log.Infof("processStageLastestBlockDownload: block %d verification against catchpoint failed, another attempt will be made; err = %v", blockRound, err)
372386
continue
373387
}
374388
return cs.abort(fmt.Errorf("processStageLastestBlockDownload failed when calling VerifyCatchpoint : %v", err))

catchup/ledgerFetcher.go

+17-8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"strconv"
2828
"time"
2929

30+
"github.com/algorand/go-algorand/config"
3031
"github.com/algorand/go-algorand/data/basics"
3132
"github.com/algorand/go-algorand/ledger"
3233
"github.com/algorand/go-algorand/logging"
@@ -39,12 +40,8 @@ var errNoLedgerForRound = errors.New("No ledger available for given round")
3940
const (
4041
// maxCatchpointFileChunkSize is a rough estimate for the worst-case scenario we're going to have of all the accounts data per a single catchpoint file chunk.
4142
maxCatchpointFileChunkSize = ledger.BalancesPerCatchpointFileChunk * basics.MaxEncodedAccountDataSize
42-
// maxCatchpointFileDownloadDuration is the maximum time we would wait for download an entire catchpoint file
43-
maxCatchpointFileDownloadDuration = 45 * time.Minute
44-
// expectedWorstDownloadSpeedBytesPerSecond defines the worst case-scenario download speed we expect to get while downloading a catchpoint file
45-
expectedWorstDownloadSpeedBytesPerSecond = 20 * 1024
46-
// maxCatchpointFileChunkDownloadDuration is the maximum amount of time we would wait to download a single chunk off a catchpoint file
47-
maxCatchpointFileChunkDownloadDuration = 2*time.Minute + maxCatchpointFileChunkSize*time.Second/expectedWorstDownloadSpeedBytesPerSecond
43+
// defaultMinCatchpointFileDownloadBytesPerSecond defines the worst-case scenario download speed we expect to get while downloading a catchpoint file
44+
defaultMinCatchpointFileDownloadBytesPerSecond = 20 * 1024
4845
// catchpointFileStreamReadSize defines the number of bytes we would attempt to read at each itration from the incoming http data stream
4946
catchpointFileStreamReadSize = 4096
5047
)
@@ -62,14 +59,16 @@ type ledgerFetcher struct {
6259
log logging.Logger
6360
peers []network.Peer
6461
reporter ledgerFetcherReporter
62+
config config.Local
6563
}
6664

67-
func makeLedgerFetcher(net network.GossipNode, accessor ledger.CatchpointCatchupAccessor, log logging.Logger, reporter ledgerFetcherReporter) *ledgerFetcher {
65+
func makeLedgerFetcher(net network.GossipNode, accessor ledger.CatchpointCatchupAccessor, log logging.Logger, reporter ledgerFetcherReporter, cfg config.Local) *ledgerFetcher {
6866
return &ledgerFetcher{
6967
net: net,
7068
accessor: accessor,
7169
log: log,
7270
reporter: reporter,
71+
config: cfg,
7372
}
7473
}
7574

@@ -101,7 +100,8 @@ func (lf *ledgerFetcher) getPeerLedger(ctx context.Context, peer network.HTTPPee
101100
if err != nil {
102101
return err
103102
}
104-
timeoutContext, timeoutContextCancel := context.WithTimeout(ctx, maxCatchpointFileDownloadDuration)
103+
104+
timeoutContext, timeoutContextCancel := context.WithTimeout(ctx, lf.config.MaxCatchpointDownloadDuration)
105105
defer timeoutContextCancel()
106106
request = request.WithContext(timeoutContext)
107107
network.SetUserAgentHeader(request.Header)
@@ -133,6 +133,15 @@ func (lf *ledgerFetcher) getPeerLedger(ctx context.Context, peer network.HTTPPee
133133
err = fmt.Errorf("getPeerLedger : http ledger fetcher response has an invalid content type : %s", contentTypes[0])
134134
return err
135135
}
136+
137+
// maxCatchpointFileChunkDownloadDuration is the maximum amount of time we would wait to download a single chunk off a catchpoint file
138+
maxCatchpointFileChunkDownloadDuration := 2 * time.Minute
139+
if lf.config.MinCatchpointFileDownloadBytesPerSecond > 0 {
140+
maxCatchpointFileChunkDownloadDuration += maxCatchpointFileChunkSize * time.Second / time.Duration(lf.config.MinCatchpointFileDownloadBytesPerSecond)
141+
} else {
142+
maxCatchpointFileChunkDownloadDuration += maxCatchpointFileChunkSize * time.Second / defaultMinCatchpointFileDownloadBytesPerSecond
143+
}
144+
136145
watchdogReader := makeWatchdogStreamReader(response.Body, catchpointFileStreamReadSize, 2*maxCatchpointFileChunkSize, maxCatchpointFileChunkDownloadDuration)
137146
defer watchdogReader.Close()
138147
tarReader := tar.NewReader(watchdogReader)

catchup/ledgerFetcher_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/stretchr/testify/require"
2727

2828
"github.com/algorand/go-algorand/components/mocks"
29+
"github.com/algorand/go-algorand/config"
2930
"github.com/algorand/go-algorand/data/basics"
3031
"github.com/algorand/go-algorand/ledger"
3132
"github.com/algorand/go-algorand/logging"
@@ -38,7 +39,7 @@ func (lf *dummyLedgerFetcherReporter) updateLedgerFetcherProgress(*ledger.Catchp
3839
}
3940

4041
func TestNoPeersAvailable(t *testing.T) {
41-
lf := makeLedgerFetcher(&mocks.MockNetwork{}, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{})
42+
lf := makeLedgerFetcher(&mocks.MockNetwork{}, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{}, config.GetDefaultLocal())
4243
err := lf.downloadLedger(context.Background(), basics.Round(0))
4344
require.Equal(t, errNoPeersAvailable, err)
4445
lf.peers = append(lf.peers, &lf) // The peer is an opaque interface.. we can add anything as a Peer.
@@ -47,7 +48,7 @@ func TestNoPeersAvailable(t *testing.T) {
4748
}
4849

4950
func TestNonParsableAddress(t *testing.T) {
50-
lf := makeLedgerFetcher(&mocks.MockNetwork{}, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{})
51+
lf := makeLedgerFetcher(&mocks.MockNetwork{}, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{}, config.GetDefaultLocal())
5152
peer := testHTTPPeer(":def")
5253
err := lf.getPeerLedger(context.Background(), &peer, basics.Round(0))
5354
require.Error(t, err)
@@ -75,7 +76,7 @@ func TestLedgerFetcherErrorResponseHandling(t *testing.T) {
7576
w.WriteHeader(httpServerResponse)
7677
})
7778

78-
lf := makeLedgerFetcher(&mocks.MockNetwork{}, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{})
79+
lf := makeLedgerFetcher(&mocks.MockNetwork{}, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{}, config.GetDefaultLocal())
7980
peer := testHTTPPeer(listener.Addr().String())
8081
err = lf.getPeerLedger(context.Background(), &peer, basics.Round(0))
8182
require.Equal(t, errNoLedgerForRound, err)

cmd/goal/account.go

+4
Original file line numberDiff line numberDiff line change
@@ -570,10 +570,12 @@ func printAccountInfo(client libgoal.Client, address string, account v1.Account)
570570
if len(name) == 0 {
571571
name = "<unnamed>"
572572
}
573+
_, name = unicodePrintable(name)
573574
units := assetParams.UnitName
574575
if len(units) == 0 {
575576
units = "units"
576577
}
578+
_, units = unicodePrintable(units)
577579
total := assetDecimalsFmt(assetParams.Total, assetParams.Decimals)
578580
url := ""
579581
if len(assetParams.URL) != 0 {
@@ -602,11 +604,13 @@ func printAccountInfo(client libgoal.Client, address string, account v1.Account)
602604
if len(assetName) == 0 {
603605
assetName = "<unnamed>"
604606
}
607+
_, assetName = unicodePrintable(assetName)
605608

606609
unitName := assetParams.UnitName
607610
if len(unitName) == 0 {
608611
unitName = "units"
609612
}
613+
_, unitName = unicodePrintable(unitName)
610614

611615
frozen := ""
612616
if assetHolding.Frozen {

cmd/goal/accountsList.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@ func (accountList *AccountsList) outputAccount(addr string, acctInfo v1.Account,
228228
if len(acctInfo.AssetParams) > 0 {
229229
var out []string
230230
for curid, params := range acctInfo.AssetParams {
231-
out = append(out, fmt.Sprintf("%d (%d %s)", curid, params.Total, params.UnitName))
231+
_, unitName := unicodePrintable(params.UnitName)
232+
out = append(out, fmt.Sprintf("%d (%d %s)", curid, params.Total, unitName))
232233
}
233234
fmt.Printf("\t[created asset IDs: %s]", strings.Join(out, ", "))
234235
}

cmd/goal/asset.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func init() {
6767
createAssetCmd.MarkFlagRequired("creator")
6868

6969
destroyAssetCmd.Flags().StringVar(&assetManager, "manager", "", "Manager account to issue the destroy transaction (defaults to creator)")
70-
destroyAssetCmd.Flags().StringVar(&assetCreator, "creator", "", "Account address for asset to destroy")
70+
destroyAssetCmd.Flags().StringVar(&assetCreator, "creator", "", "Creator account address for asset to destroy")
7171
destroyAssetCmd.Flags().Uint64Var(&assetID, "assetid", 0, "Asset ID to destroy")
7272
destroyAssetCmd.Flags().StringVar(&assetUnitName, "asset", "", "Unit name of asset to destroy")
7373

@@ -256,7 +256,7 @@ var createAssetCmd = &cobra.Command{
256256
var destroyAssetCmd = &cobra.Command{
257257
Use: "destroy",
258258
Short: "Destroy an asset",
259-
Long: `Issue a transaction deleting an asset from the network. This transaction must be issued by the asset owner, who must hold all outstanding asset tokens.`,
259+
Long: `Issue a transaction deleting an asset from the network. This transaction must be issued by the asset manager while the creator holds all of the asset's tokens.`,
260260
Args: validateNoPosArgsFn,
261261
Run: func(cmd *cobra.Command, _ []string) {
262262
checkTxValidityPeriodCmdFlags(cmd)
@@ -603,8 +603,8 @@ var infoAssetCmd = &cobra.Command{
603603

604604
fmt.Printf("Asset ID: %d\n", assetID)
605605
fmt.Printf("Creator: %s\n", params.Creator)
606-
fmt.Printf("Asset name: %s\n", params.AssetName)
607-
fmt.Printf("Unit name: %s\n", params.UnitName)
606+
reportInfof("Asset name: %s\n", params.AssetName)
607+
reportInfof("Unit name: %s\n", params.UnitName)
608608
fmt.Printf("Maximum issue: %s %s\n", assetDecimalsFmt(params.Total, params.Decimals), params.UnitName)
609609
fmt.Printf("Reserve amount: %s %s\n", assetDecimalsFmt(res.Amount, params.Decimals), params.UnitName)
610610
fmt.Printf("Issued: %s %s\n", assetDecimalsFmt(params.Total-res.Amount, params.Decimals), params.UnitName)

cmd/goal/messages.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ const (
6464
infoNodeStatus = "Last committed block: %d\nTime since last block: %s\nSync Time: %s\nLast consensus protocol: %s\nNext consensus protocol: %s\nRound for next consensus protocol: %d\nNext consensus protocol supported: %v"
6565
catchupStoppedOnUnsupported = "Last supported block (%d) is committed. The next block consensus protocol is not supported. Catchup service is stopped."
6666
infoNodeCatchpointCatchupStatus = "Last committed block: %d\nSync Time: %s\nCatchpoint: %s"
67-
infoNodeCatchpointCatchupAccounts = "Catchpoint total accounts: %d\nCatchpoint accounts processed: %d"
67+
infoNodeCatchpointCatchupAccounts = "Catchpoint total accounts: %d\nCatchpoint accounts processed: %d\nCatchpoint accounts verified: %d"
6868
infoNodeCatchpointCatchupBlocks = "Catchpoint total blocks: %d\nCatchpoint downloaded blocks: %d"
6969
nodeLastCatchpoint = "Last Catchpoint: %s"
7070
errorNodeCreationIPFailure = "Parsing passed IP %v failed: need a valid IPv4 or IPv6 address with a specified port number"
7171
errorNodeNotDetected = "Algorand node does not appear to be running: %s"
72-
errorNodeStatus = "Cannot contact Algorand node: %s."
72+
errorNodeStatus = "Cannot contact Algorand node: %s"
7373
errorNodeFailedToStart = "Algorand node failed to start: %s"
7474
errorNodeRunning = "Node must be stopped before writing APIToken"
7575
errorNodeFailGenToken = "Cannot generate API token: %s"

cmd/goal/node.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ func makeStatusString(stat generatedV2.NodeStatusResponse) string {
423423

424424
if stat.CatchpointTotalAccounts != nil && (*stat.CatchpointTotalAccounts > 0) && stat.CatchpointProcessedAccounts != nil {
425425
statusString = statusString + "\n" + fmt.Sprintf(infoNodeCatchpointCatchupAccounts, *stat.CatchpointTotalAccounts,
426-
*stat.CatchpointProcessedAccounts)
426+
*stat.CatchpointProcessedAccounts, *stat.CatchpointVerifiedAccounts)
427427
}
428428
if stat.CatchpointAcquiredBlocks != nil && stat.CatchpointTotalBlocks != nil && (*stat.CatchpointAcquiredBlocks+*stat.CatchpointTotalBlocks > 0) {
429429
statusString = statusString + "\n" + fmt.Sprintf(infoNodeCatchpointCatchupBlocks, *stat.CatchpointTotalBlocks,

cmd/opdoc/opdoc.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,17 @@ func opToMarkdown(out io.Writer, op *logic.OpSpec) (err error) {
120120
}
121121
out.Write([]byte("\n"))
122122
}
123+
123124
if op.Returns == nil {
124125
fmt.Fprintf(out, "- Pushes: _None_\n")
125126
} else {
126-
fmt.Fprintf(out, "- Pushes: %s", op.Returns[0].String())
127-
for _, rt := range op.Returns[1:] {
128-
fmt.Fprintf(out, ", %s", rt.String())
127+
if len(op.Returns) == 1 {
128+
fmt.Fprintf(out, "- Pushes: %s", op.Returns[0].String())
129+
} else {
130+
fmt.Fprintf(out, "- Pushes: *... stack*, %s", op.Returns[0].String())
131+
for _, rt := range op.Returns[1:] {
132+
fmt.Fprintf(out, ", %s", rt.String())
133+
}
129134
}
130135
fmt.Fprintf(out, "\n")
131136
}

0 commit comments

Comments
 (0)