native: optimize vote reward data (fix #2844)

Signed-off-by: ZhangTao1596 <zhangtao@ngd.neo.org>
This commit is contained in:
ZhangTao1596 2023-04-20 10:46:19 +08:00
parent 7b109586ca
commit fb7fce0775
9 changed files with 177 additions and 69 deletions

View file

@ -123,3 +123,10 @@ formats.
Removal of Peer unmarshalling with string based ports is scheduled for ~September 2023 Removal of Peer unmarshalling with string based ports is scheduled for ~September 2023
(~0.105.0 release). (~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.

View file

@ -2363,7 +2363,12 @@ unsubloop:
// CalculateClaimable calculates the amount of GAS generated by owning specified // CalculateClaimable calculates the amount of GAS generated by owning specified
// amount of NEO between specified blocks. // amount of NEO between specified blocks.
func (bc *Blockchain) CalculateClaimable(acc util.Uint160, endHeight uint32) (*big.Int, error) { 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. // FeePerByte returns transaction network fee per byte.

View file

@ -739,9 +739,6 @@ func TestBlockchain_GetTransaction(t *testing.T) {
func TestBlockchain_GetClaimable(t *testing.T) { func TestBlockchain_GetClaimable(t *testing.T) {
bc, acc := chain.NewSingle(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) { t.Run("first generation period", func(t *testing.T) {
amount, err := bc.CalculateClaimable(acc.ScriptHash(), 1) amount, err := bc.CalculateClaimable(acc.ScriptHash(), 1)

View file

@ -409,7 +409,7 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
var ( var (
cs = cache.committee cs = cache.committee
isCacheRW bool isCacheRW bool
key = make([]byte, 38) key = make([]byte, 34)
) )
for i := range cs { for i := range cs {
if cs[i].Votes.Sign() > 0 { if cs[i].Votes.Sign() > 0 {
@ -423,17 +423,9 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
tmp.Div(tmp, cs[i].Votes) tmp.Div(tmp, cs[i].Votes)
key = makeVoterKey([]byte(cs[i].Key), key) 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 { if !isCacheRW {
cache = ic.DAO.GetRWCache(n.ID).(*NeoCache) cache = ic.DAO.GetRWCache(n.ID).(*NeoCache)
isCacheRW = true isCacheRW = true
@ -447,33 +439,19 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
return nil return nil
} }
func (n *NEO) getGASPerVote(d *dao.Simple, key []byte, indexes []uint32) []big.Int { func (n *NEO) getLatestGASPerVote(d *dao.Simple, key []byte) big.Int {
sort.Slice(indexes, func(i, j int) bool { var g big.Int
return indexes[i] < indexes[j] cache := d.GetROCache(n.ID).(*NeoCache)
}) if g, ok := cache.gasPerVoteCache[string(key[1:])]; ok {
start := make([]byte, 4) return g
binary.BigEndian.PutUint32(start, indexes[len(indexes)-1]) }
item := d.GetStorageItem(n.ID, key)
need := len(indexes) if item == nil {
var reward = make([]big.Int, need) g = *big.NewInt(0)
collected := 0 } else {
d.Seek(n.ID, storage.SeekRange{ g = *bigint.FromBytes(item)
Prefix: key, }
Start: start, return g
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) increaseBalance(ic *interop.Context, h util.Uint160, si *state.StorageItem, amount *big.Int, checkBal *big.Int) (func(), error) { 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 { if ic.Block == nil || ic.Block.Index == 0 || ic.Block.Index == acc.BalanceHeight {
return nil, nil 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 { if err != nil {
return nil, err return nil, err
} }
acc.BalanceHeight = ic.Block.Index acc.BalanceHeight = ic.Block.Index
if acc.VoteTo != nil {
latestGasPerVote := n.getLatestGASPerVote(ic.DAO, makeVoterKey(acc.VoteTo.Bytes()))
acc.LastGasPerVote = latestGasPerVote
}
return gen, nil 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 { func (n *NEO) unclaimedGas(ic *interop.Context, args []stackitem.Item) stackitem.Item {
u := toUint160(args[0]) u := toUint160(args[0])
end := uint32(toBigInt(args[1]).Int64()) end := uint32(toBigInt(args[1]).Int64())
gen, err := n.CalculateBonus(ic.DAO, u, end) gen, err := n.CalculateBonus(ic, u, end)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -647,10 +629,7 @@ func (n *NEO) dropCandidateIfZero(d *dao.Simple, cache *NeoCache, pub *keys.Publ
d.DeleteStorageItem(n.ID, makeValidatorKey(pub)) d.DeleteStorageItem(n.ID, makeValidatorKey(pub))
voterKey := makeVoterKey(pub.Bytes()) voterKey := makeVoterKey(pub.Bytes())
d.Seek(n.ID, storage.SeekRange{Prefix: voterKey}, func(k, v []byte) bool { d.DeleteStorageItem(n.ID, voterKey)
d.DeleteStorageItem(n.ID, append(voterKey, k...)) // d.Seek cuts prefix, thus need to append it again.
return true
})
delete(cache.gasPerVoteCache, string(voterKey)) delete(cache.gasPerVoteCache, string(voterKey))
return true return true
@ -661,7 +640,7 @@ func makeVoterKey(pub []byte, prealloc ...[]byte) []byte {
if len(prealloc) != 0 { if len(prealloc) != 0 {
key = prealloc[0] key = prealloc[0]
} else { } else {
key = make([]byte, 34, 38) key = make([]byte, 34)
} }
key[0] = prefixVoterRewardPerCommittee key[0] = prefixVoterRewardPerCommittee
copy(key[1:], pub) 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 // CalculateBonus calculates amount of gas generated for holding value NEO from start to end block
// and having voted for active committee member. // 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) key := makeAccountKey(acc)
si := d.GetStorageItem(n.ID, key) si := ic.DAO.GetStorageItem(n.ID, key)
if si == nil { if si == nil {
return nil, storage.ErrKeyNotFound return nil, storage.ErrKeyNotFound
} }
@ -680,19 +662,19 @@ func (n *NEO) CalculateBonus(d *dao.Simple, acc util.Uint160, end uint32) (*big.
if err != nil { if err != nil {
return nil, err 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) { func (n *NEO) calculateBonus(d *dao.Simple, acc *state.NEOBalance, end uint32) (*big.Int, error) {
r, err := n.CalculateNEOHolderReward(d, value, start, end) r, err := n.CalculateNEOHolderReward(d, &acc.Balance, acc.BalanceHeight, end)
if err != nil || vote == nil { if err != nil || acc.VoteTo == nil {
return r, err return r, err
} }
var key = makeVoterKey(vote.Bytes()) var key = makeVoterKey(acc.VoteTo.Bytes())
var reward = n.getGASPerVote(d, key, []uint32{start, end}) var reward = n.getLatestGASPerVote(d, key)
var tmp = (&reward[1]).Sub(&reward[1], &reward[0]) var tmp = big.NewInt(0).Sub(&reward, &acc.LastGasPerVote)
tmp.Mul(tmp, value) tmp.Mul(tmp, &acc.Balance)
tmp.Div(tmp, bigVoterRewardFactor) tmp.Div(tmp, bigVoterRewardFactor)
tmp.Add(tmp, r) tmp.Add(tmp, r)
return tmp, nil 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 { if err := n.ModifyAccountVotes(acc, ic.DAO, new(big.Int).Neg(&acc.Balance), false); err != nil {
return err return err
} }
if pub != nil && pub != acc.VoteTo {
acc.LastGasPerVote = n.getLatestGASPerVote(ic.DAO, makeVoterKey(pub.Bytes()))
}
oldVote := acc.VoteTo oldVote := acc.VoteTo
acc.VoteTo = pub acc.VoteTo = pub
if err := n.ModifyAccountVotes(acc, ic.DAO, &acc.Balance, true); err != nil { if err := n.ModifyAccountVotes(acc, ic.DAO, &acc.Balance, true); err != nil {

View file

@ -3,13 +3,16 @@ package native_test
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"math" "math"
"math/big" "math/big"
"sort" "sort"
"strings"
"testing" "testing"
"github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/internal/contracts"
"github.com/nspcc-dev/neo-go/internal/random" "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/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
@ -305,6 +308,15 @@ func TestNEO_GetAccountState(t *testing.T) {
neoValidatorInvoker := newNeoValidatorsClient(t) neoValidatorInvoker := newNeoValidatorsClient(t)
e := neoValidatorInvoker.Executor 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) { t.Run("empty", func(t *testing.T) {
neoValidatorInvoker.Invoke(t, stackitem.Null{}, "getAccountState", util.Uint160{}) neoValidatorInvoker.Invoke(t, stackitem.Null{}, "getAccountState", util.Uint160{})
}) })
@ -318,8 +330,100 @@ func TestNEO_GetAccountState(t *testing.T) {
stackitem.Make(amount), stackitem.Make(amount),
stackitem.Make(lub), stackitem.Make(lub),
stackitem.Null{}, stackitem.Null{},
stackitem.Make(0),
}), "getAccountState", acc.ScriptHash()) }), "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) { func TestNEO_CommitteeBountyOnPersist(t *testing.T) {

View file

@ -19,8 +19,9 @@ type NEP17Balance struct {
// NEOBalance represents the balance state of a NEO-token. // NEOBalance represents the balance state of a NEO-token.
type NEOBalance struct { type NEOBalance struct {
NEP17Balance NEP17Balance
BalanceHeight uint32 BalanceHeight uint32
VoteTo *keys.PublicKey VoteTo *keys.PublicKey
LastGasPerVote big.Int
} }
// NEP17BalanceFromBytes converts the serialized NEP17Balance to a structure. // 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(&s.Balance),
stackitem.NewBigInteger(big.NewInt(int64(s.BalanceHeight))), stackitem.NewBigInteger(big.NewInt(int64(s.BalanceHeight))),
voteItem, voteItem,
stackitem.NewBigInteger(&s.LastGasPerVote),
}), nil }), nil
} }
@ -157,5 +159,12 @@ func (s *NEOBalance) FromStackItem(item stackitem.Item) error {
return fmt.Errorf("invalid public key bytes: %w", err) return fmt.Errorf("invalid public key bytes: %w", err)
} }
s.VoteTo = pub 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 return nil
} }

View file

@ -15,9 +15,10 @@ import (
// AccountState contains info about a NEO holder. // AccountState contains info about a NEO holder.
type AccountState struct { type AccountState struct {
Balance int Balance int
Height int Height int
VoteTo interop.PublicKey VoteTo interop.PublicKey
LastGasPerVote int
} }
// Hash represents NEO contract hash. // Hash represents NEO contract hash.

View file

@ -372,9 +372,9 @@ func TestClientNEOContract(t *testing.T) {
require.Equal(t, int64(1000_0000_0000), regP) require.Equal(t, int64(1000_0000_0000), regP)
acc0 := testchain.PrivateKey(0).PublicKey().GetScriptHash() 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.NoError(t, err)
require.Equal(t, big.NewInt(48000), uncl) require.Equal(t, big.NewInt(10000), uncl)
accState, err := neoR.GetAccountState(acc0) accState, err := neoR.GetAccountState(acc0)
require.NoError(t, err) require.NoError(t, err)

View file

@ -83,7 +83,7 @@ const (
faultedTxHashLE = "82279bfe9bada282ca0f8cb8e0bb124b921af36f00c69a518320322c6f4fef60" faultedTxHashLE = "82279bfe9bada282ca0f8cb8e0bb124b921af36f00c69a518320322c6f4fef60"
faultedTxBlock uint32 = 23 faultedTxBlock uint32 = 23
invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA"
block20StateRootLE = "33b4cee6a59b9dc9d186fc235dc81e2ffe74418d7d777d538422a62b8e635ef2" block20StateRootLE = "a2841baec40c6b752ba959c2b2cfee20b6beeabb85460224929bc9ff358bf8d2"
) )
var ( var (
@ -943,11 +943,11 @@ var rpcTestCases = map[string][]rpcTestCase{
}, { }, {
State: "Added", 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}, 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", 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}, 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", 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}, 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},