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 }