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

[stakingindex] historical staking indexer #4290

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
156 changes: 156 additions & 0 deletions pkg/util/abiutil/param.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package abiutil

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/pkg/errors"

"github.com/iotexproject/iotex-address/address"

"github.com/iotexproject/iotex-core/action"
)

type (
// EventParam is a struct to hold smart contract event parameters, which can easily convert a param to go type
EventParam struct {
params []any
nameToIndex map[string]int
}
)

var (
// ErrInvlidEventParam is an error for invalid event param
ErrInvlidEventParam = errors.New("invalid event param")
)

// EventField is a helper function to get a field from event param
func EventField[T any](e EventParam, name string) (T, error) {
id, ok := e.nameToIndex[name]
if !ok {
var zeroValue T
return zeroValue, errors.Wrapf(ErrInvlidEventParam, "field %s not found", name)
}
return EventFieldByID[T](e, id)
}

// EventFieldByID is a helper function to get a field from event param
func EventFieldByID[T any](e EventParam, id int) (T, error) {
field, ok := e.fieldByID(id).(T)
if !ok {
return field, errors.Wrapf(ErrInvlidEventParam, "field %d got %#v, expect %T", id, e.fieldByID(id), field)
}
return field, nil
}

func (e EventParam) field(name string) any {
return e.params[e.nameToIndex[name]]
}

func (e EventParam) fieldByID(id int) any {
return e.params[id]
}

func (e EventParam) String() string {
return fmt.Sprintf("%+v", e.params)
}

// FieldUint256 is a helper function to get a uint256 field from event param
func (e EventParam) FieldUint256(name string) (*big.Int, error) {
return EventField[*big.Int](e, name)
}

// FieldByIDUint256 is a helper function to get a uint256 field from event param
func (e EventParam) FieldByIDUint256(id int) (*big.Int, error) {
return EventFieldByID[*big.Int](e, id)
}

// FieldBytes12 is a helper function to get a bytes12 field from event param
func (e EventParam) FieldBytes12(name string) (string, error) {
id, ok := e.nameToIndex[name]
if !ok {
return "", errors.Wrapf(ErrInvlidEventParam, "field %s not found", name)
}
return e.FieldByIDBytes12(id)
}

// FieldByIDBytes12 is a helper function to get a bytes12 field from event param
func (e EventParam) FieldByIDBytes12(id int) (string, error) {
data, err := EventFieldByID[[12]byte](e, id)
if err != nil {
return "", err
}
// remove trailing zeros
tail := len(data) - 1
for ; tail >= 0 && data[tail] == 0; tail-- {
}
return string(data[:tail+1]), nil
}

// FieldUint256Slice is a helper function to get a uint256 slice field from event param
func (e EventParam) FieldUint256Slice(name string) ([]*big.Int, error) {
return EventField[[]*big.Int](e, name)
}

// FieldByIDUint256Slice is a helper function to get a uint256 slice field from event param
func (e EventParam) FieldByIDUint256Slice(id int) ([]*big.Int, error) {
return EventFieldByID[[]*big.Int](e, id)
}

// FieldAddress is a helper function to get an address field from event param
func (e EventParam) FieldAddress(name string) (address.Address, error) {
commAddr, err := EventField[common.Address](e, name)
if err != nil {
return nil, err
}
return address.FromBytes(commAddr.Bytes())
}

// FieldByIDAddress is a helper function to get an address field from event param
func (e EventParam) FieldByIDAddress(id int) (address.Address, error) {
commAddr, err := EventFieldByID[common.Address](e, id)
if err != nil {
return nil, err
}
return address.FromBytes(commAddr.Bytes())
}

