summaryrefslogtreecommitdiff
path: root/ledger/simulation/simulator.go
blob: 0a33e5f12b7c3e98aafe3bc3e34ba4cc5674a5ff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
// Copyright (C) 2019-2024 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand.  If not, see <https://www.gnu.org/licenses/>.

package simulation

import (
	"errors"
	"fmt"

	"github.com/algorand/go-algorand/crypto"
	"github.com/algorand/go-algorand/data"
	"github.com/algorand/go-algorand/data/basics"
	"github.com/algorand/go-algorand/data/bookkeeping"
	"github.com/algorand/go-algorand/data/transactions"
	"github.com/algorand/go-algorand/data/transactions/logic"
	"github.com/algorand/go-algorand/data/transactions/verify"
	"github.com/algorand/go-algorand/ledger/eval"
	"github.com/algorand/go-algorand/ledger/ledgercore"
	"github.com/algorand/go-algorand/protocol"
)

// Request packs simulation related txn-group(s), and configurations that are overlapping the ones in real transactions.
type Request struct {
	Round                 basics.Round
	TxnGroups             [][]transactions.SignedTxn
	AllowEmptySignatures  bool
	AllowMoreLogging      bool
	AllowUnnamedResources bool
	ExtraOpcodeBudget     uint64
	TraceConfig           ExecTraceConfig
}

// simulatorLedger patches the ledger interface to use a constant latest round.
type simulatorLedger struct {
	*data.Ledger
	start basics.Round
}

// Latest is part of the ledger.Ledger interface.
// We override this to use the set latest to prevent racing with the network
func (l simulatorLedger) Latest() basics.Round {
	return l.start
}

// LatestTotals is part of the ledger.Ledger interface.
func (l simulatorLedger) LatestTotals() (basics.Round, ledgercore.AccountTotals, error) {
	totals, err := l.Totals(l.start)
	return l.start, totals, err
}

// LookupLatest would implicitly use the latest round in the _underlying_
// Ledger, it would give wrong results if that ledger has moved forward. But it
// should never be called, as the REST API is the only code using this function,
// and the REST API should never have access to a simulatorLedger.
func (l simulatorLedger) LookupLatest(addr basics.Address) (basics.AccountData, basics.Round, basics.MicroAlgos, error) {
	err := errors.New("unexpected call to LookupLatest")
	return basics.AccountData{}, 0, basics.MicroAlgos{}, err
}

// StartEvaluator is part of the ledger.Ledger interface. We override this so that
// the eval.LedgerForEvaluator value passed into eval.StartEvaluator is a simulatorLedger,
// not a data.Ledger. This ensures our overridden LookupLatest method will be used.
func (l simulatorLedger) StartEvaluator(hdr bookkeeping.BlockHeader, paysetHint, maxTxnBytesPerBlock int, tracer logic.EvalTracer) (*eval.BlockEvaluator, error) {
	if tracer == nil {
		return nil, errors.New("tracer is nil")
	}
	return eval.StartEvaluator(&l, hdr,
		eval.EvaluatorOptions{
			PaysetHint:          paysetHint,
			Generate:            true,
			Validate:            true,
			MaxTxnBytesPerBlock: maxTxnBytesPerBlock,
			Tracer:              tracer,
		})
}

// SimulatorError is the base error type for all simulator errors.
type SimulatorError struct {
	err error
}

func (s SimulatorError) Error() string {
	return s.err.Error()
}

func (s SimulatorError) Unwrap() error {
	return s.err
}

// InvalidRequestError occurs when an invalid transaction group was submitted to the simulator.
type InvalidRequestError struct {
	SimulatorError
}

// EvalFailureError represents an error that occurred during evaluation.
type EvalFailureError struct {
	SimulatorError
}

// Simulator is a transaction group simulator for the block evaluator.
type Simulator struct {
	ledger       simulatorLedger
	developerAPI bool
}

// MakeSimulator creates a new simulator from a ledger.
func MakeSimulator(ledger *data.Ledger, developerAPI bool) *Simulator {
	return &Simulator{
		ledger:       simulatorLedger{ledger, 0}, // start round to be specified in Simulate method
		developerAPI: developerAPI,
	}
}

