Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add account min balance field #1596

Merged
merged 4 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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
}

Check warning on line 1008 in idb/postgres/postgres.go

View check run for this annotation

Codecov / codecov/patch

idb/postgres/postgres.go#L1005-L1008

Added lines #L1005 - L1008 were not covered by tests
}
for req.rows.Next() {
var addr []byte
var microalgos uint64
Expand Down Expand Up @@ -1131,20 +1142,14 @@
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
gmalouf marked this conversation as resolved.
Show resolved Hide resolved

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)
})
}
}
Loading