// UnpackEventParam is a helper function to unpack event parameters
func UnpackEventParam(abiEvent *abi.Event, log *action.Log) (*EventParam, error) {
// unpack non-indexed fields
params := make(map[string]any)
if len(log.Data) > 0 {
if err := abiEvent.Inputs.UnpackIntoMap(params, log.Data); err != nil {
return nil, errors.Wrap(err, "unpack event data failed")
}
}
// unpack indexed fields
args := make(abi.Arguments, 0)
for _, arg := range abiEvent.Inputs {
if arg.Indexed {
args = append(args, arg)
}
}
topics := make([]common.Hash, 0)
for i, topic := range log.Topics {
if i > 0 {
topics = append(topics, common.Hash(topic))
}
}
err := abi.ParseTopicsIntoMap(params, args, topics)
if err != nil {
return nil, errors.Wrap(err, "unpack event indexed fields failed")
}
// create event param
event := &EventParam{
params: make([]any, 0, len(abiEvent.Inputs)),
nameToIndex: make(map[string]int),
}
for i, arg := range abiEvent.Inputs {
event.params = append(event.params, params[arg.Name])
event.nameToIndex[arg.Name] = i
}
return event, nil
}
113 changes: 113 additions & 0 deletions systemcontractindex/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package systemcontractindex

import (
"context"

"github.com/pkg/errors"

"github.com/iotexproject/iotex-core/db"
"github.com/iotexproject/iotex-core/db/batch"
"github.com/iotexproject/iotex-core/pkg/util/byteutil"
)

type (
// IndexerCommon is the common struct for all contract indexers
// It provides the basic functions, including
// 1. kvstore
// 2. put/get index height
// 3. contract address
IndexerCommon struct {
kvstore db.KVStore
ns string
key []byte
startHeight uint64
height uint64
contractAddress string
}

stateType interface {
Load(kvstore db.KVStore) error
}
kvStoreWithVersion interface {
db.KVStore
WithVersion(version uint64) db.KVStore
}
)

// NewIndexerCommon creates a new IndexerCommon
func NewIndexerCommon(kvstore db.KVStore, ns string, key []byte, contractAddress string, startHeight uint64) *IndexerCommon {
return &IndexerCommon{
kvstore: kvstore,
ns: ns,
key: key,
startHeight: startHeight,
contractAddress: contractAddress,
}
}

// Start starts the indexer
func (s *IndexerCommon) Start(ctx context.Context) error {
if err := s.kvstore.Start(ctx); err != nil {
return err
}
h, err := s.loadHeight()
if err != nil {
return err
}
s.height = h
return nil
}

// Stop stops the indexer
func (s *IndexerCommon) Stop(ctx context.Context) error {
return s.kvstore.Stop(ctx)
}

// StateAt loads the state at the given height
func (s *IndexerCommon) StateAt(state stateType, height uint64) error {
if kvstore, ok := s.kvstore.(kvStoreWithVersion); ok {
return state.Load(kvstore.WithVersion(height))
}
return errors.New("kvstore does not support versioning")
}

// ContractAddress returns the contract address
func (s *IndexerCommon) ContractAddress() string { return s.contractAddress }

// Height returns the tip block height
func (s *IndexerCommon) Height() uint64 {
return s.height
}

func (s *IndexerCommon) loadHeight() (uint64, error) {
// get the tip block height
var height uint64
h, err := s.kvstore.Get(s.ns, s.key)
if err != nil {
if !errors.Is(err, db.ErrNotExist) {
return 0, err
}
height = 0
} else {
height = byteutil.BytesToUint64BigEndian(h)
}
return height, nil
}

// StartHeight returns the start height of the indexer
func (s *IndexerCommon) StartHeight() uint64 { return s.startHeight }

// Commit commits the height to the indexer
func (s *IndexerCommon) Commit(height uint64, delta batch.KVStoreBatch) error {
s.height = height
delta.Put(s.ns, s.key, byteutil.Uint64ToBytesBigEndian(height), "failed to put height")
return s.kvstore.WriteBatch(delta)
}

// ExpectedHeight returns the expected height
func (s *IndexerCommon) ExpectedHeight() uint64 {
if s.height < s.startHeight {
return s.startHeight
}
return s.height + 1
}
124 changes: 124 additions & 0 deletions systemcontractindex/stakingindex/bucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package stakingindex