func txnHasNoSignature(txn transactions.SignedTxn) bool {
	return txn.Sig.Blank() && txn.Msig.Blank() && txn.Lsig.Blank()
}

// A randomly generated private key. The actual value does not matter, as long as this is a valid
// private key.
var proxySigner = crypto.PrivateKey{
	128, 128, 92, 23, 212, 119, 175, 51, 157, 2, 165,
	215, 137, 37, 82, 42, 52, 227, 54, 41, 243, 67,
	141, 76, 208, 17, 199, 17, 140, 46, 113, 0, 159,
	50, 105, 52, 77, 104, 118, 200, 104, 220, 105, 21,
	147, 162, 191, 236, 115, 201, 197, 128, 8, 91, 224,
	78, 104, 209, 2, 185, 110, 28, 42, 97,
}

// check verifies that the transaction is well-formed and has valid or missing signatures.
// An invalid transaction group error is returned if the transaction is not well-formed or there are invalid signatures.
// To make things easier, we support submitting unsigned transactions and will respond whether signatures are missing.
func (s Simulator) check(hdr bookkeeping.BlockHeader, txgroup []transactions.SignedTxn, tracer logic.EvalTracer, overrides ResultEvalOverrides) error {
	proxySignerSecrets, err := crypto.SecretKeyToSignatureSecrets(proxySigner)
	if err != nil {
		return err
	}

	// If signaturesOptional is enabled, find and prep any transactions that are missing signatures.
	// We will modify a copy of these transactions to pass signature verification. The modifications
	// will not affect the input txgroup slice.
	//
	// Note: currently we only support missing transaction signatures, but it should be possible to
	// support unsigned delegated LogicSigs as well. A single-signature unsigned delegated LogicSig
	// is indistinguishable from an escrow LogicSig, so we would need to decide on another way of
	// denoting that a LogicSig's delegation signature is omitted, e.g. by setting all the bits of
	// the signature.
	txnsToVerify := make([]transactions.SignedTxn, len(txgroup))
	for i, stxn := range txgroup {
		if stxn.Txn.Type == protocol.StateProofTx {
			return errors.New("cannot simulate StateProof transactions")
		}
		if overrides.AllowEmptySignatures && txnHasNoSignature(stxn) {
			// Replace the signed txn with one signed by the proxySigner. At evaluation this would
			// raise an error, since the proxySigner's public key likely does not have authority
			// over the sender's account. However, this will pass validation, since the signature
			// itself is valid.
			txnsToVerify[i] = stxn.Txn.Sign(proxySignerSecrets)
		} else {
			txnsToVerify[i] = stxn
		}
	}

	// Verify the signed transactions are well-formed and have valid signatures
	_, err = verify.TxnGroupWithTracer(txnsToVerify, &hdr, nil, s.ledger, tracer)
	if err != nil {
		err = InvalidRequestError{SimulatorError{err}}
	}
	return err
}

func (s Simulator) evaluate(hdr bookkeeping.BlockHeader, stxns []transactions.SignedTxn, tracer logic.EvalTracer) (*ledgercore.ValidatedBlock, error) {
	// s.ledger has 'StartEvaluator' because *data.Ledger is embedded in the simulatorLedger
	// and data.Ledger embeds *ledger.Ledger
	eval, err := s.ledger.StartEvaluator(hdr, len(stxns), 0, tracer)
	if err != nil {
		return nil, err
	}

	group := transactions.WrapSignedTxnsWithAD(stxns)

	err = eval.TransactionGroup(group)
	if err != nil {
		return nil, EvalFailureError{SimulatorError{err}}
	}

	// Finally, process any pending end-of-block state changes.
	vb, err := eval.GenerateBlock()
	if err != nil {
		return nil, err
	}

	return vb, nil
}

