Skip to content

Commit

Permalink
Add account min balance field (algorand#1596)
Browse files Browse the repository at this point in the history
* Add account min balance field

* Update test resource boxes.json to expect min_balance field to be present/set.

* Add unit test for minBalance calculation inspired by go-algorand.

---------

Co-authored-by: Gary Malouf <[email protected]>
  • Loading branch information
jasonpaulos and gmalouf authored May 23, 2024
1 parent 5fdba20 commit b019536
Show file tree
Hide file tree
Showing 11 changed files with 657 additions and 343 deletions.
3 changes: 3 additions & 0 deletions accounting/rewind.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ func AccountAtRound(ctx context.Context, account models.Account, round uint64, d
acct.PendingRewards = 0
acct.Amount = acct.AmountWithoutPendingRewards

// MinBalance is not supported.
acct.MinBalance = 0

// TODO: Clear out the closed-at field as well. Like Rewards we cannot know this value for all accounts.
//acct.ClosedAt = 0

Expand Down
275 changes: 138 additions & 137 deletions api/generated/common/routes.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions api/generated/common/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

357 changes: 179 additions & 178 deletions api/generated/v2/routes.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions api/generated/v2/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions api/indexer.oas2.json
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,7 @@
"amount-without-pending-rewards",
"rewards",
"status",
"min-balance",
"total-apps-opted-in",
"total-assets-opted-in",
"total-box-bytes",
Expand All @@ -1044,6 +1045,10 @@
"description": "total number of MicroAlgos in the account",
"type": "integer"
},
"min-balance": {
"description": "MicroAlgo balance required by the account.\n\nThe requirement grows based on asset and application usage.",
"type": "integer"
},
"amount-without-pending-rewards": {
"description": "specifies the amount of MicroAlgos in the account, without the pending rewards.",
"type": "integer"
Expand Down
5 changes: 5 additions & 0 deletions api/indexer.oas3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,10 @@
"description": "The round in which this account last proposed the block.",
"type": "integer"
},
"min-balance": {
"description": "MicroAlgo balance required by the account.\n\nThe requirement grows based on asset and application usage.",
"type": "integer"
},
"participation": {
"$ref": "#/components/schemas/AccountParticipation"
},
Expand Down Expand Up @@ -828,6 +832,7 @@
"address",
"amount",
"amount-without-pending-rewards",
"min-balance",
"pending-rewards",
"rewards",
"round",
Expand Down
65 changes: 45 additions & 20 deletions api/test_resources/boxes.json

Large diffs are not rendered by default.

21 changes: 13 additions & 8 deletions idb/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,17 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
db.log.Warnf("long query %fs: %s", dt.Seconds(), req.query)
}
}()
var proto config.ConsensusParams
{
var ok bool
// temporarily cast req.blockheader.CurrentProtocol(string) to protocol.ConsensusVersion
proto, ok = config.Consensus[protocol.ConsensusVersion(req.blockheader.CurrentProtocol)]
if !ok {
err := fmt.Errorf("get protocol err (%s)", req.blockheader.CurrentProtocol)
req.out <- idb.AccountRow{Error: err}
return
}
}
for req.rows.Next() {
var addr []byte
var microalgos uint64
Expand Down Expand Up @@ -1131,20 +1142,14 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
account.IncentiveEligible = omitEmpty(accountData.IncentiveEligible)
account.LastHeartbeat = omitEmpty(uint64(accountData.LastHeartbeat))
account.LastProposed = omitEmpty(uint64(accountData.LastProposed))

account.MinBalance = itypes.AccountMinBalance(accountData, &proto)
}

