Skip to content

Commit

Permalink
[stakingindex] implement indexer for new staking contract (#4237)
Browse files Browse the repository at this point in the history
  • Loading branch information
envestcc authored Jun 7, 2024
1 parent f69d4cd commit 2f8f01a
Show file tree
Hide file tree
Showing 9 changed files with 1,608 additions and 0 deletions.
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
}
101 changes: 101 additions & 0 deletions systemcontractindex/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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"
)

// 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
type IndexerCommon struct {
kvstore db.KVStore
ns string
key []byte
startHeight uint64
height uint64
contractAddress string
}

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

// KVStore returns the kvstore
func (s *IndexerCommon) KVStore() db.KVStore { return s.kvstore }

// 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 {
delta.Put(s.ns, s.key, byteutil.Uint64ToBytesBigEndian(height), "failed to put height")
if err := s.kvstore.WriteBatch(delta); err != nil {
return err
}
s.height = height
return nil
}

// ExpectedHeight returns the expected height
func (s *IndexerCommon) ExpectedHeight() uint64 {
if s.height < s.startHeight {
return s.startHeight
}
return s.height + 1
}
122 changes: 122 additions & 0 deletions systemcontractindex/stakingindex/bucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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{
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
clone.StakedAmount = new(big.Int).Set(b.StakedAmount)
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

0 comments on commit 2f8f01a

Please sign in to comment.