func (s Simulator) simulateWithTracer(txgroup []transactions.SignedTxn, tracer logic.EvalTracer, overrides ResultEvalOverrides) (*ledgercore.ValidatedBlock, error) {
	prevBlockHdr, err := s.ledger.BlockHdr(s.ledger.start)
	if err != nil {
		return nil, err
	}
	nextBlock := bookkeeping.MakeBlock(prevBlockHdr)
	hdr := nextBlock.BlockHeader

	// check that the transaction is well-formed and mark whether signatures are missing
	err = s.check(hdr, txgroup, tracer, overrides)
	if err != nil {
		return nil, err
	}

	// check that the extra budget is not exceeding simulation extra budget limit
	if overrides.ExtraOpcodeBudget > MaxExtraOpcodeBudget {
		return nil, InvalidRequestError{
			SimulatorError{
				fmt.Errorf(
					"extra budget %d > simulation extra budget limit %d",
					overrides.ExtraOpcodeBudget, MaxExtraOpcodeBudget),
			},
		}
	}

	vb, err := s.evaluate(hdr, txgroup, tracer)
	return vb, err
}

// Simulate simulates a transaction group using the simulator. Will error if the transaction group is not well-formed.
func (s Simulator) Simulate(simulateRequest Request) (Result, error) {
	if simulateRequest.Round != 0 {
		s.ledger.start = simulateRequest.Round
	} else {
		// Access underlying data.Ledger to get the real latest round
		s.ledger.start = s.ledger.Ledger.Latest()
	}

	simulatorTracer, err := makeEvalTracer(s.ledger.start, simulateRequest, s.developerAPI)
	if err != nil {
		return Result{}, err
	}

	if len(simulateRequest.TxnGroups) != 1 {
		return Result{}, InvalidRequestError{
			SimulatorError{
				err: fmt.Errorf("expected 1 transaction group, got %d", len(simulateRequest.TxnGroups)),
			},
		}
	}

	block, err := s.simulateWithTracer(simulateRequest.TxnGroups[0], simulatorTracer, simulatorTracer.result.EvalOverrides)
	if err != nil {
		var verifyError *verify.TxGroupError
		switch {
		case errors.As(err, &verifyError):
			if verifyError.GroupIndex < 0 {
				// This group failed verification, but the problem can't be blamed on a single transaction.
				return Result{}, InvalidRequestError{SimulatorError{err}}
			}
			simulatorTracer.result.TxnGroups[0].FailureMessage = verifyError.Error()
			simulatorTracer.result.TxnGroups[0].FailedAt = TxnPath{uint64(verifyError.GroupIndex)}
		case errors.As(err, &EvalFailureError{}):
			simulatorTracer.result.TxnGroups[0].FailureMessage = err.Error()
			simulatorTracer.result.TxnGroups[0].FailedAt = simulatorTracer.failedAt
		default:
			// error is not related to evaluation
			return Result{}, err
		}
	}

	if simulatorTracer.result.TxnGroups[0].UnnamedResourcesAccessed != nil {
		// Remove private fields for easier test comparison
		simulatorTracer.result.TxnGroups[0].UnnamedResourcesAccessed.removePrivateFields()
		if !simulatorTracer.result.TxnGroups[0].UnnamedResourcesAccessed.HasResources() {
			simulatorTracer.result.TxnGroups[0].UnnamedResourcesAccessed = nil
		}
		for i := range simulatorTracer.result.TxnGroups[0].Txns {
			txnResult := &simulatorTracer.result.TxnGroups[0].Txns[i]
			txnResult.UnnamedResourcesAccessed.removePrivateFields()
			if !txnResult.UnnamedResourcesAccessed.HasResources() {
				// Clean up any unused local resource assignments
				txnResult.UnnamedResourcesAccessed = nil
			}
		}
	}

	simulatorTracer.result.Block = block

	// Update total cost by aggregating individual txn costs
	totalCost := uint64(0)
	for _, txn := range simulatorTracer.result.TxnGroups[0].Txns {
		totalCost += txn.AppBudgetConsumed
	}
	simulatorTracer.result.TxnGroups[0].AppBudgetConsumed = totalCost

	return *simulatorTracer.result, nil
}