if account.Status == "NotParticipating" {
account.PendingRewards = 0
} else {
// TODO: pending rewards calculation doesn't belong in database layer (this is just the most covenient place which has all the data)
// TODO: replace config.Consensus. config.Consensus map[protocol.ConsensusVersion]ConsensusParams
// temporarily cast req.blockheader.CurrentProtocol(string) to protocol.ConsensusVersion
proto, ok := config.Consensus[protocol.ConsensusVersion(req.blockheader.CurrentProtocol)]
if !ok {
err = fmt.Errorf("get protocol err (%s)", req.blockheader.CurrentProtocol)
req.out <- idb.AccountRow{Error: err}
break
}
rewardsUnits := uint64(0)
if proto.RewardUnit != 0 {
rewardsUnits = microalgos / proto.RewardUnit
Expand Down
84 changes: 84 additions & 0 deletions types/min_balance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package types

import (
"github.com/algorand/go-algorand-sdk/v2/protocol/config"
sdk "github.com/algorand/go-algorand-sdk/v2/types"
)

// stateSchemaMinBalance computes the MinBalance requirements for a StateSchema
// based on the consensus parameters
func stateSchemaMinBalance(sm sdk.StateSchema, proto *config.ConsensusParams) uint64 {
// Flat cost for each key/value pair
flatCost := proto.SchemaMinBalancePerEntry * (sm.NumUint + sm.NumByteSlice)

// Cost for uints
uintCost := proto.SchemaUintMinBalance * sm.NumUint

// Cost for byte slices
bytesCost := proto.SchemaBytesMinBalance * sm.NumByteSlice

// Sum the separate costs
return flatCost + uintCost + bytesCost
}

// minBalance computes the minimum balance requirements for an account based on
// some consensus parameters. MinBalance should correspond roughly to how much
// storage the account is allowed to store on disk.
func minBalance(
proto *config.ConsensusParams,
totalAssets uint64,
totalAppSchema sdk.StateSchema,
totalAppParams uint64, totalAppLocalStates uint64,
totalExtraAppPages uint64,
totalBoxes uint64, totalBoxBytes uint64,
) uint64 {
var min uint64

// First, base MinBalance
min = proto.MinBalance

// MinBalance for each Asset
assetCost := proto.MinBalance * totalAssets
min += assetCost

// Base MinBalance for each created application
appCreationCost := proto.AppFlatParamsMinBalance * totalAppParams
min += appCreationCost

// Base MinBalance for each opted in application
appOptInCost := proto.AppFlatOptInMinBalance * totalAppLocalStates
min += appOptInCost

// MinBalance for state usage measured by LocalStateSchemas and
// GlobalStateSchemas
schemaCost := stateSchemaMinBalance(totalAppSchema, proto)
min += schemaCost

// MinBalance for each extra app program page
extraAppProgramLenCost := proto.AppFlatParamsMinBalance * totalExtraAppPages
min += extraAppProgramLenCost

// Base MinBalance for each created box
boxBaseCost := proto.BoxFlatMinBalance * totalBoxes
min += boxBaseCost

// Per byte MinBalance for boxes
boxByteCost := proto.BoxByteMinBalance * totalBoxBytes
min += boxByteCost

return min
}

// AccountMinBalance computes the minimum balance requirements for an account
// based on some consensus parameters. MinBalance should correspond roughly to
// how much storage the account is allowed to store on disk.
func AccountMinBalance(account sdk.AccountData, proto *config.ConsensusParams) uint64 {
return minBalance(
proto,
account.TotalAssets,
account.TotalAppSchema,
account.TotalAppParams, account.TotalAppLocalStates,
uint64(account.TotalExtraAppPages),
account.TotalBoxes, account.TotalBoxBytes,
)
}
175 changes: 175 additions & 0 deletions types/min_balance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package types

import (
"github.com/algorand/go-algorand-sdk/v2/protocol/config"
sdk "github.com/algorand/go-algorand-sdk/v2/types"
"github.com/stretchr/testify/assert"
"testing"
)

func TestMinBalance(t *testing.T) {
testConsensusParams := &config.ConsensusParams{
MinBalance: 100000,
AppFlatParamsMinBalance: 100000,
AppFlatOptInMinBalance: 100000,
SchemaMinBalancePerEntry: 25000,
SchemaUintMinBalance: 3500,
SchemaBytesMinBalance: 25000,
BoxFlatMinBalance: 2500,
BoxByteMinBalance: 400,
}

tests := []struct {
name string
expectedResult uint64
proto *config.ConsensusParams
totalAssets uint64
totalAppSchema sdk.StateSchema
totalAppParams uint64
totalAppLocalStates uint64
totalExtraAppPages uint64
totalBoxes uint64
totalBoxBytes uint64
}{
{
"Passing all 0s/empties to minBalance",
0,
&config.ConsensusParams{},
0,
sdk.StateSchema{},
0,
0,
0,
0,
0,
},
{
"Base Case: Use non-zero consensus minBalance with otherwise 0s/empties",
100000,
testConsensusParams,
0,
sdk.StateSchema{},
0,
0,
0,
0,
0,
},
{
"Base Case with non-zero totalAssets",
testConsensusParams.MinBalance + (testConsensusParams.MinBalance * 20),
testConsensusParams,
20,
sdk.StateSchema{},
0,
0,
0,
0,
0,
},
{
"Layering in created applications",
testConsensusParams.MinBalance + (testConsensusParams.MinBalance * 20) +
(testConsensusParams.AppFlatParamsMinBalance * 30),
testConsensusParams,
20,
sdk.StateSchema{},
30,
0,
0,
0,
0,
},
{
"Layering in opted in applications",
testConsensusParams.MinBalance + (testConsensusParams.MinBalance * 20) +
(testConsensusParams.AppFlatParamsMinBalance * 30) + (testConsensusParams.AppFlatOptInMinBalance * 5),
testConsensusParams,
20,
sdk.StateSchema{},
30,
5,
0,
0,
0,
},
{
"Including State Usage Costs",
testConsensusParams.MinBalance + (testConsensusParams.MinBalance * 20) +
(testConsensusParams.AppFlatParamsMinBalance * 30) + (testConsensusParams.AppFlatOptInMinBalance * 5) +
(testConsensusParams.SchemaMinBalancePerEntry * (500 + 1000)) +
(testConsensusParams.SchemaUintMinBalance * 500) +
(testConsensusParams.SchemaBytesMinBalance * 1000),
testConsensusParams,
20,
sdk.StateSchema{
NumUint: 500,
NumByteSlice: 1000,
},
30,
5,
0,
0,
0,
},
{
"Including Extra App Pages",
testConsensusParams.MinBalance + (testConsensusParams.MinBalance * 20) +
(testConsensusParams.AppFlatParamsMinBalance * 30) + (testConsensusParams.AppFlatOptInMinBalance * 5) +
(testConsensusParams.SchemaMinBalancePerEntry * (500 + 1000)) +
(testConsensusParams.SchemaUintMinBalance * 500) +
(testConsensusParams.SchemaBytesMinBalance * 1000) +
(testConsensusParams.AppFlatParamsMinBalance * 300),
testConsensusParams,
20,
sdk.StateSchema{
NumUint: 500,
NumByteSlice: 1000,
},
30,
5,
300,
0,
0,
},
{
"Add in Total Boxes and Bytes",
testConsensusParams.MinBalance + (testConsensusParams.MinBalance * 20) +
(testConsensusParams.AppFlatParamsMinBalance * 30) + (testConsensusParams.AppFlatOptInMinBalance * 5) +
(testConsensusParams.SchemaMinBalancePerEntry * (500 + 1000)) +
(testConsensusParams.SchemaUintMinBalance * 500) +
(testConsensusParams.SchemaBytesMinBalance * 1000) +
(testConsensusParams.AppFlatParamsMinBalance * 300) +
(testConsensusParams.BoxFlatMinBalance * 8) +
(testConsensusParams.BoxByteMinBalance * 7500),
testConsensusParams,
20,
sdk.StateSchema{
NumUint: 500,
NumByteSlice: 1000,
},
30,
5,
300,
8,
7500,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := minBalance(
test.proto,
test.totalAssets,
test.totalAppSchema,
test.totalAppParams,
test.totalAppLocalStates,
test.totalExtraAppPages,
test.totalBoxes,
test.totalBoxBytes,
)

assert.Equal(t, test.expectedResult, result)
})
}
}

0 comments on commit b019536

Please sign in to comment.