diff --git a/Dockerfile b/Dockerfile index ff68ff7..e8ad2c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,10 +20,13 @@ COPY rosetta-deso/services services COPY rosetta-deso/main.go . # include core src -COPY core/desohash ../core/desohash -COPY core/cmd ../core/cmd -COPY core/lib ../core/lib -COPY core/migrate ../core/migrate +COPY core/desohash ../core/desohash +COPY core/cmd ../core/cmd +COPY core/lib ../core/lib +COPY core/migrate ../core/migrate +COPY core/bls ../core/bls +COPY core/collections ../core/collections +COPY core/consensus ../core/consensus # build rosetta-deso RUN GOOS=linux go build -mod=mod -a -installsuffix cgo -o bin/rosetta-deso main.go diff --git a/deso/block.go b/deso/block.go index 132cfbf..4da5ba0 100644 --- a/deso/block.go +++ b/deso/block.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "sort" "strconv" + "strings" "github.com/coinbase/rosetta-sdk-go/types" "github.com/deso-protocol/core/lib" @@ -61,7 +62,7 @@ func (node *Node) GetBlock(hash string) *types.Block { return &types.Block{ BlockIdentifier: blockIdentifier, ParentBlockIdentifier: parentBlockIdentifier, - Timestamp: int64(blockNode.Header.TstampSecs) * 1000, + Timestamp: int64(blockNode.Header.TstampNanoSecs) / 1e6, // Convert nanoseconds to milliseconds Transactions: transactions, } } @@ -76,7 +77,7 @@ func (node *Node) GetBlock(hash string) *types.Block { return &types.Block{ BlockIdentifier: blockIdentifier, ParentBlockIdentifier: parentBlockIdentifier, - Timestamp: int64(blockNode.Header.TstampSecs) * 1000, + Timestamp: int64(blockNode.Header.TstampNanoSecs) / 1e6, // Convert nanoseconds to milliseconds Transactions: node.GetTransactionsForConvertBlock(block), } } @@ -251,6 +252,18 @@ func (node *Node) GetTransactionsForConvertBlock(block *lib.MsgDeSoBlock) []*typ // Add inputs for DAO Coin Limit Orders daoCoinLimitOrderOps := node.getDAOCoinLimitOrderOps(txn, utxoOpsForTxn, len(ops)) ops = append(ops, daoCoinLimitOrderOps...) + + // Add operations for stake transactions + stakeOps := node.getStakeOps(txn, utxoOpsForTxn, len(ops)) + ops = append(ops, stakeOps...) + + // Add operations for unstake transactions + unstakeOps := node.getUnstakeOps(txn, utxoOpsForTxn, len(ops)) + ops = append(ops, unstakeOps...) + + // Add operations for unlock stake transactions + unlockStakeOps := node.getUnlockStakeOps(txn, utxoOpsForTxn, len(ops)) + ops = append(ops, unlockStakeOps...) } transaction.Operations = squashOperations(ops) @@ -258,6 +271,43 @@ func (node *Node) GetTransactionsForConvertBlock(block *lib.MsgDeSoBlock) []*typ transactions = append(transactions, transaction) } + // Create a dummy transaction for the "block level" operations + // when we have the additional slice of utxo operations. This is used + // to capture staking rewards that are paid out at the block level, but + // have no transaction associated with them. The array of utxo operations + // at the index of # transactions in block + 1 is ALWAYS the block level + // utxo operations. At the end of a PoS epoch, we distribute staking rewards + // by either adding to the stake entry with the specified validator or + // directly deposit staking rewards to the staker's DESO balance. This is based + // on the configuration a user specifies when staking with a validator. + // We capture the non-restaked rewards the same way we capture basic transfer outputs, + // but with a dummy transaction. These are simple OUTPUTS to a staker's DESO balance. + // We capture the restaked rewards by adding an OUTPUT + // to the valiator's subaccount. + if len(utxoOpsForBlock) == len(block.Txns)+1 { + blockHash, err := block.Hash() + if err != nil { + // This is bad if this happens. + glog.Error(errors.Wrapf(err, "GetTransactionsForConvertBlock: Problem fetching block hash")) + return transactions + } + utxoOpsForBlockLevel := utxoOpsForBlock[len(utxoOpsForBlock)-1] + var ops []*types.Operation + // Add outputs for Stake Reward Distributions that are not re-staked. We use a fake transaction to + // avoid panic in getStakingRewardDistributionOps. + implicitOutputOps := node.getImplicitOutputs(&lib.MsgDeSoTxn{TxOutputs: nil}, utxoOpsForBlockLevel, len(ops)) + ops = append(ops, implicitOutputOps...) + // Add outputs for Stake Reward Distributions that are re-staked + stakeRewardOps := node.getStakingRewardDistributionOps(utxoOpsForBlockLevel, len(ops)) + ops = append(ops, stakeRewardOps...) + + transaction := &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{Hash: fmt.Sprintf("blockHash-%v", blockHash.String())}, + } + transaction.Operations = squashOperations(ops) + transactions = append(transactions, transaction) + } + return transactions } @@ -273,7 +323,8 @@ func (node *Node) getBlockTransactionsWithHypersync(blockHeight uint64, blockHas return []*types.Transaction{} } - balances, lockedBalances := node.Index.GetHypersyncBlockBalances(blockHeight) + // TODO: do we need staked and locked stake balances here? + balances, lockedBalances, stakedDESOBalances, lockedStakeDESOBalances := node.Index.GetHypersyncBlockBalances(blockHeight) // We create a fake genesis block that will contain a portion of the balances downloaded during hypersync. // In addition, we need to lowkey reinvent these transactions, including, in particular, their transaction @@ -353,6 +404,82 @@ func (node *Node) getBlockTransactionsWithHypersync(blockHeight uint64, blockHas }) } + // We create a fake genesis block of all the staked DESO at the latest snapshot height. + // We represent all DESO staked to a single validator as a single subaccount. + // This mirrors how we treat creator coins, a pool of DESO as a subaccount of the creator. + // This acts as a pool of all the DESO staked in StakeEntry objects with a given Validator. + // We chose to use validators instead of stakers here as there are fewer validators and + // thus fewer subaccounts to keep track of. Note that we are using PKIDs for the validator + // instead of a public key, so we do not need to track balance changes when a validator + // has a swap identity performed on it. + // Account identifier: Validator PKID + // Subaccount identifier: VALIDATOR_ENTRY + for pkid, balance := range stakedDESOBalances { + if balance == 0 { + continue + } + nextHash := lib.Sha256DoubleHash([]byte(currentHash)) + currentHash = string(nextHash[:]) + transactions = append(transactions, &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: nextHash.String(), + }, + Operations: squashOperations([]*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Account: node.getValidatorEntrySubAccountIdentifierForValidator(lib.NewPKID(pkid[:])), + Amount: &types.Amount{ + Value: strconv.FormatUint(balance, 10), + Currency: &Currency, + }, + Status: &SuccessStatus, + Type: OutputOpType, + }, + }), + }) + } + + // We create a fake genesis block of all the locked stake DESO at the latest snapshot height. + // Locked stake DESO is represented as a subaccount of the staker. A locked stake entry is + // unique by the combination of Staker PKID + Validator PKID + LockedAtEpochNumber, so we + // use Validator PKID + LockedAtEpochNumber as the subaccount identifier. Note that we are + // using PKIDs for the validator and staker instead of public keys, so we do not need to + // track balance changes if a swap identity were performed on either. + // Account Identifier: Staker PKID + // Subaccount Identifier: LOCKED_STAKE_ENTRY || Validator PKID || LockedAtEpochNumber + for lockedStakeBalanceMapKey, balance := range lockedStakeDESOBalances { + if balance == 0 { + continue + } + nextHash := lib.Sha256DoubleHash([]byte(currentHash)) + currentHash = string(nextHash[:]) + transactions = append(transactions, &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: nextHash.String(), + }, + Operations: squashOperations([]*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Account: node.getLockedStakeEntryIdentifierForValidator( + &lockedStakeBalanceMapKey.StakerPKID, + &lockedStakeBalanceMapKey.ValidatorPKID, + lockedStakeBalanceMapKey.LockedAtEpochNumber, + ), + Amount: &types.Amount{ + Value: strconv.FormatUint(balance, 10), + Currency: &Currency, + }, + Status: &SuccessStatus, + Type: OutputOpType, + }, + }), + }) + } + sort.Slice(transactions, func(ii, jj int) bool { return bytes.Compare([]byte(transactions[ii].TransactionIdentifier.Hash), []byte(transactions[jj].TransactionIdentifier.Hash)) > 0 @@ -563,6 +690,10 @@ func (node *Node) getSwapIdentityOps(txn *lib.MsgDeSoTxn, utxoOpsForTxn []*lib.U }, }) + // TODO: Do we need to do this for ValidatorEntry and LockedStakeEntries as well? If so, + // we'll need to expose add ToValidatorEntry, FromValidatorEntry, ToLockedStakeEntries, and + // FromLockedStakeEntries to the UtxoOperation. + return operations } @@ -858,6 +989,217 @@ func (node *Node) getBalanceModelSpends(txn *lib.MsgDeSoTxn, utxoOpsForTxn []*li return operations } +// getLockedStakeEntrySubAccountIdentifierForValidator returns a SubAccountIdentifier for a locked stake entry. +func (node *Node) getLockedStakeEntryIdentifierForValidator(stakerPKID *lib.PKID, validatorPKID *lib.PKID, lockedAtEpochNumber uint64) *types.AccountIdentifier { + return &types.AccountIdentifier{ + Address: lib.Base58CheckEncode(stakerPKID.ToBytes(), false, node.Params), + SubAccount: &types.SubAccountIdentifier{ + Address: fmt.Sprintf("%v-%v-%v", LockedStakeEntry, lib.Base58CheckEncode(validatorPKID.ToBytes(), false, node.Params), lockedAtEpochNumber), + }, + } +} + +func (node *Node) getValidatorEntrySubAccountIdentifierForValidator(validatorPKID *lib.PKID) *types.AccountIdentifier { + return &types.AccountIdentifier{ + Address: lib.Base58CheckEncode(validatorPKID.ToBytes(), false, node.Params), + SubAccount: &types.SubAccountIdentifier{ + Address: ValidatorEntry, + }, + } +} + +func (node *Node) GetValidatorPKIDFromSubAccountIdentifier(subAccount *types.SubAccountIdentifier) (*lib.PKID, error) { + if subAccount == nil || !strings.HasPrefix(subAccount.Address, LockedStakeEntry) { + return nil, fmt.Errorf("invalid subaccount for validator PKID extraction") + } + segments := strings.Split(subAccount.Address, "-") + if len(segments) != 2 { + return nil, fmt.Errorf("invalid subaccount for validator PKID extraction") + } + validatorPKIDBytes, _, err := lib.Base58CheckDecode(segments[1]) + if err != nil { + return nil, fmt.Errorf("invalid subaccount for validator PKID extraction") + } + return lib.NewPKID(validatorPKIDBytes), nil +} + +func (node *Node) getStakeOps(txn *lib.MsgDeSoTxn, utxoOps []*lib.UtxoOperation, numOps int) []*types.Operation { + if txn.TxnMeta.GetTxnType() != lib.TxnTypeStake { + return nil + } + + var operations []*types.Operation + for _, utxoOp := range utxoOps { + // We only need an OUTPUT operation here as the INPUT operation + // is covered by the getBalanceModelSpends function. + if utxoOp.Type == lib.OperationTypeStake { + prevValidatorEntry := utxoOp.PrevValidatorEntry + if prevValidatorEntry == nil { + // TODO: This is a bad error... + glog.Error("getStakeOps: prevValidatorEntry was nil") + continue + } + realTxMeta := txn.TxnMeta.(*lib.StakeMetadata) + if realTxMeta == nil { + glog.Error("getStakeOps: realTxMeta was nil") + continue + } + stakeAmountNanos := realTxMeta.StakeAmountNanos + if !stakeAmountNanos.IsUint64() { + glog.Error("getStakeOps: stakeAmountNanos was not a uint64") + continue + } + stakeAmountNanosUint64 := stakeAmountNanos.Uint64() + + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(numOps), + }, + Account: node.getValidatorEntrySubAccountIdentifierForValidator(prevValidatorEntry.ValidatorPKID), + Amount: &types.Amount{ + Value: strconv.FormatUint(stakeAmountNanosUint64, 10), + Currency: &Currency, + }, + Status: &SuccessStatus, + Type: OutputOpType, + }) + numOps++ + } + } + return operations +} + +func (node *Node) getUnstakeOps(txn *lib.MsgDeSoTxn, utxoOps []*lib.UtxoOperation, numOps int) []*types.Operation { + if txn.TxnMeta.GetTxnType() != lib.TxnTypeUnstake { + return nil + } + + var operations []*types.Operation + for _, utxoOp := range utxoOps { + if utxoOp.Type == lib.OperationTypeUnstake { + prevValidatorEntry := utxoOp.PrevValidatorEntry + if prevValidatorEntry == nil { + glog.Error("getUnstakeOps: prevValidatorEntry was nil") + continue + } + prevStakeEntries := utxoOp.PrevStakeEntries + if len(prevStakeEntries) != 1 { + glog.Error("getUnstakeOps: prevStakeEntries was not of length 1") + continue + } + prevStakeEntry := prevStakeEntries[0] + realTxMeta := txn.TxnMeta.(*lib.UnstakeMetadata) + if realTxMeta == nil { + glog.Error("getUnstakeOps: realTxMeta was nil") + continue + } + unstakeAmountNanos := realTxMeta.UnstakeAmountNanos + if !unstakeAmountNanos.IsUint64() { + glog.Error("getUnstakeOps: unstakeAmountNanos was not a uint64") + continue + } + unstakeAmountNanosUint64 := unstakeAmountNanos.Uint64() + // First use an "input" from the ValidatorEntry + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(numOps), + }, + Account: node.getValidatorEntrySubAccountIdentifierForValidator(prevValidatorEntry.ValidatorPKID), + Amount: &types.Amount{ + Value: strconv.FormatUint(unstakeAmountNanosUint64, 10), + Currency: &Currency, + }, + Status: &SuccessStatus, + Type: InputOpType, + }) + numOps++ + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(numOps), + }, + Account: node.getLockedStakeEntryIdentifierForValidator( + prevStakeEntry.StakerPKID, + prevValidatorEntry.ValidatorPKID, + utxoOp.LockedAtEpochNumber, + ), + Amount: &types.Amount{ + Value: strconv.FormatUint(unstakeAmountNanosUint64, 10), + Currency: &Currency, + }, + Status: &SuccessStatus, + Type: OutputOpType, + }) + numOps++ + } + } + return operations +} + +func (node *Node) getUnlockStakeOps(txn *lib.MsgDeSoTxn, utxoOps []*lib.UtxoOperation, numOps int) []*types.Operation { + if txn.TxnMeta.GetTxnType() != lib.TxnTypeUnlockStake { + return nil + } + + var operations []*types.Operation + for _, utxoOp := range utxoOps { + if utxoOp.Type == lib.OperationTypeUnlockStake { + for _, prevLockedStakeEntry := range utxoOp.PrevLockedStakeEntries { + if !prevLockedStakeEntry.LockedAmountNanos.IsUint64() { + glog.Error("getUnlockStakeOps: lockedAmountNanos was not a uint64") + continue + } + lockedAmountNanosUint64 := prevLockedStakeEntry.LockedAmountNanos.Uint64() + // Each locked stake entry is an "input" to the UnlockStake txn + // We spend ALL the lockedAmountNanos from each locked stake entry in + // an unlocked transaction + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(numOps), + }, + Account: node.getLockedStakeEntryIdentifierForValidator( + prevLockedStakeEntry.StakerPKID, + prevLockedStakeEntry.ValidatorPKID, + prevLockedStakeEntry.LockedAtEpochNumber, + ), + Amount: &types.Amount{ + Value: strconv.FormatUint(lockedAmountNanosUint64, 10), + Currency: &Currency, + }, + Status: &SuccessStatus, + Type: InputOpType, + }) + numOps++ + } + } + } + return operations +} + +func (node *Node) getStakingRewardDistributionOps(utxoOps []*lib.UtxoOperation, numOps int) []*types.Operation { + var operations []*types.Operation + + for _, utxoOp := range utxoOps { + if utxoOp.Type == lib.OperationTypeStakeDistributionRestake { + prevValidatorEntry := utxoOp.PrevValidatorEntry + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(numOps), + }, + Account: node.getValidatorEntrySubAccountIdentifierForValidator(prevValidatorEntry.ValidatorPKID), + Amount: &types.Amount{ + Value: strconv.FormatUint(utxoOp.StakeAmountNanosDiff, 10), + Currency: &Currency, + }, + + Status: &SuccessStatus, + Type: OutputOpType, + }) + numOps++ + } + } + return operations +} + func (node *Node) getImplicitOutputs(txn *lib.MsgDeSoTxn, utxoOpsForTxn []*lib.UtxoOperation, numOps int) []*types.Operation { var operations []*types.Operation numOutputs := uint32(len(txn.TxOutputs)) @@ -887,7 +1229,8 @@ func (node *Node) getImplicitOutputs(txn *lib.MsgDeSoTxn, utxoOpsForTxn []*lib.U numOps++ } - if utxoOp.Type == lib.OperationTypeAddBalance { + if utxoOp.Type == lib.OperationTypeAddBalance || + utxoOp.Type == lib.OperationTypeStakeDistributionPayToBalance { operations = append(operations, &types.Operation{ OperationIdentifier: &types.OperationIdentifier{ Index: int64(numOps), diff --git a/deso/block_test.go b/deso/block_test.go index 9b1382b..1b55584 100644 --- a/deso/block_test.go +++ b/deso/block_test.go @@ -37,7 +37,7 @@ func TestUtxoOpsProblem(t *testing.T) { rosettaIndexOpts.ValueDir = rosettaIndexDir rosettaIndex, err := badger.Open(rosettaIndexOpts) require.NoError(err) - node.Index = NewIndex(rosettaIndex) + node.Index = NewIndex(rosettaIndex, node.chainDB) // Listen to transaction and block events so we can fill RosettaIndex with relevant data node.EventManager = lib.NewEventManager() diff --git a/deso/events.go b/deso/events.go index 45b5a3d..0847421 100644 --- a/deso/events.go +++ b/deso/events.go @@ -1,14 +1,22 @@ package deso import ( + "bytes" "fmt" "github.com/btcsuite/btcd/btcec" "github.com/deso-protocol/core/lib" "github.com/dgraph-io/badger/v3" "github.com/golang/glog" "github.com/pkg/errors" + "math" ) +type LockedStakeBalanceMapKey struct { + StakerPKID lib.PKID + ValidatorPKID lib.PKID + LockedAtEpochNumber uint64 +} + func (node *Node) handleSnapshotCompleted() { node.Index.dbMutex.Lock() defer node.Index.dbMutex.Unlock() @@ -74,7 +82,7 @@ func (node *Node) handleSnapshotCompleted() { balancesMap[*pubKey] = balance if err := node.Index.PutSingleBalanceSnapshotWithTxn( - indexTxn, currentBlockHeight, false, *pubKey, balance); err != nil { + indexTxn, currentBlockHeight, DESOBalance, *pubKey, balance); err != nil { return errors.Wrapf(err, "Problem updating balance snapshot in index, "+ "on key (%v), value (%v), and height (%v)", keyCopy, valCopy, snapshot.CurrentEpochSnapshotMetadata.FirstSnapshotBlockHeight) @@ -82,14 +90,14 @@ func (node *Node) handleSnapshotCompleted() { currentCounter += 1 if currentCounter >= balancesPerBlock && currentBlockHeight < snapshotBlockHeight { - node.Index.PutHypersyncBlockBalances(currentBlockHeight, false, balancesMap) + node.Index.PutHypersyncBlockBalances(currentBlockHeight, DESOBalance, balancesMap) balancesMap = make(map[lib.PublicKey]uint64) currentBlockHeight++ currentCounter = 0 } } if currentCounter > 0 { - node.Index.PutHypersyncBlockBalances(currentBlockHeight, false, balancesMap) + node.Index.PutHypersyncBlockBalances(currentBlockHeight, DESOBalance, balancesMap) } return nil }) @@ -170,7 +178,7 @@ func (node *Node) handleSnapshotCompleted() { // We have to also put the balances in the other index. Not doing this would cause // balances to return zero when we're PAST the first snapshot block height. if err := node.Index.PutSingleBalanceSnapshotWithTxn( - indexTxn, currentBlockHeight, true, pubKey, lockedDeSoNanos); err != nil { + indexTxn, currentBlockHeight, CreatorCoinLockedBalance, pubKey, lockedDeSoNanos); err != nil { return errors.Wrapf(err, "PutSingleBalanceSnapshotWithTxn: problem with "+ "pubkey (%v), lockedDeSoNanos (%v) and firstSnapshotHeight (%v)", @@ -179,14 +187,14 @@ func (node *Node) handleSnapshotCompleted() { currentCounter += 1 if currentCounter >= balancesPerBlock && currentBlockHeight < snapshotBlockHeight { - node.Index.PutHypersyncBlockBalances(currentBlockHeight, true, balancesMap) + node.Index.PutHypersyncBlockBalances(currentBlockHeight, CreatorCoinLockedBalance, balancesMap) balancesMap = make(map[lib.PublicKey]uint64) currentBlockHeight++ currentCounter = 0 } } if currentCounter > 0 { - node.Index.PutHypersyncBlockBalances(currentBlockHeight, true, balancesMap) + node.Index.PutHypersyncBlockBalances(currentBlockHeight, CreatorCoinLockedBalance, balancesMap) } return nil }) @@ -197,7 +205,200 @@ func (node *Node) handleSnapshotCompleted() { } } - return + // Create a new scope to avoid name collision errors + { + // Iterate over all the validator entries in the db, look up the corresponding public + // keys, and then set all the validator entry balances in the db. + // + // No need to pass the snapshot because we know we don't have any ancestral + // records yet. + + err := node.Index.db.Update(func(indexTxn *badger.Txn) error { + // TODO: check all these comments. + return node.GetBlockchain().DB().View(func(chainTxn *badger.Txn) error { + dbPrefixx := append([]byte{}, lib.Prefixes.PrefixValidatorByStatusAndStakeAmount...) + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false + // Go in reverse order since a larger count is better. + opts.Reverse = true + + it := chainTxn.NewIterator(opts) + defer it.Close() + + totalCount := uint64(0) + // TODO: I really hate how we iterate over the index twice + // for each balance type. We can do better. + for it.Seek(dbPrefixx); it.ValidForPrefix(dbPrefixx); it.Next() { + totalCount++ + } + currentBlockHeight := uint64(1) + balancesPerBlock := totalCount / snapshotBlockHeight + validatorEntryBalances := make(map[lib.PublicKey]uint64) + if totalCount < snapshotBlockHeight { + balancesPerBlock = 1 + } + currentCounter := uint64(0) + + // The key for the validator by status and stake amount looks like + // this: Prefix, , , -> nil + // So we need to chop off the status to pull out the total stake amount nanos and the validator PKID + maxUint256 := lib.FixedWidthEncodeUint256(lib.MaxUint256) + prefix := append(dbPrefixx, lib.EncodeUint8(math.MaxUint8)...) + prefix = append(prefix, maxUint256...) + for it.Seek(prefix); it.ValidForPrefix(dbPrefixx); it.Next() { + rawKey := it.Item().Key() + + // Strip the prefix and status off the key and check its length. It + // should contain a uint256 followed by a validator PKID. + totalStakeAmountAndValidatorPKIDKey := rawKey[2:] + expectedLength := len(maxUint256) + btcec.PubKeyBytesLenCompressed + if len(totalStakeAmountAndValidatorPKIDKey) != expectedLength { + return fmt.Errorf("invalid key length %d should be at least %d", + len(totalStakeAmountAndValidatorPKIDKey), expectedLength) + } + rr := bytes.NewReader(totalStakeAmountAndValidatorPKIDKey) + totalStakeAmountNanos, err := lib.FixedWidthDecodeUint256(rr) + if err != nil { + return fmt.Errorf("problem decoding stake amount: %v", err) + } + if !totalStakeAmountNanos.IsUint64() { + return fmt.Errorf("FixedWidthDecodeUint256: Stake amount is not a uint64") + } + validatorPKIDBytes := make([]byte, btcec.PubKeyBytesLenCompressed) + _, err = rr.Read(validatorPKIDBytes[:]) + if err != nil { + return fmt.Errorf("problem reading validator PKID: %v", err) + } + // For simplicity, we just interpret the PKID as a public key + // so we can reuse existing functions. + validatorPubKey := *lib.NewPublicKey(validatorPKIDBytes) + validatorEntryBalances[validatorPubKey] = totalStakeAmountNanos.Uint64() + + // We have to also put the balances in the other index. Not doing this would cause + // balances to return zero when we're PAST the first snapshot block height. + if err = node.Index.PutSingleBalanceSnapshotWithTxn( + indexTxn, currentBlockHeight, ValidatorStakedDESOBalance, validatorPubKey, + totalStakeAmountNanos.Uint64()); err != nil { + return errors.Wrapf(err, "PutSingleBalanceSnapshotWithTxn: problem with "+ + "validatorPubKey (%v), totalStakeAmountNanos (%v) and firstSnapshotHeight (%v)", + validatorPubKey, totalStakeAmountNanos, snapshot.CurrentEpochSnapshotMetadata.FirstSnapshotBlockHeight) + } + + currentCounter++ + if currentCounter >= balancesPerBlock && currentBlockHeight < snapshotBlockHeight { + node.Index.PutHypersyncBlockBalances(currentBlockHeight, ValidatorStakedDESOBalance, validatorEntryBalances) + validatorEntryBalances = make(map[lib.PublicKey]uint64) + currentBlockHeight++ + currentCounter = 0 + } + } + if currentCounter > 0 { + node.Index.PutHypersyncBlockBalances(currentBlockHeight, ValidatorStakedDESOBalance, validatorEntryBalances) + } + return nil + }) + }) + if err != nil { + glog.Errorf(lib.CLog(lib.Red, fmt.Sprintf("handleSnapshotCompleted: Problem iterating staked "+ + "balances DeSo nanos: error: (%v)", err))) + } + } + + // Create a new scope to avoid name collision errors + { + // Iterate over all the locked stake entries in the db, look up the corresponding public + // keys, and then set all the locked stake entry balances in the db. + // + // No need to pass the snapshot because we know we don't have any ancestral + // records yet. + + err := node.Index.db.Update(func(indexTxn *badger.Txn) error { + // TODO: check all these comments. + return node.GetBlockchain().DB().View(func(chainTxn *badger.Txn) error { + dbPrefixx := append([]byte{}, lib.Prefixes.PrefixLockedStakeByValidatorAndStakerAndLockedAt...) + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = true + // Go in reverse order since a larger count is better. + opts.Reverse = true + + it := chainTxn.NewIterator(opts) + defer it.Close() + + totalCount := uint64(0) + // TODO: I really hate how we iterate over the index twice + // for each balance type. We can do better. + for it.Seek(dbPrefixx); it.ValidForPrefix(dbPrefixx); it.Next() { + totalCount++ + } + currentBlockHeight := uint64(1) + balancesPerBlock := totalCount / snapshotBlockHeight + lockedStakerValidatorBalances := make(map[LockedStakeBalanceMapKey]uint64) + if totalCount < snapshotBlockHeight { + balancesPerBlock = 1 + } + currentCounter := uint64(0) + for it.Seek(dbPrefixx); it.ValidForPrefix(dbPrefixx); it.Next() { + rawValueCopy, err := it.Item().ValueCopy(nil) + if err != nil { + return errors.Wrapf(err, "Problem iterating over chain database, "+ + "on key (%v) and value (%v)", it.Item().Key(), rawValueCopy) + } + rr := bytes.NewReader(rawValueCopy) + lockedStakeEntry, err := lib.DecodeDeSoEncoder(&lib.LockedStakeEntry{}, rr) + if err != nil { + return errors.Wrapf(err, "Problem decoding locked stake entry: %v", err) + } + if !lockedStakeEntry.LockedAmountNanos.IsUint64() { + return fmt.Errorf("LockedAmountNanos is not a uint64") + } + if !lockedStakeEntry.LockedAmountNanos.IsUint64() { + return fmt.Errorf("LockedAtEpochNumber is not a uint64") + } + lockedStakeAmountNanos := lockedStakeEntry.LockedAmountNanos.Uint64() + + lockedStakerMapKey := LockedStakeBalanceMapKey{ + StakerPKID: *lockedStakeEntry.StakerPKID, + ValidatorPKID: *lockedStakeEntry.ValidatorPKID, + LockedAtEpochNumber: lockedStakeEntry.LockedAtEpochNumber, + } + lockedStakerValidatorBalances[lockedStakerMapKey] = lockedStakeAmountNanos + + // We have to also put the balances in the other index. Not doing this would cause + // balances to return zero when we're PAST the first snapshot block height. We are + // writing to the same key multiple times, but this is okay since we just need the sum + // which gets updated each time we write to the map. + if err = node.Index.PutSingleLockedStakeBalanceSnapshotWithTxn( + indexTxn, currentBlockHeight, LockedStakeDESOBalance, lockedStakeEntry.StakerPKID, + lockedStakeEntry.ValidatorPKID, lockedStakeEntry.LockedAtEpochNumber, + lockedStakeEntry.LockedAmountNanos.Uint64()); err != nil { + return errors.Wrapf(err, "PutSingleBalanceSnapshotWithTxn: problem with "+ + "stakerPKID (%v), validatorPKID (%v) stakeAmountNanos (%v) and firstSnapshotHeight "+ + "(%v)", lockedStakeEntry.StakerPKID, lockedStakeEntry.ValidatorPKID, + lockedStakeEntry.LockedAmountNanos.Uint64(), + snapshot.CurrentEpochSnapshotMetadata.FirstSnapshotBlockHeight) + } + + currentCounter++ + if currentCounter >= balancesPerBlock && currentBlockHeight < snapshotBlockHeight { + node.Index.PutHypersyncBlockLockedStakeBalances(currentBlockHeight, + lockedStakerValidatorBalances, LockedStakeDESOBalance) + lockedStakerValidatorBalances = make(map[LockedStakeBalanceMapKey]uint64) + currentBlockHeight++ + currentCounter = 0 + } + } + if currentCounter > 0 { + node.Index.PutHypersyncBlockLockedStakeBalances(currentBlockHeight, + lockedStakerValidatorBalances, LockedStakeDESOBalance) + } + return nil + }) + }) + if err != nil { + glog.Errorf(lib.CLog(lib.Red, fmt.Sprintf("handleSnapshotCompleted: Problem iterating locked stake "+ + "balances DeSo nanos: error: (%v)", err))) + } + } } } @@ -223,17 +424,9 @@ func (node *Node) handleBlockConnected(event *lib.BlockEvent) { // don't have a snapshot. We output extra metadata for this block to ensure // Rosetta connects it appropriately. - // Save the UTXOOps. These are used to compute all of the meta information - // that Rosetta needs. - err := node.Index.PutUtxoOps(event.Block, event.UtxoOps) - if err != nil { - glog.Errorf("PutSpentUtxos: %v", err) - } - // Save a balance snapshot balances := event.UtxoView.PublicKeyToDeSoBalanceNanos - err = node.Index.PutBalanceSnapshot(event.Block.Header.Height, false, balances) - if err != nil { + if err := node.Index.PutBalanceSnapshot(event.Block.Header.Height, DESOBalance, balances); err != nil { glog.Errorf("PutBalanceSnapshot: %v", err) } @@ -260,10 +453,55 @@ func (node *Node) handleBlockConnected(event *lib.BlockEvent) { lockedBalances[*lib.NewPublicKey(profile.PublicKey)] = balanceToPut } - err = node.Index.PutBalanceSnapshot(event.Block.Header.Height, true, lockedBalances) - if err != nil { + if err := node.Index.PutBalanceSnapshot(event.Block.Header.Height, CreatorCoinLockedBalance, + lockedBalances); err != nil { glog.Errorf("PutLockedBalanceSnapshot: %v", err) } + + // Iterate over all validator entries that may have been modified. + validatorEntries := event.UtxoView.ValidatorPKIDToValidatorEntry + validatorEntryBalances := make(map[lib.PublicKey]uint64, len(validatorEntries)) + for _, validator := range validatorEntries { + balanceToPut := uint64(0) + if !validator.IsDeleted() { + if !validator.TotalStakeAmountNanos.IsUint64() { + glog.Errorf("handleBlockConnected: TotalStakeAmountNanos is not a uint64") + continue + } + balanceToPut = validator.TotalStakeAmountNanos.Uint64() + } + validatorEntryBalances[*lib.NewPublicKey(validator.ValidatorPKID.ToBytes())] = balanceToPut + } + if err := node.Index.PutBalanceSnapshot(event.Block.Header.Height, ValidatorStakedDESOBalance, validatorEntryBalances + ); err != nil { + glog.Errorf("PutStakedBalanceSnapshot: %v", err) + } + + // Iterate over all locked stake entries that may have been modified. + lockedStakeEntries := event.UtxoView.LockedStakeMapKeyToLockedStakeEntry + lockedStakeEntryBalances := make(map[LockedStakeBalanceMapKey]uint64, len(lockedStakeEntries)) + for _, lockedStakeEntry := range lockedStakeEntries { + balanceToPut := uint64(0) + if !lockedStakeEntry.IsDeleted() { + if !lockedStakeEntry.LockedAmountNanos.IsUint64() { + glog.Errorf("handleBlockConnected: LockedAmountNanos is not a uint64") + continue + } + balanceToPut = lockedStakeEntry.LockedAmountNanos.Uint64() + } + lockedStakeEntryBalances[LockedStakeBalanceMapKey{ + StakerPKID: *lockedStakeEntry.StakerPKID, + ValidatorPKID: *lockedStakeEntry.ValidatorPKID, + LockedAtEpochNumber: lockedStakeEntry.LockedAtEpochNumber, + }] = balanceToPut + } + + if err := node.Index.PutLockedStakeBalanceSnapshot(event.Block.Header.Height, LockedStakeDESOBalance, + lockedStakeEntryBalances); err != nil { + glog.Errorf("PutLockedStakeBalanceSnapshot: %v", err) + } + + // TODO: Add support for locked DESO balance entries via coin lockups here. } func (node *Node) handleTransactionConnected(event *lib.TransactionEvent) { diff --git a/deso/index.go b/deso/index.go index 65ddf71..9a7d337 100644 --- a/deso/index.go +++ b/deso/index.go @@ -24,18 +24,29 @@ const ( // Fake genesis balances we use to initialize Rosetta when running hypersync. // See comments in block.go and convertBlock() for more details. - // -> map[lib.PublicKey]uint64 + // -> map[lib.PublicKey]uint64 PrefixHypersyncBlockHeightToBalances = byte(3) + + // DESO staked to validators + // -> <> + PrefixValidatorStakedBalanceSnapshots = byte(4) + + // Locked Stake entries + // -> <> + PrefixLockedStakeBalanceSnapshots = byte(5) ) type RosettaIndex struct { - db *badger.DB - dbMutex sync.Mutex + db *badger.DB + dbMutex sync.Mutex + chainDB *badger.DB + snapshot *lib.Snapshot } -func NewIndex(db *badger.DB) *RosettaIndex { +func NewIndex(db *badger.DB, chainDB *badger.DB) *RosettaIndex { return &RosettaIndex{ - db: db, + db: db, + chainDB: chainDB, } } @@ -43,108 +54,132 @@ func NewIndex(db *badger.DB) *RosettaIndex { // Utxo Operations // -func (index *RosettaIndex) utxoOpsKey(blockHash *lib.BlockHash) []byte { - prefix := append([]byte{}, PrefixUtxoOps) - prefix = append(prefix, blockHash.ToBytes()...) - return prefix -} - -func (index *RosettaIndex) PutUtxoOps(block *lib.MsgDeSoBlock, utxoOps [][]*lib.UtxoOperation) error { - blockHash, err := block.Hash() - if err != nil { - return err - } - - opBundle := &lib.UtxoOperationBundle{ - UtxoOpBundle: utxoOps, - } - bytes := lib.EncodeToBytes(block.Header.Height, opBundle) - - return index.db.Update(func(txn *badger.Txn) error { - return txn.Set(index.utxoOpsKey(blockHash), bytes) - }) -} - func (index *RosettaIndex) GetUtxoOps(block *lib.MsgDeSoBlock) ([][]*lib.UtxoOperation, error) { blockHash, err := block.Hash() if err != nil { return nil, err } - - opBundle := &lib.UtxoOperationBundle{} - - err = index.db.View(func(txn *badger.Txn) error { - utxoOpsItem, err := txn.Get(index.utxoOpsKey(blockHash)) - if err != nil { - return err - } - - return utxoOpsItem.Value(func(valBytes []byte) error { - rr := bytes.NewReader(valBytes) - if exist, err := lib.DecodeFromBytes(opBundle, rr); !exist || err != nil { - return errors.Wrapf(err, "Problem decoding utxoops, exist: (%v)", exist) - } - return nil - }) - }) - if err != nil { - return nil, err - } - - return opBundle.UtxoOpBundle, nil + return lib.GetUtxoOperationsForBlock(index.chainDB, index.snapshot, blockHash) } // // Balance Snapshots // -func balanceSnapshotKey(isLockedBalance bool, publicKey *lib.PublicKey, blockHeight uint64, balance uint64) []byte { - startPrefix := PrefixBalanceSnapshots - if isLockedBalance { - startPrefix = PrefixLockedBalanceSnapshots +type BalanceType uint8 + +const ( + DESOBalance BalanceType = 0 + CreatorCoinLockedBalance BalanceType = 1 + ValidatorStakedDESOBalance BalanceType = 2 + LockedStakeDESOBalance BalanceType = 3 +) + +func (balanceType BalanceType) BalanceTypePrefix() byte { + switch balanceType { + case DESOBalance: + return PrefixBalanceSnapshots + case CreatorCoinLockedBalance: + return PrefixLockedBalanceSnapshots + case ValidatorStakedDESOBalance: + return PrefixValidatorStakedBalanceSnapshots + case LockedStakeDESOBalance: + return PrefixLockedStakeBalanceSnapshots + default: + panic("unknown balance type") } +} - prefix := append([]byte{}, startPrefix) +func balanceSnapshotKey(balanceType BalanceType, publicKey *lib.PublicKey, blockHeight uint64, balance uint64) []byte { + prefix := append([]byte{}, balanceType.BalanceTypePrefix()) prefix = append(prefix, publicKey[:]...) prefix = append(prefix, lib.EncodeUint64(blockHeight)...) prefix = append(prefix, lib.EncodeUint64(balance)...) return prefix } -func hypersyncHeightToBlockKey(blockHeight uint64, isLocked bool) []byte { +func lockedStakeBalanceSnapshotKey( + balanceType BalanceType, stakerPKID *lib.PKID, validatorPKID *lib.PKID, lockedAtEpochNumber uint64, + blockHeight uint64, balance uint64, +) []byte { + prefix := append([]byte{}, balanceType.BalanceTypePrefix()) + prefix = append(prefix, stakerPKID[:]...) + prefix = append(prefix, validatorPKID[:]...) + prefix = append(prefix, lib.EncodeUint64(lockedAtEpochNumber)...) + prefix = append(prefix, lib.EncodeUint64(blockHeight)...) + prefix = append(prefix, lib.EncodeUint64(balance)...) + return prefix +} - lockedByte := byte(0) - if isLocked { - lockedByte = byte(1) +func (balanceType BalanceType) HypersyncBalanceTypePrefix() byte { + switch balanceType { + case DESOBalance: + return 0 + case CreatorCoinLockedBalance: + return 1 + case ValidatorStakedDESOBalance: + return 2 + case LockedStakeDESOBalance: + return 3 + default: + panic("unknown balance type") } +} +func hypersyncHeightToBlockKey(blockHeight uint64, balanceType BalanceType) []byte { prefix := append([]byte{}, PrefixHypersyncBlockHeightToBalances) prefix = append(prefix, lib.EncodeUint64(blockHeight)...) - prefix = append(prefix, lockedByte) + prefix = append(prefix, balanceType.HypersyncBalanceTypePrefix()) return prefix } -func (index *RosettaIndex) PutHypersyncBlockBalances(blockHeight uint64, isLocked bool, balances map[lib.PublicKey]uint64) { - +func (index *RosettaIndex) PutHypersyncBlockBalances( + blockHeight uint64, balanceType BalanceType, balances map[lib.PublicKey]uint64) { + if balanceType != DESOBalance && balanceType != CreatorCoinLockedBalance && + balanceType != ValidatorStakedDESOBalance { + glog.Error("PutHypersyncBlockBalances: Invalid balance type passed in") + return + } err := index.db.Update(func(txn *badger.Txn) error { blockBytes := bytes.NewBuffer([]byte{}) if err := gob.NewEncoder(blockBytes).Encode(&balances); err != nil { return err } - return txn.Set(hypersyncHeightToBlockKey(blockHeight, isLocked), blockBytes.Bytes()) + return txn.Set(hypersyncHeightToBlockKey(blockHeight, balanceType), blockBytes.Bytes()) }) if err != nil { glog.Error(errors.Wrapf(err, "PutHypersyncBlockBalances: Problem putting block: Error:")) } } +func (index *RosettaIndex) PutHypersyncBlockLockedStakeBalances( + blockHeight uint64, stakeEntries map[LockedStakeBalanceMapKey]uint64, balanceType BalanceType) { + if balanceType != LockedStakeDESOBalance { + glog.Error("PutHypersyncBlockLockedStakeBalances: Invalid balance type passed in") + return + } + err := index.db.Update(func(txn *badger.Txn) error { + blockBytes := bytes.NewBuffer([]byte{}) + if err := gob.NewEncoder(blockBytes).Encode(&stakeEntries); err != nil { + return err + } + return txn.Set(hypersyncHeightToBlockKey(blockHeight, balanceType), blockBytes.Bytes()) + }) + if err != nil { + glog.Error(errors.Wrapf(err, "PutHypersyncBlockLockedStakeBalances: Problem putting block: Error:")) + } +} + func (index *RosettaIndex) GetHypersyncBlockBalances(blockHeight uint64) ( - _balances map[lib.PublicKey]uint64, _lockedBalances map[lib.PublicKey]uint64) { + _balances map[lib.PublicKey]uint64, _lockedBalances map[lib.PublicKey]uint64, + _stakedDESOBalances map[lib.PublicKey]uint64, _lockedStakeDESOBalances map[LockedStakeBalanceMapKey]uint64) { balances := make(map[lib.PublicKey]uint64) - lockedBalances := make(map[lib.PublicKey]uint64) + creatorCoinLockedBalances := make(map[lib.PublicKey]uint64) + validatorStakedDESOBalances := make(map[lib.PublicKey]uint64) + lockedStakeDESOBalances := make(map[LockedStakeBalanceMapKey]uint64) err := index.db.View(func(txn *badger.Txn) error { - itemBalances, err := txn.Get(hypersyncHeightToBlockKey(blockHeight, false)) + itemBalances, err := txn.Get(hypersyncHeightToBlockKey(blockHeight, DESOBalance)) if err != nil { return err } @@ -152,43 +187,75 @@ func (index *RosettaIndex) GetHypersyncBlockBalances(blockHeight uint64) ( if err != nil { return err } - if err := gob.NewDecoder(bytes.NewReader(balancesBytes)).Decode(&balances); err != nil { + if err = gob.NewDecoder(bytes.NewReader(balancesBytes)).Decode(&balances); err != nil { return err } - itemLocked, err := txn.Get(hypersyncHeightToBlockKey(blockHeight, true)) + itemCreatorCoinLockedBalances, err := txn.Get(hypersyncHeightToBlockKey(blockHeight, CreatorCoinLockedBalance)) if err != nil { return err } - lockedBytes, err := itemLocked.ValueCopy(nil) + creatorCoinLockedBalancesBytes, err := itemCreatorCoinLockedBalances.ValueCopy(nil) if err != nil { return err } - if err := gob.NewDecoder(bytes.NewReader(lockedBytes)).Decode(&lockedBalances); err != nil { + if err = gob.NewDecoder(bytes.NewReader(creatorCoinLockedBalancesBytes)).Decode( + &creatorCoinLockedBalances); err != nil { + return err + } + itemValidatorStakedBalances, err := txn.Get(hypersyncHeightToBlockKey(blockHeight, ValidatorStakedDESOBalance)) + if err != nil { + return err + } + validatorStakedBalancesBytes, err := itemValidatorStakedBalances.ValueCopy(nil) + if err != nil { + return err + } + if err = gob.NewDecoder(bytes.NewReader(validatorStakedBalancesBytes)). + Decode(&validatorStakedDESOBalances); err != nil { + return err + } + lockedStakedItemBalances, err := txn.Get(hypersyncHeightToBlockKey(blockHeight, LockedStakeDESOBalance)) + if err != nil { + return err + } + lockedStakedBalancesBytes, err := lockedStakedItemBalances.ValueCopy(nil) + if err != nil { + return err + } + if err = gob.NewDecoder(bytes.NewReader(lockedStakedBalancesBytes)). + Decode(&lockedStakeDESOBalances); err != nil { return err } return nil }) + // TODO: Handle this error better. if err != nil { glog.Error(errors.Wrapf(err, "GetHypersyncBlockBalances: Problem getting block at height (%v)", blockHeight)) } - return balances, lockedBalances + return balances, creatorCoinLockedBalances, validatorStakedDESOBalances, lockedStakeDESOBalances } func (index *RosettaIndex) PutBalanceSnapshot( - height uint64, isLockedBalance bool, balances map[lib.PublicKey]uint64) error { - + height uint64, balanceType BalanceType, balances map[lib.PublicKey]uint64) error { + if balanceType != DESOBalance && balanceType != CreatorCoinLockedBalance && + balanceType != ValidatorStakedDESOBalance { + return errors.New("PutBalanceSnapshot: Invalid balance type passed in") + } return index.db.Update(func(txn *badger.Txn) error { - return index.PutBalanceSnapshotWithTxn(txn, height, isLockedBalance, balances) + return index.PutBalanceSnapshotWithTxn(txn, height, balanceType, balances) }) } func (index *RosettaIndex) PutBalanceSnapshotWithTxn( - txn *badger.Txn, height uint64, isLockedBalance bool, balances map[lib.PublicKey]uint64) error { - + txn *badger.Txn, height uint64, balanceType BalanceType, balances map[lib.PublicKey]uint64) error { + if balanceType != DESOBalance && balanceType != CreatorCoinLockedBalance && + balanceType != ValidatorStakedDESOBalance { + return errors.New("PutBalanceSnapshotWithTxn: Invalid balance type passed in") + } for pk, bal := range balances { - if err := txn.Set(balanceSnapshotKey(isLockedBalance, &pk, height, bal), []byte{}); err != nil { + if err := txn.Set(balanceSnapshotKey(balanceType, &pk, height, bal), []byte{}); err != nil { return errors.Wrapf(err, "Error in PutBalanceSnapshot for block height: "+ "%v pub key: %v balance: %v", height, pk, bal) } @@ -197,18 +264,65 @@ func (index *RosettaIndex) PutBalanceSnapshotWithTxn( } func (index *RosettaIndex) PutSingleBalanceSnapshotWithTxn( - txn *badger.Txn, height uint64, isLockedBalance bool, publicKey lib.PublicKey, balance uint64) error { - - if err := txn.Set(balanceSnapshotKey(isLockedBalance, &publicKey, height, balance), []byte{}); err != nil { + txn *badger.Txn, height uint64, balanceType BalanceType, publicKey lib.PublicKey, balance uint64) error { + if balanceType != DESOBalance && balanceType != CreatorCoinLockedBalance && + balanceType != ValidatorStakedDESOBalance { + return errors.New("PutSingleBalanceSnapshotWithTxn: Invalid balance type passed in") + } + if err := txn.Set(balanceSnapshotKey(balanceType, &publicKey, height, balance), []byte{}); err != nil { return errors.Wrapf(err, "Error in PutBalanceSnapshot for block height: "+ "%v pub key: %v balance: %v", height, publicKey, balance) } return nil } -func GetBalanceForPublicKeyAtBlockHeightWithTxn( - txn *badger.Txn, isLockedBalance bool, publicKey *lib.PublicKey, blockHeight uint64) uint64 { +func (index *RosettaIndex) PutLockedStakeBalanceSnapshot( + height uint64, balanceType BalanceType, balances map[LockedStakeBalanceMapKey]uint64) error { + if balanceType != LockedStakeDESOBalance { + return errors.New("PutLockedStakeBalanceSnapshot: Invalid balance type passed in") + } + return index.db.Update(func(txn *badger.Txn) error { + return index.PutLockedStakeBalanceSnapshotWithTxn(txn, height, balanceType, balances) + }) +} + +func (index *RosettaIndex) PutLockedStakeBalanceSnapshotWithTxn( + txn *badger.Txn, height uint64, balanceType BalanceType, balances map[LockedStakeBalanceMapKey]uint64) error { + if balanceType != LockedStakeDESOBalance { + return errors.New("PutLockedStakeBalanceSnapshotWithTxn: Invalid balance type passed in") + } + for pk, bal := range balances { + if err := txn.Set(lockedStakeBalanceSnapshotKey( + balanceType, &pk.StakerPKID, &pk.ValidatorPKID, pk.LockedAtEpochNumber, height, bal), []byte{}, + ); err != nil { + return errors.Wrapf(err, "Error in PutLockedStakeBalanceSnapshotWithTxn for block height: "+ + "%v pub key: %v balance: %v", height, pk, bal) + } + } + return nil +} +func (index *RosettaIndex) PutSingleLockedStakeBalanceSnapshotWithTxn( + txn *badger.Txn, height uint64, balanceType BalanceType, stakerPKID *lib.PKID, + validatorPKID *lib.PKID, lockedAtEpochNumber uint64, balance uint64) error { + if balanceType != LockedStakeDESOBalance { + return errors.New("PutSingleLockedStakeBalanceSnapshotWithTxn: Invalid balance type passed in") + } + if err := txn.Set(lockedStakeBalanceSnapshotKey( + balanceType, stakerPKID, validatorPKID, lockedAtEpochNumber, height, balance), []byte{}); err != nil { + return errors.Wrapf(err, "Error in PutSingleLockedStakeBalanceSnapshotWithTxn for block height: "+ + "%v staker pkid: %v validator pkid: %v balance: %v", height, stakerPKID, validatorPKID, balance) + } + return nil +} + +func GetBalanceForPublicKeyAtBlockHeightWithTxn( + txn *badger.Txn, balanceType BalanceType, publicKey *lib.PublicKey, blockHeight uint64) uint64 { + if balanceType != DESOBalance && balanceType != CreatorCoinLockedBalance && + balanceType != ValidatorStakedDESOBalance { + glog.Errorf("GetBalanceForPublicKeyAtBlockHeightWithTxn: Invalid balance type passed in") + return 0 + } opts := badger.DefaultIteratorOptions opts.PrefetchValues = false @@ -221,7 +335,7 @@ func GetBalanceForPublicKeyAtBlockHeightWithTxn( // Since we iterate backwards, the prefix must be bigger than all possible // values that could actually exist. This means the key we use the pubkey // and block height with max balance. - maxPrefix := balanceSnapshotKey(isLockedBalance, publicKey, blockHeight, math.MaxUint64) + maxPrefix := balanceSnapshotKey(balanceType, publicKey, blockHeight, math.MaxUint64) // We don't want to consider any keys that don't involve our public key. This // will cause the iteration to stop if we don't have any values for our current // public key. @@ -246,11 +360,82 @@ func GetBalanceForPublicKeyAtBlockHeightWithTxn( return lib.DecodeUint64(keyFound[1+len(publicKey)+len(lib.EncodeUint64(blockHeight)):]) } -func (index *RosettaIndex) GetBalanceSnapshot(isLockedBalance bool, publicKey *lib.PublicKey, blockHeight uint64) uint64 { +func (index *RosettaIndex) GetBalanceSnapshot( + balanceType BalanceType, publicKey *lib.PublicKey, blockHeight uint64, +) uint64 { + if balanceType != DESOBalance && balanceType != CreatorCoinLockedBalance && + balanceType != ValidatorStakedDESOBalance { + glog.Errorf("GetBalanceSnapshot: Invalid balance type passed in") + return 0 + } balanceFound := uint64(0) index.db.View(func(txn *badger.Txn) error { balanceFound = GetBalanceForPublicKeyAtBlockHeightWithTxn( - txn, isLockedBalance, publicKey, blockHeight) + txn, balanceType, publicKey, blockHeight) + return nil + }) + + return balanceFound +} + +func GetLockedStakeBalanceForPublicKeyAtBlockHeightWithTxn( + txn *badger.Txn, balanceType BalanceType, stakerPKID *lib.PKID, validatorPKID *lib.PKID, lockedAtEpochNumber uint64, + blockHeight uint64, +) uint64 { + if balanceType != LockedStakeDESOBalance { + glog.Errorf("GetLockedStakeBalanceForPublicKeyAtBlockHeightWithTxn: Invalid balance type passed in") + return 0 + } + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false + + // Go in reverse order. + opts.Reverse = true + + it := txn.NewIterator(opts) + defer it.Close() + + // Since we iterate backwards, the prefix must be bigger than all possible + // values that could actually exist. This means the key we use the pubkey + // and block height with max balance. + maxPrefix := lockedStakeBalanceSnapshotKey( + balanceType, stakerPKID, validatorPKID, lockedAtEpochNumber, blockHeight, math.MaxUint64) + // We don't want to consider any keys that don't involve our public key. This + // will cause the iteration to stop if we don't have any values for our current + // public key. + // One byte for the prefix + validForPrefix := maxPrefix[:1+len(stakerPKID.ToBytes())] + var keyFound []byte + for it.Seek(maxPrefix); it.ValidForPrefix(validForPrefix); it.Next() { + item := it.Item() + keyFound = item.Key() + // Break after the first key we iterate over because we only + // want the first one. + break + } + // No key found means this user's balance has never appeared in a block. + if keyFound == nil { + return 0 + } + + // If we get here we found a valid key. Decode the balance from it + // and return it. + // One byte for the prefix + return lib.DecodeUint64(keyFound[1+len(stakerPKID.ToBytes())+len(lib.EncodeUint64(blockHeight)):]) +} + +func (index *RosettaIndex) GetLockedStakeBalanceSnapshot( + balanceType BalanceType, stakerPKID *lib.PKID, validatorPKID *lib.PKID, lockedAtEpochNumber uint64, + blockHeight uint64, +) uint64 { + if balanceType != LockedStakeDESOBalance { + glog.Errorf("GetLockedStakeBalanceSnapshot: Invalid balance type passed in") + return 0 + } + balanceFound := uint64(0) + index.db.View(func(txn *badger.Txn) error { + balanceFound = GetLockedStakeBalanceForPublicKeyAtBlockHeightWithTxn( + txn, balanceType, stakerPKID, validatorPKID, lockedAtEpochNumber, blockHeight) return nil }) diff --git a/deso/node.go b/deso/node.go index ab23e2c..2388164 100644 --- a/deso/node.go +++ b/deso/node.go @@ -234,7 +234,7 @@ func (node *Node) Start(exitChannels ...*chan os.Signal) { rosettaIndexOpts := lib.PerformanceBadgerOptions(rosettaIndexDir) rosettaIndexOpts.ValueDir = rosettaIndexDir rosettaIndex, err := badger.Open(rosettaIndexOpts) - node.Index = NewIndex(rosettaIndex) + node.Index = NewIndex(rosettaIndex, node.chainDB) // Listen to transaction and block events so we can fill RosettaIndex with relevant data node.EventManager = lib.NewEventManager() @@ -294,6 +294,8 @@ func (node *Node) Start(exitChannels ...*chan os.Signal) { "", lib.HypersyncDefaultMaxQueueSize, ) + // Set the snapshot on the rosetta index. + node.Index.snapshot = node.GetBlockchain().Snapshot() if err != nil { if shouldRestart { glog.Infof(lib.CLog(lib.Red, fmt.Sprintf("Start: Got en error while starting server and shouldRestart "+ diff --git a/deso/types.go b/deso/types.go index 64e632f..954172d 100644 --- a/deso/types.go +++ b/deso/types.go @@ -18,6 +18,15 @@ const ( // CreatorCoin is the SubAccount address for a public key's // total DESO locked in their creator coin. CreatorCoin = "CREATOR_COIN" + + // ValidatorEntry is the SubAccount address prefix for a + // PKID's total DESO staked to a given validator entry. + ValidatorEntry = "VALIDATOR_ENTRY" + + // LockedStakeEntry is the SubAccount address prefix for + // a PKID's total DESO locked in a given locked + // stake entry. + LockedStakeEntry = "LOCKED_STAKE_ENTRY" ) var ( diff --git a/go.mod b/go.mod index c042916..fceef6f 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/deso-protocol/core v1.1.0 github.com/deso-protocol/go-deadlock v1.0.0 github.com/deso-protocol/go-merkle-tree v1.0.0 - github.com/dgraph-io/badger/v3 v3.2103.0 + github.com/dgraph-io/badger/v3 v3.2103.5 github.com/golang/glog v1.0.0 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 @@ -34,7 +34,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/decred/dcrd/lru v1.1.1 // indirect - github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/ethereum/go-ethereum v1.9.25 // indirect github.com/fatih/color v1.13.0 // indirect @@ -87,10 +87,10 @@ require ( github.com/vmihailenco/tagparser v0.1.2 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.opencensus.io v0.23.0 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2 // indirect + golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect golang.org/x/text v0.3.6 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect golang.org/x/tools v0.1.5 // indirect diff --git a/go.sum b/go.sum index 9838699..f6ea074 100644 --- a/go.sum +++ b/go.sum @@ -129,11 +129,13 @@ github.com/deso-protocol/go-merkle-tree v1.0.0/go.mod h1:V/vbg/maaNv6G7zf9VVs645 github.com/dgraph-io/badger/v2 v2.2007.2/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= github.com/dgraph-io/badger/v3 v3.2103.0 h1:abkD2EnP3+6Tj8h5LI1y00dJ9ICKTIAzvG9WmZ8S2c4= github.com/dgraph-io/badger/v3 v3.2103.0/go.mod h1:GHMCYxuDWyzbHkh4k3yyg4PM61tJPFfEGSMbE3Vd5QE= +github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.4-0.20210309073149-3836124cdc5a/go.mod h1:MIonLggsKgZLUSt414ExgwNtlOL5MuEoAJP514mwGe8= github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= @@ -217,6 +219,7 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -311,6 +314,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -551,6 +555,7 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -607,6 +612,7 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -661,6 +667,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2 h1:wM1k/lXfpc5HdkJJyW9GELpd8ERGdnh8sMGL6Gzq3Ho= golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/services/account_service.go b/services/account_service.go index 165e9d5..5098f97 100644 --- a/services/account_service.go +++ b/services/account_service.go @@ -5,7 +5,9 @@ import ( "encoding/hex" "fmt" "github.com/deso-protocol/rosetta-deso/deso" + "math" "strconv" + "strings" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" @@ -57,6 +59,7 @@ func accountBalanceCurrent(node *deso.Node, account *types.AccountIdentifier) (* return nil, wrapErr(ErrDeSo, err) } + // TODO: Hook up new PosMempool mempoolView, err := node.GetMempool().GetAugmentedUniversalView() if err != nil { return nil, wrapErr(ErrDeSo, err) @@ -85,6 +88,78 @@ func accountBalanceCurrent(node *deso.Node, account *types.AccountIdentifier) (* if mempoolProfileEntry != nil { mempoolBalance = mempoolProfileEntry.CreatorCoinEntry.DeSoLockedNanos } + } else if account.SubAccount.Address == deso.ValidatorEntry { + + var dbValidatorEntry *lib.ValidatorEntry + dbValidatorPKID := dbView.GetPKIDForPublicKey(publicKeyBytes).PKID + dbValidatorEntry, err = dbView.GetValidatorByPKID(dbValidatorPKID) + if err != nil { + return nil, wrapErr(ErrDeSo, err) + } + if dbValidatorEntry != nil { + if !dbValidatorEntry.TotalStakeAmountNanos.IsUint64() { + return nil, wrapErr(ErrDeSo, fmt.Errorf("TotalStakeAmountNanos is not a uint64")) + } + dbBalance = dbValidatorEntry.TotalStakeAmountNanos.Uint64() + } + + mempoolValidatorPKID := mempoolView.GetPKIDForPublicKey(publicKeyBytes).PKID + + var mempoolValidatorEntry *lib.ValidatorEntry + mempoolValidatorEntry, err = mempoolView.GetValidatorByPKID(mempoolValidatorPKID) + if err != nil { + return nil, wrapErr(ErrDeSo, err) + } + if mempoolValidatorEntry != nil { + if !mempoolValidatorEntry.TotalStakeAmountNanos.IsUint64() { + return nil, wrapErr(ErrDeSo, fmt.Errorf("TotalStakeAmountNanos is not a uint64")) + } + mempoolBalance = mempoolValidatorEntry.TotalStakeAmountNanos.Uint64() + } + } else if strings.HasPrefix(account.SubAccount.Address, deso.LockedStakeEntry) { + // Pull out the validator PKID + var validatorPKID *lib.PKID + validatorPKID, err = node.GetValidatorPKIDFromSubAccountIdentifier(account.SubAccount) + if err != nil { + return nil, wrapErr(ErrDeSo, err) + } + + dbStakerPKID := dbView.GetPKIDForPublicKey(publicKeyBytes).PKID + dbLockedStakeEntries, err := dbView.GetLockedStakeEntriesInRange(validatorPKID, dbStakerPKID, 0, math.MaxUint64) + if err != nil { + return nil, wrapErr(ErrDeSo, err) + } + dbRunningBalance := uint64(0) + for _, dbLockedStakeEntry := range dbLockedStakeEntries { + if !dbLockedStakeEntry.LockedAmountNanos.IsUint64() { + return nil, wrapErr(ErrDeSo, fmt.Errorf("AmountNanos is not a uint64")) + } + dbRunningBalance, err = lib.SafeUint64().Add(dbRunningBalance, dbLockedStakeEntry.LockedAmountNanos.Uint64()) + if err != nil { + return nil, wrapErr(ErrDeSo, err) + } + } + dbBalance = dbRunningBalance + + mempoolStakerPKID := mempoolView.GetPKIDForPublicKey(publicKeyBytes).PKID + + mempoolStakeEntries, err := mempoolView.GetLockedStakeEntriesInRange(validatorPKID, mempoolStakerPKID, 0, math.MaxUint64) + if err != nil { + return nil, wrapErr(ErrDeSo, err) + } + mempoolRunningBalance := uint64(0) + for _, mempoolStakeEntry := range mempoolStakeEntries { + if !mempoolStakeEntry.LockedAmountNanos.IsUint64() { + return nil, wrapErr(ErrDeSo, fmt.Errorf("AmountNanos is not a uint64")) + } + mempoolRunningBalance, err = lib.SafeUint64().Add(mempoolRunningBalance, mempoolStakeEntry.LockedAmountNanos.Uint64()) + if err != nil { + return nil, wrapErr(ErrDeSo, err) + } + } + mempoolBalance = mempoolRunningBalance + } else { + return nil, wrapErr(ErrDeSo, fmt.Errorf("Invalid SubAccount")) } block := &types.BlockIdentifier{ @@ -141,12 +216,24 @@ func accountBalanceSnapshot(node *deso.Node, account *types.AccountIdentifier, b return nil, wrapErr(ErrInvalidPublicKey, err) } publicKey := lib.NewPublicKey(publicKeyBytes) - + // We assume that address is a PKID for certain subaccounts. Parsing the bytes + // as a NewPKID will give us the appropriate subaccount. + pkid := lib.NewPKID(publicKeyBytes) var balance uint64 if account.SubAccount == nil { - balance = node.Index.GetBalanceSnapshot(false, publicKey, blockHeight) + balance = node.Index.GetBalanceSnapshot(deso.DESOBalance, publicKey, blockHeight) } else if account.SubAccount.Address == deso.CreatorCoin { - balance = node.Index.GetBalanceSnapshot(true, publicKey, blockHeight) + balance = node.Index.GetBalanceSnapshot(deso.CreatorCoinLockedBalance, publicKey, blockHeight) + } else if strings.HasPrefix(account.SubAccount.Address, deso.ValidatorEntry) { + balance = node.Index.GetBalanceSnapshot(deso.ValidatorStakedDESOBalance, publicKey, blockHeight) + } else if strings.HasPrefix(account.SubAccount.Address, deso.LockedStakeEntry) { + validatorPKID, err := node.GetValidatorPKIDFromSubAccountIdentifier(account.SubAccount) + if err != nil { + return nil, wrapErr(ErrDeSo, err) + } + balance = node.Index.GetLockedStakeBalanceSnapshot(deso.LockedStakeDESOBalance, pkid, validatorPKID, 0, blockHeight) + } else { + return nil, wrapErr(ErrDeSo, fmt.Errorf("Invalid SubAccount")) } //fmt.Printf("height: %v, addr (cc): %v, bal: %v\n", desoBlock.Header.Height, lib.PkToStringTestnet(publicKeyBytes), balance) @@ -190,6 +277,7 @@ func (s *AccountAPIService) AccountCoins( return nil, wrapErr(ErrDeSo, err) } + // TODO: Update for balance model. coins := []*types.Coin{} for _, utxoEntry := range utxoEntries { diff --git a/services/construction_service.go b/services/construction_service.go index aa71564..be88d28 100644 --- a/services/construction_service.go +++ b/services/construction_service.go @@ -160,7 +160,7 @@ func (s *ConstructionAPIService) ConstructionMetadata(ctx context.Context, reque } var options preprocessOptions - if err := types.UnmarshalMap(request.Options, &options); err != nil { + if err = types.UnmarshalMap(request.Options, &options); err != nil { return nil, wrapErr(ErrUnableToParseIntermediateResult, err) } diff --git a/services/mempool_service.go b/services/mempool_service.go index e0de29d..011ebf0 100644 --- a/services/mempool_service.go +++ b/services/mempool_service.go @@ -27,12 +27,7 @@ func (s *MempoolAPIService) Mempool(ctx context.Context, request *types.NetworkR //return nil, wrapErr(ErrUnavailableOffline, nil) } - mempool := s.node.GetMempool() - transactions, _, err := mempool.GetTransactionsOrderedByTimeAdded() - if err != nil { - return nil, ErrDeSo - } - + transactions := s.node.GetMempool().GetOrderedTransactions() transactionIdentifiers := []*types.TransactionIdentifier{} for _, transaction := range transactions { transactionIdentifiers = append(transactionIdentifiers, &types.TransactionIdentifier{Hash: transaction.Hash.String()})