mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-25 03:47:18 +00:00
native: optimize vote reward data (fix #2844)
Signed-off-by: ZhangTao1596 <zhangtao@ngd.neo.org>
This commit is contained in:
parent
7b109586ca
commit
fb7fce0775
9 changed files with 177 additions and 69 deletions
|
@ -123,3 +123,10 @@ formats.
|
|||
|
||||
Removal of Peer unmarshalling with string based ports is scheduled for ~September 2023
|
||||
(~0.105.0 release).
|
||||
|
||||
## `NEOBalance` from stack item
|
||||
|
||||
We check struct items count before convert LastGasPerVote to let RPC client be compatible with
|
||||
old versions.
|
||||
|
||||
Removal of this compatiblility code is scheduled for Sep-Oct 2023.
|
||||
|
|
|
@ -2363,7 +2363,12 @@ unsubloop:
|
|||
// CalculateClaimable calculates the amount of GAS generated by owning specified
|
||||
// amount of NEO between specified blocks.
|
||||
func (bc *Blockchain) CalculateClaimable(acc util.Uint160, endHeight uint32) (*big.Int, error) {
|
||||
return bc.contracts.NEO.CalculateBonus(bc.dao, acc, endHeight)
|
||||
nextBlock, err := bc.getFakeNextBlock(bc.BlockHeight() + 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ic := bc.newInteropContext(trigger.Application, bc.dao, nextBlock, nil)
|
||||
return bc.contracts.NEO.CalculateBonus(ic, acc, endHeight)
|
||||
}
|
||||
|
||||
// FeePerByte returns transaction network fee per byte.
|
||||
|
|
|
@ -739,9 +739,6 @@ func TestBlockchain_GetTransaction(t *testing.T) {
|
|||
|
||||
func TestBlockchain_GetClaimable(t *testing.T) {
|
||||
bc, acc := chain.NewSingle(t)
|
||||
e := neotest.NewExecutor(t, bc, acc, acc)
|
||||
|
||||
e.GenerateNewBlocks(t, 10)
|
||||
|
||||
t.Run("first generation period", func(t *testing.T) {
|
||||
amount, err := bc.CalculateClaimable(acc.ScriptHash(), 1)
|
||||
|
|
|
@ -409,7 +409,7 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
|
|||
var (
|
||||
cs = cache.committee
|
||||
isCacheRW bool
|
||||
key = make([]byte, 38)
|
||||
key = make([]byte, 34)
|
||||
)
|
||||
for i := range cs {
|
||||
if cs[i].Votes.Sign() > 0 {
|
||||
|
@ -423,17 +423,9 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
|
|||
tmp.Div(tmp, cs[i].Votes)
|
||||
|
||||
key = makeVoterKey([]byte(cs[i].Key), key)
|
||||
r := n.getLatestGASPerVote(ic.DAO, key)
|
||||
tmp.Add(tmp, &r)
|
||||
|
||||
var r *big.Int
|
||||
if g, ok := cache.gasPerVoteCache[cs[i].Key]; ok {
|
||||
r = &g
|
||||
} else {
|
||||
reward := n.getGASPerVote(ic.DAO, key[:34], []uint32{ic.Block.Index + 1})
|
||||
r = &reward[0]
|
||||
}
|
||||
tmp.Add(tmp, r)
|
||||
|
||||
binary.BigEndian.PutUint32(key[34:], ic.Block.Index+1)
|
||||
if !isCacheRW {
|
||||
cache = ic.DAO.GetRWCache(n.ID).(*NeoCache)
|
||||
isCacheRW = true
|
||||
|
@ -447,33 +439,19 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *NEO) getGASPerVote(d *dao.Simple, key []byte, indexes []uint32) []big.Int {
|
||||
sort.Slice(indexes, func(i, j int) bool {
|
||||
return indexes[i] < indexes[j]
|
||||
})
|
||||
start := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(start, indexes[len(indexes)-1])
|
||||
|
||||
need := len(indexes)
|
||||
var reward = make([]big.Int, need)
|
||||
collected := 0
|
||||
d.Seek(n.ID, storage.SeekRange{
|
||||
Prefix: key,
|
||||
Start: start,
|
||||
Backwards: true,
|
||||
}, func(k, v []byte) bool {
|
||||
if len(k) == 4 {
|
||||
num := binary.BigEndian.Uint32(k)
|
||||
for i, ind := range indexes {
|
||||
if reward[i].Sign() == 0 && num <= ind {
|
||||
reward[i] = *bigint.FromBytes(v)
|
||||
collected++
|
||||
}
|
||||
}
|
||||
}
|
||||
return collected < need
|
||||
})
|
||||
return reward
|
||||
func (n *NEO) getLatestGASPerVote(d *dao.Simple, key []byte) big.Int {
|
||||
var g big.Int
|
||||
cache := d.GetROCache(n.ID).(*NeoCache)
|
||||
if g, ok := cache.gasPerVoteCache[string(key[1:])]; ok {
|
||||
return g
|
||||
}
|
||||
item := d.GetStorageItem(n.ID, key)
|
||||
if item == nil {
|
||||
g = *big.NewInt(0)
|
||||
} else {
|
||||
g = *bigint.FromBytes(item)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func (n *NEO) increaseBalance(ic *interop.Context, h util.Uint160, si *state.StorageItem, amount *big.Int, checkBal *big.Int) (func(), error) {
|
||||
|
@ -527,11 +505,15 @@ func (n *NEO) distributeGas(ic *interop.Context, acc *state.NEOBalance) (*big.In
|
|||
if ic.Block == nil || ic.Block.Index == 0 || ic.Block.Index == acc.BalanceHeight {
|
||||
return nil, nil
|
||||
}
|
||||
gen, err := n.calculateBonus(ic.DAO, acc.VoteTo, &acc.Balance, acc.BalanceHeight, ic.Block.Index)
|
||||
gen, err := n.calculateBonus(ic.DAO, acc, ic.Block.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acc.BalanceHeight = ic.Block.Index
|
||||
if acc.VoteTo != nil {
|
||||
latestGasPerVote := n.getLatestGASPerVote(ic.DAO, makeVoterKey(acc.VoteTo.Bytes()))
|
||||
acc.LastGasPerVote = latestGasPerVote
|
||||
}
|
||||
|
||||
return gen, nil
|
||||
}
|
||||
|
@ -539,7 +521,7 @@ func (n *NEO) distributeGas(ic *interop.Context, acc *state.NEOBalance) (*big.In
|
|||
func (n *NEO) unclaimedGas(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
u := toUint160(args[0])
|
||||
end := uint32(toBigInt(args[1]).Int64())
|
||||
gen, err := n.CalculateBonus(ic.DAO, u, end)
|
||||
gen, err := n.CalculateBonus(ic, u, end)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -647,10 +629,7 @@ func (n *NEO) dropCandidateIfZero(d *dao.Simple, cache *NeoCache, pub *keys.Publ
|
|||
d.DeleteStorageItem(n.ID, makeValidatorKey(pub))
|
||||
|
||||
voterKey := makeVoterKey(pub.Bytes())
|
||||
d.Seek(n.ID, storage.SeekRange{Prefix: voterKey}, func(k, v []byte) bool {
|
||||
d.DeleteStorageItem(n.ID, append(voterKey, k...)) // d.Seek cuts prefix, thus need to append it again.
|
||||
return true
|
||||
})
|
||||
d.DeleteStorageItem(n.ID, voterKey)
|
||||
delete(cache.gasPerVoteCache, string(voterKey))
|
||||
|
||||
return true
|
||||
|
@ -661,7 +640,7 @@ func makeVoterKey(pub []byte, prealloc ...[]byte) []byte {
|
|||
if len(prealloc) != 0 {
|
||||
key = prealloc[0]
|
||||
} else {
|
||||
key = make([]byte, 34, 38)
|
||||
key = make([]byte, 34)
|
||||
}
|
||||
key[0] = prefixVoterRewardPerCommittee
|
||||
copy(key[1:], pub)
|
||||
|
@ -670,9 +649,12 @@ func makeVoterKey(pub []byte, prealloc ...[]byte) []byte {
|
|||
|
||||
// CalculateBonus calculates amount of gas generated for holding value NEO from start to end block
|
||||
// and having voted for active committee member.
|
||||
func (n *NEO) CalculateBonus(d *dao.Simple, acc util.Uint160, end uint32) (*big.Int, error) {
|
||||
func (n *NEO) CalculateBonus(ic *interop.Context, acc util.Uint160, end uint32) (*big.Int, error) {
|
||||
if ic.Block == nil || end != ic.Block.Index {
|
||||
return nil, errors.New("can't calculate bonus of height unequal (BlockHeight + 1)")
|
||||
}
|
||||
key := makeAccountKey(acc)
|
||||
si := d.GetStorageItem(n.ID, key)
|
||||
si := ic.DAO.GetStorageItem(n.ID, key)
|
||||
if si == nil {
|
||||
return nil, storage.ErrKeyNotFound
|
||||
}
|
||||
|
@ -680,19 +662,19 @@ func (n *NEO) CalculateBonus(d *dao.Simple, acc util.Uint160, end uint32) (*big.
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n.calculateBonus(d, st.VoteTo, &st.Balance, st.BalanceHeight, end)
|
||||
return n.calculateBonus(ic.DAO, st, end)
|
||||
}
|
||||
|
||||
func (n *NEO) calculateBonus(d *dao.Simple, vote *keys.PublicKey, value *big.Int, start, end uint32) (*big.Int, error) {
|
||||
r, err := n.CalculateNEOHolderReward(d, value, start, end)
|
||||
if err != nil || vote == nil {
|
||||
func (n *NEO) calculateBonus(d *dao.Simple, acc *state.NEOBalance, end uint32) (*big.Int, error) {
|
||||
r, err := n.CalculateNEOHolderReward(d, &acc.Balance, acc.BalanceHeight, end)
|
||||
if err != nil || acc.VoteTo == nil {
|
||||
return r, err
|
||||
}
|
||||
|
||||
var key = makeVoterKey(vote.Bytes())
|
||||
var reward = n.getGASPerVote(d, key, []uint32{start, end})
|
||||
var tmp = (&reward[1]).Sub(&reward[1], &reward[0])
|
||||
tmp.Mul(tmp, value)
|
||||
var key = makeVoterKey(acc.VoteTo.Bytes())
|
||||
var reward = n.getLatestGASPerVote(d, key)
|
||||
var tmp = big.NewInt(0).Sub(&reward, &acc.LastGasPerVote)
|
||||
tmp.Mul(tmp, &acc.Balance)
|
||||
tmp.Div(tmp, bigVoterRewardFactor)
|
||||
tmp.Add(tmp, r)
|
||||
return tmp, nil
|
||||
|
@ -869,6 +851,9 @@ func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pub *keys.Public
|
|||
if err := n.ModifyAccountVotes(acc, ic.DAO, new(big.Int).Neg(&acc.Balance), false); err != nil {
|
||||
return err
|
||||
}
|
||||
if pub != nil && pub != acc.VoteTo {
|
||||
acc.LastGasPerVote = n.getLatestGASPerVote(ic.DAO, makeVoterKey(pub.Bytes()))
|
||||
}
|
||||
oldVote := acc.VoteTo
|
||||
acc.VoteTo = pub
|
||||
if err := n.ModifyAccountVotes(acc, ic.DAO, &acc.Balance, true); err != nil {
|
||||
|
|
|
@ -3,13 +3,16 @@ package native_test
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/internal/contracts"
|
||||
"github.com/nspcc-dev/neo-go/internal/random"
|
||||
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||
|
@ -305,6 +308,15 @@ func TestNEO_GetAccountState(t *testing.T) {
|
|||
neoValidatorInvoker := newNeoValidatorsClient(t)
|
||||
e := neoValidatorInvoker.Executor
|
||||
|
||||
cfg := e.Chain.GetConfig()
|
||||
committeeSize := cfg.GetCommitteeSize(0)
|
||||
validatorSize := cfg.GetNumOfCNs(0)
|
||||
advanceChain := func(t *testing.T) {
|
||||
for i := 0; i < committeeSize; i++ {
|
||||
neoValidatorInvoker.AddNewBlock(t)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
neoValidatorInvoker.Invoke(t, stackitem.Null{}, "getAccountState", util.Uint160{})
|
||||
})
|
||||
|
@ -318,8 +330,100 @@ func TestNEO_GetAccountState(t *testing.T) {
|
|||
stackitem.Make(amount),
|
||||
stackitem.Make(lub),
|
||||
stackitem.Null{},
|
||||
stackitem.Make(0),
|
||||
}), "getAccountState", acc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("lastGasPerVote", func(t *testing.T) {
|
||||
const (
|
||||
GasPerBlock = 5
|
||||
VoterRewardRatio = 80
|
||||
)
|
||||
getAccountState := func(t *testing.T, account util.Uint160) *state.NEOBalance {
|
||||
stack, err := neoValidatorInvoker.TestInvoke(t, "getAccountState", account)
|
||||
require.NoError(t, err)
|
||||
as := new(state.NEOBalance)
|
||||
err = as.FromStackItem(stack.Pop().Item())
|
||||
require.NoError(t, err)
|
||||
return as
|
||||
}
|
||||
|
||||
amount := int64(1000)
|
||||
acc := e.NewAccount(t)
|
||||
neoValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), acc.ScriptHash(), amount, nil)
|
||||
as := getAccountState(t, acc.ScriptHash())
|
||||
require.Equal(t, uint64(amount), as.Balance.Uint64())
|
||||
require.Equal(t, e.Chain.BlockHeight(), as.BalanceHeight)
|
||||
require.Equal(t, uint64(0), as.LastGasPerVote.Uint64())
|
||||
committee, _ := e.Chain.GetCommittee()
|
||||
neoValidatorInvoker.WithSigners(e.Validator, e.Validator.(neotest.MultiSigner).Single(0)).Invoke(t, true, "registerCandidate", committee[0].Bytes())
|
||||
neoValidatorInvoker.WithSigners(acc).Invoke(t, true, "vote", acc.ScriptHash(), committee[0].Bytes())
|
||||
as = getAccountState(t, acc.ScriptHash())
|
||||
require.Equal(t, uint64(0), as.LastGasPerVote.Uint64())
|
||||
advanceChain(t)
|
||||
neoValidatorInvoker.WithSigners(acc).Invoke(t, true, "transfer", acc.ScriptHash(), acc.ScriptHash(), amount, nil)
|
||||
as = getAccountState(t, acc.ScriptHash())
|
||||
expect := GasPerBlock * native.GASFactor * VoterRewardRatio / 100 * (uint64(e.Chain.BlockHeight()) / uint64(committeeSize))
|
||||
expect = expect * uint64(committeeSize) / uint64(validatorSize+committeeSize) * native.NEOTotalSupply / as.Balance.Uint64()
|
||||
require.Equal(t, e.Chain.BlockHeight(), as.BalanceHeight)
|
||||
require.Equal(t, expect, as.LastGasPerVote.Uint64())
|
||||
})
|
||||
}
|
||||
|
||||
func TestNEO_GetAccountStateInteropAPI(t *testing.T) {
|
||||
neoValidatorInvoker := newNeoValidatorsClient(t)
|
||||
e := neoValidatorInvoker.Executor
|
||||
|
||||
cfg := e.Chain.GetConfig()
|
||||
committeeSize := cfg.GetCommitteeSize(0)
|
||||
validatorSize := cfg.GetNumOfCNs(0)
|
||||
advanceChain := func(t *testing.T) {
|
||||
for i := 0; i < committeeSize; i++ {
|
||||
neoValidatorInvoker.AddNewBlock(t)
|
||||
}
|
||||
}
|
||||
|
||||
amount := int64(1000)
|
||||
acc := e.NewAccount(t)
|
||||
neoValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), acc.ScriptHash(), amount, nil)
|
||||
committee, _ := e.Chain.GetCommittee()
|
||||
neoValidatorInvoker.WithSigners(e.Validator, e.Validator.(neotest.MultiSigner).Single(0)).Invoke(t, true, "registerCandidate", committee[0].Bytes())
|
||||
neoValidatorInvoker.WithSigners(acc).Invoke(t, true, "vote", acc.ScriptHash(), committee[0].Bytes())
|
||||
advanceChain(t)
|
||||
neoValidatorInvoker.WithSigners(acc).Invoke(t, true, "transfer", acc.ScriptHash(), acc.ScriptHash(), amount, nil)
|
||||
|
||||
var hashAStr string
|
||||
for i := 0; i < util.Uint160Size; i++ {
|
||||
hashAStr += fmt.Sprintf("%#x", acc.ScriptHash()[i])
|
||||
if i != util.Uint160Size-1 {
|
||||
hashAStr += ", "
|
||||
}
|
||||
}
|
||||
src := `package testaccountstate
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/neo"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
)
|
||||
func GetLastGasPerVote() int {
|
||||
accState := neo.GetAccountState(interop.Hash160{` + hashAStr + `})
|
||||
if accState == nil {
|
||||
panic("nil state")
|
||||
}
|
||||
return accState.LastGasPerVote
|
||||
}`
|
||||
ctr := neotest.CompileSource(t, e.Validator.ScriptHash(), strings.NewReader(src), &compiler.Options{
|
||||
Name: "testaccountstate_contract",
|
||||
})
|
||||
e.DeployContract(t, ctr, nil)
|
||||
|
||||
const (
|
||||
GasPerBlock = 5
|
||||
VoterRewardRatio = 80
|
||||
)
|
||||
expect := GasPerBlock * native.GASFactor * VoterRewardRatio / 100 * (uint64(e.Chain.BlockHeight()) / uint64(committeeSize))
|
||||
expect = expect * uint64(committeeSize) / uint64(validatorSize+committeeSize) * native.NEOTotalSupply / uint64(amount)
|
||||
ctrInvoker := e.NewInvoker(ctr.Hash, e.Committee)
|
||||
ctrInvoker.Invoke(t, stackitem.Make(expect), "getLastGasPerVote")
|
||||
}
|
||||
|
||||
func TestNEO_CommitteeBountyOnPersist(t *testing.T) {
|
||||
|
|
|
@ -19,8 +19,9 @@ type NEP17Balance struct {
|
|||
// NEOBalance represents the balance state of a NEO-token.
|
||||
type NEOBalance struct {
|
||||
NEP17Balance
|
||||
BalanceHeight uint32
|
||||
VoteTo *keys.PublicKey
|
||||
BalanceHeight uint32
|
||||
VoteTo *keys.PublicKey
|
||||
LastGasPerVote big.Int
|
||||
}
|
||||
|
||||
// NEP17BalanceFromBytes converts the serialized NEP17Balance to a structure.
|
||||
|
@ -125,6 +126,7 @@ func (s *NEOBalance) ToStackItem() (stackitem.Item, error) {
|
|||
stackitem.NewBigInteger(&s.Balance),
|
||||
stackitem.NewBigInteger(big.NewInt(int64(s.BalanceHeight))),
|
||||
voteItem,
|
||||
stackitem.NewBigInteger(&s.LastGasPerVote),
|
||||
}), nil
|
||||
}
|
||||
|
||||
|
@ -157,5 +159,12 @@ func (s *NEOBalance) FromStackItem(item stackitem.Item) error {
|
|||
return fmt.Errorf("invalid public key bytes: %w", err)
|
||||
}
|
||||
s.VoteTo = pub
|
||||
if len(structItem) >= 4 {
|
||||
lastGasPerVote, err := structItem[3].TryInteger()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid last vote reward per neo stackitem: %w", err)
|
||||
}
|
||||
s.LastGasPerVote = *lastGasPerVote
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -15,9 +15,10 @@ import (
|
|||
|
||||
// AccountState contains info about a NEO holder.
|
||||
type AccountState struct {
|
||||
Balance int
|
||||
Height int
|
||||
VoteTo interop.PublicKey
|
||||
Balance int
|
||||
Height int
|
||||
VoteTo interop.PublicKey
|
||||
LastGasPerVote int
|
||||
}
|
||||
|
||||
// Hash represents NEO contract hash.
|
||||
|
|
|
@ -372,9 +372,9 @@ func TestClientNEOContract(t *testing.T) {
|
|||
require.Equal(t, int64(1000_0000_0000), regP)
|
||||
|
||||
acc0 := testchain.PrivateKey(0).PublicKey().GetScriptHash()
|
||||
uncl, err := neoR.UnclaimedGas(acc0, 100)
|
||||
uncl, err := neoR.UnclaimedGas(acc0, chain.BlockHeight()+1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, big.NewInt(48000), uncl)
|
||||
require.Equal(t, big.NewInt(10000), uncl)
|
||||
|
||||
accState, err := neoR.GetAccountState(acc0)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -83,7 +83,7 @@ const (
|
|||
faultedTxHashLE = "82279bfe9bada282ca0f8cb8e0bb124b921af36f00c69a518320322c6f4fef60"
|
||||
faultedTxBlock uint32 = 23
|
||||
invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA"
|
||||
block20StateRootLE = "33b4cee6a59b9dc9d186fc235dc81e2ffe74418d7d777d538422a62b8e635ef2"
|
||||
block20StateRootLE = "a2841baec40c6b752ba959c2b2cfee20b6beeabb85460224929bc9ff358bf8d2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -943,11 +943,11 @@ var rpcTestCases = map[string][]rpcTestCase{
|
|||
}, {
|
||||
State: "Added",
|
||||
Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb},
|
||||
Value: []byte{0x41, 0x03, 0x21, 0x01, 0x01, 0x21, 0x01, 0x18, 0},
|
||||
Value: []byte{0x41, 0x04, 0x21, 0x01, 0x01, 0x21, 0x01, 0x18, 0x00, 0x21, 0x00},
|
||||
}, {
|
||||
State: "Changed",
|
||||
Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2},
|
||||
Value: []byte{0x41, 0x03, 0x21, 0x04, 0x2f, 0xd9, 0xf5, 0x05, 0x21, 0x01, 0x18, 0},
|
||||
Value: []byte{0x41, 0x04, 0x21, 0x04, 0x2f, 0xd9, 0xf5, 0x05, 0x21, 0x01, 0x18, 0x00, 0x21, 0x00},
|
||||
}, {
|
||||
State: "Changed",
|
||||
Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2},
|
||||
|
|
Loading…
Reference in a new issue