import (
"math/big"
"time"

"github.com/iotexproject/iotex-address/address"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"

"github.com/iotexproject/iotex-core/action/protocol/staking"
"github.com/iotexproject/iotex-core/pkg/util/byteutil"
"github.com/iotexproject/iotex-core/systemcontractindex/stakingindex/stakingpb"
)

type VoteBucket = staking.VoteBucket

type Bucket struct {
Candidate address.Address
Owner address.Address
StakedAmount *big.Int
StakedDurationBlockNumber uint64
CreatedAt uint64
UnlockedAt uint64
UnstakedAt uint64
}

func (bi *Bucket) Serialize() []byte {
return byteutil.Must(proto.Marshal(bi.toProto()))
}

// Deserialize deserializes the bucket info
func (bi *Bucket) Deserialize(b []byte) error {
m := stakingpb.Bucket{}
if err := proto.Unmarshal(b, &m); err != nil {
return err
}
return bi.loadProto(&m)
}

// clone clones the bucket info
func (bi *Bucket) toProto() *stakingpb.Bucket {
return &stakingpb.Bucket{
Candidate: bi.Candidate.String(),
CreatedAt: bi.CreatedAt,
Owner: bi.Owner.String(),
UnlockedAt: bi.UnlockedAt,
UnstakedAt: bi.UnstakedAt,
Amount: bi.StakedAmount.String(),
Duration: bi.StakedDurationBlockNumber,
}
}

func (bi *Bucket) loadProto(p *stakingpb.Bucket) error {
candidate, err := address.FromString(p.Candidate)
if err != nil {
return err
}
owner, err := address.FromString(p.Owner)
if err != nil {
return err
}
amount, ok := new(big.Int).SetString(p.Amount, 10)
if !ok {
return errors.Errorf("invalid staked amount %s", p.Amount)
}
bi.CreatedAt = p.CreatedAt
bi.UnlockedAt = p.UnlockedAt
bi.UnstakedAt = p.UnstakedAt
bi.Candidate = candidate
bi.Owner = owner
bi.StakedAmount = amount
bi.StakedDurationBlockNumber = p.Duration
return nil
}

func (b *Bucket) Clone() *Bucket {
clone := &Bucket{
StakedAmount: b.StakedAmount,
StakedDurationBlockNumber: b.StakedDurationBlockNumber,
CreatedAt: b.CreatedAt,
UnlockedAt: b.UnlockedAt,
UnstakedAt: b.UnstakedAt,
}
candidate, _ := address.FromBytes(b.Candidate.Bytes())
clone.Candidate = candidate
owner, _ := address.FromBytes(b.Owner.Bytes())
clone.Owner = owner
stakingAmount := new(big.Int).Set(b.StakedAmount)
clone.StakedAmount = stakingAmount
return clone
}

func assembleVoteBucket(token uint64, bkt *Bucket, contractAddr string, blockInterval time.Duration) *VoteBucket {
vb := VoteBucket{
Index: token,
StakedAmount: bkt.StakedAmount,
StakedDuration: time.Duration(bkt.StakedDurationBlockNumber) * blockInterval,
StakedDurationBlockNumber: bkt.StakedDurationBlockNumber,
CreateBlockHeight: bkt.CreatedAt,
StakeStartBlockHeight: bkt.CreatedAt,
UnstakeStartBlockHeight: bkt.UnstakedAt,
AutoStake: bkt.UnlockedAt == maxBlockNumber,
Candidate: bkt.Candidate,
Owner: bkt.Owner,
ContractAddress: contractAddr,
}
if bkt.UnlockedAt != maxBlockNumber {
vb.StakeStartBlockHeight = bkt.UnlockedAt
}
return &vb
}

func batchAssembleVoteBucket(idxs []uint64, bkts []*Bucket, contractAddr string, blockInterval time.Duration) []*VoteBucket {
vbs := make([]*VoteBucket, 0, len(idxs))
for i := range idxs {
if bkts[i] == nil {
vbs = append(vbs, nil)
continue
}
vbs = append(vbs, assembleVoteBucket(idxs[i], bkts[i], contractAddr, blockInterval))
}
return vbs
}
Loading
Loading