Skip to content

Commit 160bcf3

Browse files
authored
Txn: Refactor some stateless checks on transactions (#6287)
1 parent 5b9eee2 commit 160bcf3

31 files changed

+2238
-2168
lines changed

daemon/algod/api/server/v2/dryrun.go

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package v2
1818

1919
import (
20-
"encoding/base64"
2120
"fmt"
2221
"strings"
2322

@@ -559,21 +558,8 @@ func doDryrunRequest(dr *DryrunRequest, response *model.DryrunResponse) {
559558
}
560559
result.Disassembly = debug.lines
561560
result.AppCallTrace = &debug.history
562-
result.GlobalDelta = StateDeltaToStateDelta(delta.GlobalDelta)
563-
if len(delta.LocalDeltas) > 0 {
564-
localDeltas := make([]model.AccountStateDelta, 0, len(delta.LocalDeltas))
565-
for k, v := range delta.LocalDeltas {
566-
ldaddr, err2 := stxn.Txn.AddressByIndex(k, stxn.Txn.Sender)
567-
if err2 != nil {
568-
messages = append(messages, err2.Error())
569-
}
570-
localDeltas = append(localDeltas, model.AccountStateDelta{
571-
Address: ldaddr.String(),
572-
Delta: *StateDeltaToStateDelta(v),
573-
})
574-
}
575-
result.LocalDeltas = &localDeltas
576-
}
561+
result.GlobalDelta = sliceOrNil(globalDeltaToStateDelta(delta.GlobalDelta))
562+
result.LocalDeltas = sliceOrNil(localDeltasToLocalDeltas(delta, &stxn.Txn))
577563

578564
// ensure the program has not exceeded execution budget
579565
cost := maxCurrentBudget - pooledAppBudget
@@ -620,33 +606,6 @@ func doDryrunRequest(dr *DryrunRequest, response *model.DryrunResponse) {
620606
}
621607
}
622608

623-
// StateDeltaToStateDelta converts basics.StateDelta to model.StateDelta
624-
func StateDeltaToStateDelta(sd basics.StateDelta) *model.StateDelta {
625-
if len(sd) == 0 {
626-
return nil
627-
}
628-
629-
gsd := make(model.StateDelta, 0, len(sd))
630-
for k, v := range sd {
631-
value := model.EvalDelta{Action: uint64(v.Action)}
632-
if v.Action == basics.SetBytesAction {
633-
bytesVal := base64.StdEncoding.EncodeToString([]byte(v.Bytes))
634-
value.Bytes = &bytesVal
635-
} else if v.Action == basics.SetUintAction {
636-
uintVal := v.Uint
637-
value.Uint = &uintVal
638-
}
639-
// basics.DeleteAction does not require Uint/Bytes
640-
kv := model.EvalDeltaKeyValue{
641-
Key: base64.StdEncoding.EncodeToString([]byte(k)),
642-
Value: value,
643-
}
644-
gsd = append(gsd, kv)
645-
}
646-
647-
return &gsd
648-
}
649-
650609
// DeltaLogToLog base64 encode the logs
651610
func DeltaLogToLog(logs []string) (*[][]byte, error) {
652611
if len(logs) == 0 {

daemon/algod/api/server/v2/dryrun_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,12 +1084,12 @@ func TestStateDeltaToStateDelta(t *testing.T) {
10841084
Action: basics.DeleteAction,
10851085
},
10861086
}
1087-
gsd := StateDeltaToStateDelta(sd)
1088-
require.Equal(t, 3, len(*gsd))
1087+
gsd := globalDeltaToStateDelta(sd)
1088+
require.Equal(t, 3, len(gsd))
10891089

10901090
var keys []string
10911091
// test with a loop because sd is a map and iteration order is random
1092-
for _, item := range *gsd {
1092+
for _, item := range gsd {
10931093
if item.Key == b64("byteskey") {
10941094
require.Equal(t, uint64(1), item.Value.Action)
10951095
require.Nil(t, item.Value.Uint)

daemon/algod/api/server/v2/handlers.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ type LedgerForAPI interface {
118118
GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error)
119119
EncodedBlockCert(rnd basics.Round) (blk []byte, cert []byte, err error)
120120
Block(rnd basics.Round) (blk bookkeeping.Block, err error)
121-
AddressTxns(id basics.Address, r basics.Round) ([]transactions.SignedTxnWithAD, error)
121+
TxnsFrom(id basics.Address, r basics.Round) ([]transactions.Transaction, error)
122122
GetStateDeltaForRound(rnd basics.Round) (ledgercore.StateDelta, error)
123123
GetTracer() logic.EvalTracer
124124
}
@@ -229,18 +229,18 @@ func GetStateProofTransactionForRound(ctx context.Context, txnFetcher LedgerForA
229229
default:
230230
}
231231

232-
txns, err := txnFetcher.AddressTxns(transactions.StateProofSender, i)
232+
txns, err := txnFetcher.TxnsFrom(transactions.StateProofSender, i)
233233
if err != nil {
234234
return transactions.Transaction{}, err
235235
}
236236
for _, txn := range txns {
237-
if txn.Txn.Type != protocol.StateProofTx {
237+
if txn.Type != protocol.StateProofTx {
238238
continue
239239
}
240240

241-
if txn.Txn.StateProofTxnFields.Message.FirstAttestedRound <= uint64(round) &&
242-
uint64(round) <= txn.Txn.StateProofTxnFields.Message.LastAttestedRound {
243-
return txn.Txn, nil
241+
if txn.StateProofTxnFields.Message.FirstAttestedRound <= uint64(round) &&
242+
uint64(round) <= txn.StateProofTxnFields.Message.LastAttestedRound {
243+
return txn, nil
244244
}
245245
}
246246
}
@@ -1574,7 +1574,8 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string,
15741574
response.CloseRewards = &txn.ApplyData.CloseRewards.Raw
15751575
response.AssetIndex = computeAssetIndexFromTxn(txn, v2.Node.LedgerForAPI())
15761576
response.ApplicationIndex = computeAppIndexFromTxn(txn, v2.Node.LedgerForAPI())
1577-
response.LocalStateDelta, response.GlobalStateDelta = convertToDeltas(txn)
1577+
response.LocalStateDelta = sliceOrNil(localDeltasToLocalDeltas(txn.ApplyData.EvalDelta, &txn.Txn.Txn))
1578+
response.GlobalStateDelta = sliceOrNil(globalDeltaToStateDelta(txn.ApplyData.EvalDelta.GlobalDelta))
15781579
response.Logs = convertLogs(txn)
15791580
response.Inners = convertInners(&txn)
15801581
}
@@ -1623,9 +1624,6 @@ func (v2 *Handlers) getPendingTransactions(ctx echo.Context, max *uint64, format
16231624
return internalError(ctx, err, errFailedLookingUpTransactionPool, v2.Log)
16241625
}
16251626

1626-
// MatchAddress uses this to check FeeSink, we don't care about that here.
1627-
spec := transactions.SpecialAddresses{}
1628-
16291627
txnLimit := uint64(math.MaxUint64)
16301628
if max != nil && *max != 0 {
16311629
txnLimit = *max
@@ -1640,7 +1638,7 @@ func (v2 *Handlers) getPendingTransactions(ctx echo.Context, max *uint64, format
16401638
}
16411639

16421640
// continue if we have an address filter and the address doesn't match the transaction.
1643-
if addrPtr != nil && !txn.Txn.MatchAddress(*addrPtr, spec) {
1641+
if addrPtr != nil && !txn.Txn.MatchAddress(*addrPtr) {
16441642
continue
16451643
}
16461644

daemon/algod/api/server/v2/test/handlers_resources_test.go

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,20 +181,11 @@ func (l *mockLedger) Block(rnd basics.Round) (blk bookkeeping.Block, err error)
181181
return l.blocks[0], nil
182182
}
183183

184-
func (l *mockLedger) AddressTxns(id basics.Address, r basics.Round) ([]transactions.SignedTxnWithAD, error) {
185-
blk := l.blocks[r]
186-
187-
spec := transactions.SpecialAddresses{
188-
FeeSink: blk.FeeSink,
189-
RewardsPool: blk.RewardsPool,
190-
}
191-
192-
var res []transactions.SignedTxnWithAD
193-
194-
for _, tx := range blk.Payset {
195-
if tx.Txn.MatchAddress(id, spec) {
196-
signedTxn := transactions.SignedTxnWithAD{SignedTxn: transactions.SignedTxn{Txn: tx.Txn}}
197-
res = append(res, signedTxn)
184+
func (l *mockLedger) TxnsFrom(id basics.Address, r basics.Round) ([]transactions.Transaction, error) {
185+
var res []transactions.Transaction
186+
for _, tx := range l.blocks[r].Payset {
187+
if id == tx.Txn.Sender {
188+
res = append(res, tx.Txn)
198189
}
199190
}
200191
return res, nil

daemon/algod/api/server/v2/utils.go

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,16 @@ func decode(handle codec.Handle, data []byte, v interface{}) error {
288288
return nil
289289
}
290290

291-
// Helper to convert basics.StateDelta -> *model.StateDelta
292-
func stateDeltaToStateDelta(d basics.StateDelta) *model.StateDelta {
293-
if len(d) == 0 {
291+
// globalDeltaToStateDelta converts basics.StateDelta -> model.StateDelta. It
292+
// should only be used on globals, because locals require extra context to
293+
// translate account indexes.
294+
func globalDeltaToStateDelta(bsd basics.StateDelta) model.StateDelta {
295+
if len(bsd) == 0 {
294296
return nil
295297
}
296-
var delta model.StateDelta
297-
for k, v := range d {
298-
delta = append(delta, model.EvalDeltaKeyValue{
298+
msd := make(model.StateDelta, 0, len(bsd))
299+
for k, v := range bsd {
300+
msd = append(msd, model.EvalDeltaKeyValue{
299301
Key: base64.StdEncoding.EncodeToString([]byte(k)),
300302
Value: model.EvalDelta{
301303
Action: uint64(v.Action),
@@ -304,7 +306,7 @@ func stateDeltaToStateDelta(d basics.StateDelta) *model.StateDelta {
304306
},
305307
})
306308
}
307-
return &delta
309+
return msd
308310
}
309311

310312
func edIndexToAddress(index uint64, txn *transactions.Transaction, shared []basics.Address) string {
@@ -321,23 +323,21 @@ func edIndexToAddress(index uint64, txn *transactions.Transaction, shared []basi
321323
}
322324
}
323325

324-
func convertToDeltas(txn node.TxnWithStatus) (*[]model.AccountStateDelta, *model.StateDelta) {
325-
var localStateDelta *[]model.AccountStateDelta
326-
if len(txn.ApplyData.EvalDelta.LocalDeltas) > 0 {
327-
d := make([]model.AccountStateDelta, 0)
328-
shared := txn.ApplyData.EvalDelta.SharedAccts
329-
330-
for k, v := range txn.ApplyData.EvalDelta.LocalDeltas {
331-
d = append(d, model.AccountStateDelta{
332-
Address: edIndexToAddress(k, &txn.Txn.Txn, shared),
333-
Delta: *(stateDeltaToStateDelta(v)),
334-
})
335-
}
326+
func localDeltasToLocalDeltas(ed transactions.EvalDelta, txn *transactions.Transaction) []model.AccountStateDelta {
327+
if len(ed.LocalDeltas) == 0 {
328+
return nil
329+
}
330+
lsd := make([]model.AccountStateDelta, 0, len(ed.LocalDeltas))
331+
shared := ed.SharedAccts
336332

337-
localStateDelta = &d
333+
for k, v := range ed.LocalDeltas {
334+
lsd = append(lsd, model.AccountStateDelta{
335+
Address: edIndexToAddress(k, txn, shared),
336+
Delta: globalDeltaToStateDelta(v),
337+
})
338338
}
339339

340-
return localStateDelta, stateDeltaToStateDelta(txn.ApplyData.EvalDelta.GlobalDelta)
340+
return lsd
341341
}
342342

343343
func convertLogs(txn node.TxnWithStatus) *[][]byte {
@@ -381,11 +381,12 @@ func ConvertInnerTxn(txn *transactions.SignedTxnWithAD) PreEncodedTxInfo {
381381
response.AssetIndex = omitEmpty(uint64(txn.ApplyData.ConfigAsset))
382382
response.ApplicationIndex = omitEmpty(uint64(txn.ApplyData.ApplicationID))
383383

384+
response.LocalStateDelta = sliceOrNil(localDeltasToLocalDeltas(txn.ApplyData.EvalDelta, &txn.Txn))
385+
response.GlobalStateDelta = sliceOrNil(globalDeltaToStateDelta(txn.ApplyData.EvalDelta.GlobalDelta))
384386
withStatus := node.TxnWithStatus{
385387
Txn: txn.SignedTxn,
386388
ApplyData: txn.ApplyData,
387389
}
388-
response.LocalStateDelta, response.GlobalStateDelta = convertToDeltas(withStatus)
389390
response.Logs = convertLogs(withStatus)
390391
response.Inners = convertInners(&withStatus)
391392
return response

data/bookkeeping/block.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,59 @@ type (
262262
}
263263
)
264264

265+
// TxnDeadError defines an error type which indicates a transaction is outside of the
266+
// round validity window.
267+
type TxnDeadError struct {
268+
Round basics.Round
269+
FirstValid basics.Round
270+
LastValid basics.Round
271+
Early bool
272+
}
273+
274+
func (err *TxnDeadError) Error() string {
275+
return fmt.Sprintf("txn dead: round %d outside of %d--%d", err.Round, err.FirstValid, err.LastValid)
276+
}
277+
278+
// Alive checks to see if the transaction is still alive (can be applied) at the specified Round.
279+
func (bh BlockHeader) Alive(tx transactions.Header) error {
280+
// Check round validity
281+
round := bh.Round
282+
if round < tx.FirstValid || round > tx.LastValid {
283+
return &TxnDeadError{
284+
Round: round,
285+
FirstValid: tx.FirstValid,
286+
LastValid: tx.LastValid,
287+
Early: round < tx.FirstValid,
288+
}
289+
}
290+
291+
// Check genesis ID
292+
proto := config.Consensus[bh.CurrentProtocol]
293+
genesisID := bh.GenesisID
294+
if tx.GenesisID != "" && tx.GenesisID != genesisID {
295+
return fmt.Errorf("tx.GenesisID <%s> does not match expected <%s>",
296+
tx.GenesisID, genesisID)
297+
}
298+
299+
// Check genesis hash
300+
if proto.SupportGenesisHash {
301+
genesisHash := bh.GenesisHash
302+
if tx.GenesisHash != (crypto.Digest{}) && tx.GenesisHash != genesisHash {
303+
return fmt.Errorf("tx.GenesisHash <%s> does not match expected <%s>",
304+
tx.GenesisHash, genesisHash)
305+
}
306+
if proto.RequireGenesisHash && tx.GenesisHash == (crypto.Digest{}) {
307+
return fmt.Errorf("required tx.GenesisHash is missing")
308+
}
309+
} else {
310+
if tx.GenesisHash != (crypto.Digest{}) {
311+
return fmt.Errorf("tx.GenesisHash <%s> not allowed", tx.GenesisHash)
312+
}
313+
}
314+
315+
return nil
316+
}
317+
265318
// Hash returns the hash of a block header.
266319
// The hash of a block is the hash of its header.
267320
func (bh BlockHeader) Hash() BlockHash {

data/bookkeeping/block_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,3 +1069,45 @@ func TestFirstYearsBonus(t *testing.T) {
10691069
// declined to about 72% (but foundation funding probably gone anyway)
10701070
a.InDelta(0.72, float64(bonus)/float64(plan.BaseAmount), 0.01)
10711071
}
1072+
1073+
func TestAlive(t *testing.T) {
1074+
partitiontest.PartitionTest(t)
1075+
1076+
bh := BlockHeader{
1077+
Round: 0,
1078+
GenesisHash: crypto.Digest{0x42},
1079+
}
1080+
bh.CurrentProtocol = protocol.ConsensusCurrentVersion
1081+
1082+
header := transactions.Header{
1083+
FirstValid: 5000,
1084+
LastValid: 5050,
1085+
GenesisID: bh.GenesisID,
1086+
GenesisHash: bh.GenesisHash,
1087+
}
1088+
1089+
bh.Round = header.FirstValid + 1
1090+
if err := bh.Alive(header); err != nil {
1091+
t.Errorf("transaction not alive during lifetime %v", err)
1092+
}
1093+
1094+
bh.Round = header.FirstValid
1095+
if err := bh.Alive(header); err != nil {
1096+
t.Errorf("transaction not alive at issuance %v", err)
1097+
}
1098+
1099+
bh.Round = header.LastValid
1100+
if err := bh.Alive(header); err != nil {
1101+
t.Errorf("transaction not alive at expiry %v", err)
1102+
}
1103+
1104+
bh.Round = header.FirstValid - 1
1105+
if bh.Alive(header) == nil {
1106+
t.Errorf("premature transaction alive %v", header)
1107+
}
1108+
1109+
bh.Round = header.LastValid + 1
1110+
if bh.Alive(header) == nil {
1111+
t.Errorf("expired transaction alive %v", header)
1112+
}
1113+
}

data/ledger.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,25 +118,21 @@ func LoadLedger[T string | ledger.DirsAndPrefix](
118118
return l, nil
119119
}
120120

121-
// AddressTxns returns the list of transactions to/from a given address in specific round
122-
func (l *Ledger) AddressTxns(id basics.Address, r basics.Round) ([]transactions.SignedTxnWithAD, error) {
121+
// TxnsFrom returns the list of transactions sent by a given address in a round
122+
func (l *Ledger) TxnsFrom(id basics.Address, r basics.Round) ([]transactions.Transaction, error) {
123123
blk, err := l.Block(r)
124124
if err != nil {
125125
return nil, err
126126
}
127-
spec := transactions.SpecialAddresses{
128-
FeeSink: blk.FeeSink,
129-
RewardsPool: blk.RewardsPool,
130-
}
131127

132-
var res []transactions.SignedTxnWithAD
128+
var res []transactions.Transaction
133129
payset, err := blk.DecodePaysetFlat()
134130
if err != nil {
135131
return nil, err
136132
}
137133
for _, tx := range payset {
138-
if tx.Txn.MatchAddress(id, spec) {
139-
res = append(res, tx)
134+
if id == tx.Txn.Sender {
135+
res = append(res, tx.Txn)
140136
}
141137
}
142138
return res, nil

0 commit comments

Comments
 (0)