github.com/iotexproject/iotex-core@v1.14.1-rc1/action/protocol/poll/nativestaking.go (about)

     1  // Copyright (c) 2020 IoTeX Foundation
     2  // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
     3  // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
     4  // This source code is governed by Apache License 2.0 that can be found in the LICENSE file.
     5  
     6  package poll
     7  
     8  import (
     9  	"context"
    10  	"math/big"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/ethereum/go-ethereum/accounts/abi"
    15  	"github.com/ethereum/go-ethereum/common"
    16  	"github.com/pkg/errors"
    17  	"go.uber.org/zap"
    18  
    19  	"github.com/iotexproject/iotex-address/address"
    20  	"github.com/iotexproject/iotex-core/action/protocol"
    21  	"github.com/iotexproject/iotex-core/action/protocol/rolldpos"
    22  	"github.com/iotexproject/iotex-core/pkg/log"
    23  	"github.com/iotexproject/iotex-core/state"
    24  	"github.com/iotexproject/iotex-election/types"
    25  )
    26  
    27  var (
    28  	// ErrNoData is an error that there's no data in the contract
    29  	ErrNoData = errors.New("no data")
    30  	// ErrEndOfData is an error that reaching end of data in the contract
    31  	ErrEndOfData = errors.New("end of data")
    32  	// ErrWrongData is an error that data is wrong
    33  	ErrWrongData = errors.New("wrong data")
    34  )
    35  
    36  type (
    37  	// ReadContract defines a callback function to read contract
    38  	ReadContract func(context.Context, string, []byte, bool) ([]byte, error)
    39  	// NativeStaking represents native staking struct
    40  	NativeStaking struct {
    41  		readContract   ReadContract
    42  		contract       string
    43  		abi            abi.ABI
    44  		bufferEpochNum uint64
    45  		bufferResult   *VoteTally
    46  	}
    47  
    48  	pygg struct {
    49  		Count           *big.Int
    50  		Indexes         []*big.Int
    51  		StakeStartTimes []*big.Int
    52  		StakeDurations  []*big.Int
    53  		Decays          []bool
    54  		StakedAmounts   []*big.Int
    55  		CanNames        [][12]byte
    56  		Owners          []common.Address
    57  	}
    58  
    59  	// VoteTally is a map of candidates on native chain
    60  	VoteTally struct {
    61  		Candidates map[[12]byte]*state.Candidate
    62  		Buckets    []*types.Bucket
    63  	}
    64  )
    65  
    66  // NewNativeStaking creates a NativeStaking instance
    67  func NewNativeStaking(readContract ReadContract) (*NativeStaking, error) {
    68  	abi, err := abi.JSON(strings.NewReader(NsAbi))
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	if readContract == nil {
    74  		return nil, errors.New("failed to create native staking: empty read contract callback")
    75  	}
    76  
    77  	return &NativeStaking{
    78  		abi:            abi,
    79  		readContract:   readContract,
    80  		bufferEpochNum: 0,
    81  		bufferResult:   nil,
    82  	}, nil
    83  }
    84  
    85  // Votes returns the votes on height
    86  func (ns *NativeStaking) Votes(ctx context.Context, ts time.Time, correctGas bool) (*VoteTally, error) {
    87  	if ns.contract == "" {
    88  		return nil, ErrNoData
    89  	}
    90  	bcCtx := protocol.MustGetBlockchainCtx(ctx)
    91  	rp := rolldpos.MustGetProtocol(protocol.MustGetRegistry(ctx))
    92  	tipEpochNum := rp.GetEpochNum(bcCtx.Tip.Height)
    93  	if ns.bufferEpochNum == tipEpochNum && ns.bufferResult != nil {
    94  		log.L().Info("Using cache native staking data", zap.Uint64("tip height", bcCtx.Tip.Height))
    95  		return ns.bufferResult, nil
    96  	}
    97  	// read voter list from staking contract
    98  	votes := VoteTally{
    99  		Candidates: make(map[[12]byte]*state.Candidate),
   100  		Buckets:    make([]*types.Bucket, 0),
   101  	}
   102  	prevIndex := big.NewInt(0)
   103  	limit := big.NewInt(256)
   104  	featureCtx := protocol.MustGetFeatureCtx(ctx)
   105  
   106  	for {
   107  		vote, index, err := ns.readBuckets(ctx, prevIndex, limit, correctGas)
   108  		log.L().Debug("Read native buckets from contract", zap.Int("size", len(vote)))
   109  		if err == ErrEndOfData {
   110  			// all data been read
   111  			break
   112  		}
   113  		if err != nil {
   114  			log.L().Error(" read native staking contract", zap.Error(err))
   115  			return nil, err
   116  		}
   117  		err = votes.tally(vote, ts)
   118  		if featureCtx.FixUnproductiveDelegates && err != nil {
   119  			log.L().Error(" read vote tally", zap.Error(err))
   120  			return nil, err
   121  		}
   122  		if len(vote) < int(limit.Int64()) {
   123  			// all data been read
   124  			break
   125  		}
   126  		prevIndex = index
   127  	}
   128  	ns.bufferEpochNum = tipEpochNum
   129  	ns.bufferResult = &votes
   130  
   131  	return &votes, nil
   132  }
   133  
   134  func (ns *NativeStaking) readBuckets(ctx context.Context, prevIndx, limit *big.Int, correctGas bool) ([]*types.Bucket, *big.Int, error) {
   135  	data, err := ns.abi.Pack("getActivePyggs", prevIndx, limit)
   136  	if err != nil {
   137  		return nil, nil, err
   138  	}
   139  
   140  	data, err = ns.readContract(ctx, ns.contract, data, correctGas)
   141  	if err != nil {
   142  		return nil, nil, err
   143  	}
   144  
   145  	// decode the contract read result
   146  	res, err := ns.abi.Unpack("getActivePyggs", data)
   147  	if err != nil {
   148  		if err.Error() == "abi: attempting to unmarshall an empty string while arguments are expected" {
   149  			// no data in contract (one possible reason is that contract does not exist yet)
   150  			return nil, nil, ErrNoData
   151  		}
   152  		return nil, nil, err
   153  	}
   154  	pygg, err := toPgyy(res)
   155  	if err != nil {
   156  		return nil, nil, err
   157  	}
   158  	if len(pygg.CanNames) == 0 {
   159  		return nil, nil, ErrEndOfData
   160  	}
   161  
   162  	buckets := make([]*types.Bucket, len(pygg.CanNames))
   163  	for i := range pygg.CanNames {
   164  		buckets[i], err = types.NewBucket(
   165  			time.Unix(pygg.StakeStartTimes[i].Int64(), 0),
   166  			time.Duration(pygg.StakeDurations[i].Uint64()*24)*time.Hour,
   167  			pygg.StakedAmounts[i],
   168  			pygg.Owners[i].Bytes(),
   169  			pygg.CanNames[i][:],
   170  			pygg.Decays[i],
   171  		)
   172  		if err != nil {
   173  			return nil, nil, err
   174  		}
   175  	}
   176  	// last one of returned indexes should be used as starting index for next query
   177  	return buckets, pygg.Indexes[len(pygg.Indexes)-1], nil
   178  }
   179  
   180  // SetContract sets the contract address
   181  func (ns *NativeStaking) SetContract(contract string) {
   182  	if _, err := address.FromString(contract); err != nil {
   183  		zap.S().Panicf("Invalid staking contract %s", contract)
   184  	}
   185  	ns.contract = contract
   186  	zap.S().Infof("Set native staking contract address = %s", contract)
   187  }
   188  
   189  func (vt *VoteTally) tally(buckets []*types.Bucket, now time.Time) error {
   190  	for i := range buckets {
   191  		v := buckets[i]
   192  		weighted := types.CalcWeightedVotes(v, now)
   193  		if big.NewInt(0).Cmp(weighted) == 1 {
   194  			return errors.Errorf("weighted amount %s cannot be negative", weighted)
   195  		}
   196  		k := to12Bytes(v.Candidate())
   197  		if c, ok := vt.Candidates[k]; !ok {
   198  			vt.Candidates[k] = &state.Candidate{
   199  				Address:       "",
   200  				Votes:         weighted,
   201  				RewardAddress: "",
   202  				CanName:       v.Candidate(),
   203  			}
   204  		} else {
   205  			// add up the votes
   206  			c.Votes.Add(c.Votes, weighted)
   207  		}
   208  		vt.Buckets = append(vt.Buckets, v)
   209  	}
   210  	return nil
   211  }
   212  
   213  func to12Bytes(b []byte) [12]byte {
   214  	var h [12]byte
   215  	if len(b) != 12 {
   216  		panic("invalid CanName: abi stipulates CanName must be [12]byte")
   217  	}
   218  	copy(h[:], b)
   219  	return h
   220  }
   221  
   222  func toPgyy(v []interface{}) (*pygg, error) {
   223  	// struct pygg has 8 fields
   224  	if len(v) != 8 {
   225  		return nil, ErrWrongData
   226  	}
   227  
   228  	c, ok := v[0].(*big.Int)
   229  	if !ok {
   230  		return nil, ErrWrongData
   231  	}
   232  	index, ok := v[1].([]*big.Int)
   233  	if !ok {
   234  		return nil, ErrWrongData
   235  	}
   236  	start, ok := v[2].([]*big.Int)
   237  	if !ok {
   238  		return nil, ErrWrongData
   239  	}
   240  	duration, ok := v[3].([]*big.Int)
   241  	if !ok {
   242  		return nil, ErrWrongData
   243  	}
   244  	decay, ok := v[4].([]bool)
   245  	if !ok {
   246  		return nil, ErrWrongData
   247  	}
   248  	amount, ok := v[5].([]*big.Int)
   249  	if !ok {
   250  		return nil, ErrWrongData
   251  	}
   252  	name, ok := v[6].([][12]byte)
   253  	if !ok {
   254  		return nil, ErrWrongData
   255  	}
   256  	owner, ok := v[7].([]common.Address)
   257  	if !ok {
   258  		return nil, ErrWrongData
   259  	}
   260  	return &pygg{
   261  		Count:           c,
   262  		Indexes:         index,
   263  		StakeStartTimes: start,
   264  		StakeDurations:  duration,
   265  		Decays:          decay,
   266  		StakedAmounts:   amount,
   267  		CanNames:        name,
   268  		Owners:          owner,
   269  	}, nil
   270  }