core/state: merge spent and unspent coins state, use it to store more things

This change reduces pressure on DB by doing the following things:
 * not storing additional KV pair for SpentCoin
 * storing Output right in the UnspentCoin, thus eliminating the need to get a
   full transaction from DB

At the same time it makes UnspentCoin more fat and hot, but it should probably
worth it.

Also drop `GetUnspentCoinStateOrNew` as it shouldn't ever existed, UTXOs
can't come out of nowhere.

1.5M block import time (VerifyBlocks disabled) on AMD Ryzen 5 1600/16GB/HDD,
before:
real    302m9.895s
user    96m17.200s
sys     13m37.084s

after:
real    159m16.551s
user    69m58.279s
sys     7m34.334s

So it's almost two-fold which is a great improvement.
This commit is contained in:
Roman Khimov 2020-03-09 18:56:24 +03:00
parent e1f194ea7b
commit 23464401bc
7 changed files with 117 additions and 278 deletions

View file

@ -1,46 +0,0 @@
package state
import "github.com/nspcc-dev/neo-go/pkg/io"
// SpentCoin represents the state of a spent coin.
type SpentCoin struct {
TxHeight uint32
// A mapping between the index of the prevIndex and block height.
Items map[uint16]uint32
}
// NewSpentCoin returns a new SpentCoin object.
func NewSpentCoin(height uint32) *SpentCoin {
return &SpentCoin{
TxHeight: height,
Items: make(map[uint16]uint32),
}
}
// DecodeBinary implements Serializable interface.
func (s *SpentCoin) DecodeBinary(br *io.BinReader) {
s.TxHeight = br.ReadU32LE()
s.Items = make(map[uint16]uint32)
lenItems := br.ReadVarUint()
for i := 0; i < int(lenItems); i++ {
var (
key uint16
value uint32
)
key = br.ReadU16LE()
value = br.ReadU32LE()
s.Items[key] = value
}
}
// EncodeBinary implements Serializable interface.
func (s *SpentCoin) EncodeBinary(bw *io.BinWriter) {
bw.WriteU32LE(s.TxHeight)
bw.WriteVarUint(uint64(len(s.Items)))
for k, v := range s.Items {
bw.WriteU16LE(k)
bw.WriteU32LE(v)
}
}

View file

@ -1,28 +0,0 @@
package state
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/stretchr/testify/assert"
)
func TestEncodeDecodeSpentCoin(t *testing.T) {
spent := &SpentCoin{
TxHeight: 1001,
Items: map[uint16]uint32{
1: 3,
2: 8,
4: 100,
},
}
buf := io.NewBufBinWriter()
spent.EncodeBinary(buf.BinWriter)
assert.Nil(t, buf.Err)
spentDecode := new(SpentCoin)
r := io.NewBinReaderFromBuf(buf.Bytes())
spentDecode.DecodeBinary(r)
assert.Nil(t, r.Err)
assert.Equal(t, spent, spentDecode)
}

View file

@ -1,38 +1,60 @@
package state
import (
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
)
// UnspentCoin hold the state of a unspent coin.
type UnspentCoin struct {
States []Coin
Height uint32
States []OutputState
}
// OutputState combines transaction output (UTXO) and its state
// (spent/claimed...) along with the height of spend (if it's spent).
type OutputState struct {
transaction.Output
SpendHeight uint32
State Coin
}
// NewUnspentCoin returns a new unspent coin state with N confirmed states.
func NewUnspentCoin(n int) *UnspentCoin {
func NewUnspentCoin(height uint32, tx *transaction.Transaction) *UnspentCoin {
u := &UnspentCoin{
States: make([]Coin, n),
Height: height,
States: make([]OutputState, len(tx.Outputs)),
}
for i := 0; i < n; i++ {
u.States[i] = CoinConfirmed
for i := range tx.Outputs {
u.States[i] = OutputState{Output: tx.Outputs[i]}
}
return u
}
// EncodeBinary encodes UnspentCoin to the given BinWriter.
func (s *UnspentCoin) EncodeBinary(bw *io.BinWriter) {
bw.WriteU32LE(s.Height)
bw.WriteArray(s.States)
bw.WriteVarUint(uint64(len(s.States)))
for _, state := range s.States {
bw.WriteB(byte(state))
}
}
// DecodeBinary decodes UnspentCoin from the given BinReader.
func (s *UnspentCoin) DecodeBinary(br *io.BinReader) {
lenStates := br.ReadVarUint()
s.States = make([]Coin, lenStates)
for i := 0; i < int(lenStates); i++ {
s.States[i] = Coin(br.ReadB())
}
s.Height = br.ReadU32LE()
br.ReadArray(&s.States)
}
// EncodeBinary implements Serializable interface.
func (o *OutputState) EncodeBinary(w *io.BinWriter) {
o.Output.EncodeBinary(w)
w.WriteU32LE(o.SpendHeight)
w.WriteB(byte(o.State))
}
// DecodeBinary implements Serializable interface.
func (o *OutputState) DecodeBinary(r *io.BinReader) {
o.Output.DecodeBinary(r)
o.SpendHeight = r.ReadU32LE()
o.State = Coin(r.ReadB())
}

View file

@ -3,18 +3,44 @@ package state
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/internal/random"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/assert"
)
func TestDecodeEncodeUnspentCoin(t *testing.T) {
unspent := &UnspentCoin{
States: []Coin{
CoinConfirmed,
CoinSpent,
CoinSpent,
CoinSpent,
CoinConfirmed,
Height: 100500,
States: []OutputState{
{
Output: transaction.Output{
AssetID: random.Uint256(),
Amount: util.Fixed8(42),
ScriptHash: random.Uint160(),
},
SpendHeight: 201000,
State: CoinSpent,
},
{
Output: transaction.Output{
AssetID: random.Uint256(),
Amount: util.Fixed8(420),
ScriptHash: random.Uint160(),
},
SpendHeight: 0,
State: CoinConfirmed,
},
{
Output: transaction.Output{
AssetID: random.Uint256(),
Amount: util.Fixed8(4200),
ScriptHash: random.Uint160(),
},
SpendHeight: 111000,
State: CoinSpent & CoinClaimed,
},
},
}