diff --git a/dapps/valuetransfers/packages/balance/balance.go b/dapps/valuetransfers/packages/balance/balance.go index 142b95f595219b08f45b2171dc2932d4076caf57..6559145b2b172898e42831a9b31e9bda99520804 100644 --- a/dapps/valuetransfers/packages/balance/balance.go +++ b/dapps/valuetransfers/packages/balance/balance.go @@ -8,15 +8,17 @@ import ( // Balance represents a balance in the IOTA ledger. It consists out of a numeric value and a color. type Balance struct { - value int64 - color Color + // The numeric value of the balance. + Value int64 `json:"value"` + // The color of the balance. + Color Color `json:"color"` } // New creates a new Balance with the given details. func New(color Color, balance int64) (result *Balance) { result = &Balance{ - color: color, - value: balance, + Color: color, + Value: balance, } return @@ -28,7 +30,7 @@ func FromBytes(bytes []byte) (result *Balance, consumedBytes int, err error) { marshalUtil := marshalutil.New(bytes) - result.value, err = marshalUtil.ReadInt64() + result.Value, err = marshalUtil.ReadInt64() if err != nil { return } @@ -40,7 +42,7 @@ func FromBytes(bytes []byte) (result *Balance, consumedBytes int, err error) { return nil, marshalUtil.ReadOffset(), colorErr } - result.color = coinColor.(Color) + result.Color = coinColor.(Color) consumedBytes = marshalUtil.ReadOffset() return @@ -56,29 +58,19 @@ func Parse(marshalUtil *marshalutil.MarshalUtil) (*Balance, error) { return address.(*Balance), nil } -// Value returns the numeric value of the balance. -func (balance *Balance) Value() int64 { - return balance.value -} - -// Color returns the Color of the balance. -func (balance *Balance) Color() Color { - return balance.color -} - // Bytes marshals the Balance into a sequence of bytes. func (balance *Balance) Bytes() []byte { marshalUtil := marshalutil.New(Length) - marshalUtil.WriteInt64(balance.value) - marshalUtil.WriteBytes(balance.color.Bytes()) + marshalUtil.WriteInt64(balance.Value) + marshalUtil.WriteBytes(balance.Color.Bytes()) return marshalUtil.Bytes() } // String creates a human readable string of the Balance. func (balance *Balance) String() string { - return strconv.FormatInt(balance.value, 10) + " " + balance.color.String() + return strconv.FormatInt(balance.Value, 10) + " " + balance.Color.String() } // Length encodes the length of a marshaled Balance (the length of the color + 8 bytes for the balance). diff --git a/dapps/valuetransfers/packages/balance/balance_test.go b/dapps/valuetransfers/packages/balance/balance_test.go index 5dd98567cf634ec3ad3bd8ede4a42ed6d0ab4932..35401c66a5eb5bed4ace5281f4ff1de1e289feb7 100644 --- a/dapps/valuetransfers/packages/balance/balance_test.go +++ b/dapps/valuetransfers/packages/balance/balance_test.go @@ -8,8 +8,8 @@ import ( func TestMarshalUnmarshal(t *testing.T) { balance := New(ColorIOTA, 1337) - assert.Equal(t, int64(1337), balance.Value()) - assert.Equal(t, ColorIOTA, balance.Color()) + assert.Equal(t, int64(1337), balance.Value) + assert.Equal(t, ColorIOTA, balance.Color) marshaledBalance := balance.Bytes() assert.Equal(t, Length, len(marshaledBalance)) @@ -19,6 +19,6 @@ func TestMarshalUnmarshal(t *testing.T) { panic(err) } assert.Equal(t, Length, consumedBytes) - assert.Equal(t, balance.value, restoredBalance.Value()) - assert.Equal(t, balance.color, restoredBalance.Color()) + assert.Equal(t, balance.Value, restoredBalance.Value) + assert.Equal(t, balance.Color, restoredBalance.Color) } diff --git a/dapps/valuetransfers/packages/tangle/ledgerstate.go b/dapps/valuetransfers/packages/tangle/ledgerstate.go index 5c0d8c31a7b7bd0c93dcb01aaa29c35719806c22..9b676acfedfebbf7a417bfc8d63c0bf24bdc8991 100644 --- a/dapps/valuetransfers/packages/tangle/ledgerstate.go +++ b/dapps/valuetransfers/packages/tangle/ledgerstate.go @@ -25,7 +25,7 @@ func (ledgerState *LedgerState) Balances(address address.Address) (coloredBalanc ledgerState.tangle.OutputsOnAddress(address).Consume(func(output *Output) { if output.ConsumerCount() == 0 { for _, coloredBalance := range output.Balances() { - coloredBalances[coloredBalance.Color()] += coloredBalance.Value() + coloredBalances[coloredBalance.Color] += coloredBalance.Value } } }) diff --git a/dapps/valuetransfers/packages/tangle/snapshot.go b/dapps/valuetransfers/packages/tangle/snapshot.go new file mode 100644 index 0000000000000000000000000000000000000000..b13df4d81409167068c882c8c893767c030cfcaa --- /dev/null +++ b/dapps/valuetransfers/packages/tangle/snapshot.go @@ -0,0 +1,125 @@ +package tangle + +import ( + "encoding/binary" + "fmt" + "io" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" +) + +// Snapshot defines a snapshot of the ledger state. +type Snapshot map[transaction.ID]map[address.Address][]*balance.Balance + +// WriteTo writes the snapshot data to the given writer in the following format: +// transaction_count(int64) +// -> transaction_count * transaction_id(32byte) +// ->address_count(int64) +// ->address_count * address(33byte) +// ->balance_count(int64) +// ->balance_count * value(int64)+color(32byte) +func (s Snapshot) WriteTo(writer io.Writer) (int64, error) { + var bytesWritten int64 + transactionCount := len(s) + if err := binary.Write(writer, binary.LittleEndian, int64(transactionCount)); err != nil { + return 0, fmt.Errorf("unable to write transactions count: %w", err) + } + bytesWritten += 8 + for txID, addresses := range s { + if err := binary.Write(writer, binary.LittleEndian, txID); err != nil { + return bytesWritten, fmt.Errorf("unable to write transaction ID: %w", err) + } + bytesWritten += transaction.IDLength + if err := binary.Write(writer, binary.LittleEndian, int64(len(addresses))); err != nil { + return bytesWritten, fmt.Errorf("unable to write address count: %w", err) + } + bytesWritten += 8 + for addr, balances := range addresses { + if err := binary.Write(writer, binary.LittleEndian, addr); err != nil { + return bytesWritten, fmt.Errorf("unable to write address: %w", err) + } + bytesWritten += address.Length + if err := binary.Write(writer, binary.LittleEndian, int64(len(balances))); err != nil { + return bytesWritten, fmt.Errorf("unable to write balance count: %w", err) + } + bytesWritten += 8 + for _, bal := range balances { + if err := binary.Write(writer, binary.LittleEndian, bal.Value); err != nil { + return bytesWritten, fmt.Errorf("unable to write balance value: %w", err) + } + bytesWritten += 8 + if err := binary.Write(writer, binary.LittleEndian, bal.Color); err != nil { + return bytesWritten, fmt.Errorf("unable to write balance color: %w", err) + } + bytesWritten += balance.ColorLength + } + } + } + + return bytesWritten, nil +} + +// ReadFrom reads the snapshot bytes from the given reader. +// This function overrides existing content of the snapshot. +func (s Snapshot) ReadFrom(reader io.Reader) (int64, error) { + var bytesRead int64 + var transactionCount int64 + if err := binary.Read(reader, binary.LittleEndian, &transactionCount); err != nil { + return 0, fmt.Errorf("unable to read transaction count: %w", err) + } + bytesRead += 8 + + var i int64 + for ; i < transactionCount; i++ { + txIDBytes := make([]byte, transaction.IDLength) + if err := binary.Read(reader, binary.LittleEndian, txIDBytes); err != nil { + return bytesRead, fmt.Errorf("unable to read transaction ID: %w", err) + } + bytesRead += transaction.IDLength + var addrCount int64 + if err := binary.Read(reader, binary.LittleEndian, &addrCount); err != nil { + return bytesRead, fmt.Errorf("unable to read address count: %w", err) + } + bytesRead += 8 + txAddrMap := make(map[address.Address][]*balance.Balance, addrCount) + var j int64 + for ; j < addrCount; j++ { + addrBytes := make([]byte, address.Length) + if err := binary.Read(reader, binary.LittleEndian, addrBytes); err != nil { + return bytesRead, fmt.Errorf("unable to read address: %w", err) + } + bytesRead += address.Length + var balanceCount int64 + if err := binary.Read(reader, binary.LittleEndian, &balanceCount); err != nil { + return bytesRead, fmt.Errorf("unable to read balance count: %w", err) + } + bytesRead += 8 + + balances := make([]*balance.Balance, balanceCount) + var k int64 + for ; k < balanceCount; k++ { + var value int64 + if err := binary.Read(reader, binary.LittleEndian, &value); err != nil { + return bytesRead, fmt.Errorf("unable to read balance value: %w", err) + } + bytesRead += 8 + color := balance.Color{} + if err := binary.Read(reader, binary.LittleEndian, &color); err != nil { + return bytesRead, fmt.Errorf("unable to read balance color: %w", err) + } + bytesRead += balance.ColorLength + balances[k] = &balance.Balance{Value: value, Color: color} + } + addr := address.Address{} + copy(addr[:], addrBytes) + txAddrMap[addr] = balances + } + txID := transaction.ID{} + copy(txID[:], txIDBytes) + s[txID] = txAddrMap + } + + return bytesRead, nil +} diff --git a/dapps/valuetransfers/packages/tangle/tangle.go b/dapps/valuetransfers/packages/tangle/tangle.go index 3ff44e6d32a81779fdc21cf04eba0a987b42c392..21931cc9098b0821d52acd4c1a680e5e4285bcae 100644 --- a/dapps/valuetransfers/packages/tangle/tangle.go +++ b/dapps/valuetransfers/packages/tangle/tangle.go @@ -165,9 +165,6 @@ func (tangle *Tangle) BranchManager() *branchmanager.BranchManager { return tangle.branchManager } -// Snapshot defines a snapshot of the ledger state. -type Snapshot map[transaction.ID]map[address.Address][]*balance.Balance - // LoadSnapshot creates a set of outputs in the value tangle, that are forming the genesis for future transactions. func (tangle *Tangle) LoadSnapshot(snapshot Snapshot) { for transactionID, addressBalances := range snapshot { @@ -1520,9 +1517,9 @@ func (tangle *Tangle) retrieveConsumedInputDetails(tx *transaction.Transaction) // calculate the input balances for _, inputBalance := range input.Balances() { var newBalance int64 - if currentBalance, balanceExists := consumedBalances[inputBalance.Color()]; balanceExists { + if currentBalance, balanceExists := consumedBalances[inputBalance.Color]; balanceExists { // check overflows in the numbers - if inputBalance.Value() > math.MaxInt64-currentBalance { + if inputBalance.Value > math.MaxInt64-currentBalance { // TODO: make it an explicit error var err = fmt.Errorf("buffer overflow in balances of inputs") @@ -1531,11 +1528,11 @@ func (tangle *Tangle) retrieveConsumedInputDetails(tx *transaction.Transaction) return } - newBalance = currentBalance + inputBalance.Value() + newBalance = currentBalance + inputBalance.Value } else { - newBalance = inputBalance.Value() + newBalance = inputBalance.Value } - consumedBalances[inputBalance.Color()] = newBalance + consumedBalances[inputBalance.Color] = newBalance } } inputsSolid = true @@ -1556,51 +1553,51 @@ func (tangle *Tangle) checkTransactionOutputs(inputBalances map[balance.Color]in aborted := !outputs.ForEach(func(address address.Address, balances []*balance.Balance) bool { for _, outputBalance := range balances { // abort if the output creates a negative or empty output - if outputBalance.Value() <= 0 { + if outputBalance.Value <= 0 { return false } // sidestep logic if we have a newly colored output (we check the supply later) - if outputBalance.Color() == balance.ColorNew { + if outputBalance.Color == balance.ColorNew { // catch overflows - if newlyColoredCoins > math.MaxInt64-outputBalance.Value() { + if newlyColoredCoins > math.MaxInt64-outputBalance.Value { return false } - newlyColoredCoins += outputBalance.Value() + newlyColoredCoins += outputBalance.Value continue } // sidestep logic if we have ColorIOTA - if outputBalance.Color() == balance.ColorIOTA { + if outputBalance.Color == balance.ColorIOTA { // catch overflows - if uncoloredCoins > math.MaxInt64-outputBalance.Value() { + if uncoloredCoins > math.MaxInt64-outputBalance.Value { return false } - uncoloredCoins += outputBalance.Value() + uncoloredCoins += outputBalance.Value continue } // check if the used color does not exist in our supply - availableBalance, spentColorExists := inputBalances[outputBalance.Color()] + availableBalance, spentColorExists := inputBalances[outputBalance.Color] if !spentColorExists { return false } // abort if we spend more coins of the given color than we have - if availableBalance < outputBalance.Value() { + if availableBalance < outputBalance.Value { return false } // subtract the spent coins from the supply of this color - inputBalances[outputBalance.Color()] -= outputBalance.Value() + inputBalances[outputBalance.Color] -= outputBalance.Value // cleanup empty map entries (we have exhausted our funds) - if inputBalances[outputBalance.Color()] == 0 { - delete(inputBalances, outputBalance.Color()) + if inputBalances[outputBalance.Color] == 0 { + delete(inputBalances, outputBalance.Color) } } diff --git a/dapps/valuetransfers/packages/tangle/tangle_snapshot_test.go b/dapps/valuetransfers/packages/tangle/tangle_snapshot_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4011f34d9ecf46c1e0e4bcdb055619726cc5efea --- /dev/null +++ b/dapps/valuetransfers/packages/tangle/tangle_snapshot_test.go @@ -0,0 +1,88 @@ +package tangle + +import ( + "bytes" + "testing" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" + "github.com/iotaledger/hive.go/kvstore/mapdb" + "github.com/stretchr/testify/assert" +) + +func TestLoadSnapshot(t *testing.T) { + tangle := New(mapdb.NewMapDB()) + + snapshot := map[transaction.ID]map[address.Address][]*balance.Balance{ + transaction.GenesisID: { + address.Random(): []*balance.Balance{ + balance.New(balance.ColorIOTA, 337), + }, + + address.Random(): []*balance.Balance{ + balance.New(balance.ColorIOTA, 1000), + balance.New(balance.ColorIOTA, 1000), + }, + }, + } + tangle.LoadSnapshot(snapshot) + + // check whether outputs can be retrieved from tangle + for addr, balances := range snapshot[transaction.GenesisID] { + cachedOutput := tangle.TransactionOutput(transaction.NewOutputID(addr, transaction.GenesisID)) + cachedOutput.Consume(func(output *Output) { + assert.Equal(t, addr, output.Address()) + assert.ElementsMatch(t, balances, output.Balances()) + assert.True(t, output.Solid()) + assert.Equal(t, branchmanager.MasterBranchID, output.BranchID()) + }) + } +} + +func TestSnapshotMarshalUnmarshal(t *testing.T) { + const genesisBalance = 3333 + seed := wallet.NewSeed() + genesisAddr := seed.Address(GENESIS) + + snapshot := Snapshot{ + transaction.GenesisID: { + genesisAddr: { + balance.New(balance.ColorIOTA, genesisBalance), + }, + }, + } + + // includes txs count + const int64ByteSize = 8 + expectedLength := int64ByteSize + for _, addresses := range snapshot { + // tx id + expectedLength += transaction.IDLength + // addr count + expectedLength += int64ByteSize + for _, balances := range addresses { + // addr + expectedLength += address.Length + // balance count + expectedLength += int64ByteSize + // balances + expectedLength += len(balances) * (int64ByteSize + balance.ColorLength) + } + } + + var buf bytes.Buffer + written, err := snapshot.WriteTo(&buf) + assert.NoError(t, err, "writing the snapshot to the buffer should succeed") + assert.EqualValues(t, expectedLength, written, "written byte count should match the expected count") + + snapshotFromBytes := Snapshot{} + read, err := snapshotFromBytes.ReadFrom(&buf) + assert.NoError(t, err, "expected no error from reading valid snapshot bytes") + assert.EqualValues(t, expectedLength, read, "read byte count should match the expected count") + + // check that the source and unmarshaled snapshot are equivalent + assert.Equal(t, snapshot, snapshotFromBytes) +} diff --git a/dapps/valuetransfers/packages/tangle/tangle_test.go b/dapps/valuetransfers/packages/tangle/tangle_test.go index b7dc5a90361ec2f8669acba1483ecfc7d2a6a271..85309df19052b3274f3ec86911a9ccdc56109009 100644 --- a/dapps/valuetransfers/packages/tangle/tangle_test.go +++ b/dapps/valuetransfers/packages/tangle/tangle_test.go @@ -874,35 +874,6 @@ func TestGetCachedOutputsFromTransactionInputs(t *testing.T) { } } -func TestLoadSnapshot(t *testing.T) { - tangle := New(mapdb.NewMapDB()) - - snapshot := map[transaction.ID]map[address.Address][]*balance.Balance{ - transaction.GenesisID: { - address.Random(): []*balance.Balance{ - balance.New(balance.ColorIOTA, 337), - }, - - address.Random(): []*balance.Balance{ - balance.New(balance.ColorIOTA, 1000), - balance.New(balance.ColorIOTA, 1000), - }, - }, - } - tangle.LoadSnapshot(snapshot) - - // check whether outputs can be retrieved from tangle - for addr, balances := range snapshot[transaction.GenesisID] { - cachedOutput := tangle.TransactionOutput(transaction.NewOutputID(addr, transaction.GenesisID)) - cachedOutput.Consume(func(output *Output) { - assert.Equal(t, addr, output.Address()) - assert.ElementsMatch(t, balances, output.Balances()) - assert.True(t, output.Solid()) - assert.Equal(t, branchmanager.MasterBranchID, output.BranchID()) - }) - } -} - func TestRetrieveConsumedInputDetails(t *testing.T) { // test simple happy case { @@ -1549,7 +1520,7 @@ func sumOutputsByColor(outputs map[address.Address][]*balance.Balance) map[balan for _, balances := range outputs { for _, bal := range balances { - totals[bal.Color()] += bal.Value() + totals[bal.Color] += bal.Value } } diff --git a/plugins/dashboard/payload_handler.go b/plugins/dashboard/payload_handler.go index db8ab642269e0f7db2f9161de2a62dbf4d2bb23b..6ffa3593a9927bdeb97c61f3de2d74d05c3d76b4 100644 --- a/plugins/dashboard/payload_handler.go +++ b/plugins/dashboard/payload_handler.go @@ -135,8 +135,8 @@ func processValuePayload(p payload.Payload) (vp ValuePayload) { var b []Balance for _, balance := range balances { b = append(b, Balance{ - Value: balance.Value(), - Color: balance.Color().String(), + Value: balance.Value, + Color: balance.Color.String(), }) } t := OutputContent{ diff --git a/plugins/webapi/value/unspentoutputs/handler.go b/plugins/webapi/value/unspentoutputs/handler.go index e22c9b0a076be495e2816be8b2c268201e823dd8..acb5f5b83ff7f39b2cc176b8d76d792834bfde31 100644 --- a/plugins/webapi/value/unspentoutputs/handler.go +++ b/plugins/webapi/value/unspentoutputs/handler.go @@ -38,8 +38,8 @@ func Handler(c echo.Context) error { var b []utils.Balance for _, balance := range output.Balances() { b = append(b, utils.Balance{ - Value: balance.Value(), - Color: balance.Color().String(), + Value: balance.Value, + Color: balance.Color.String(), }) } diff --git a/plugins/webapi/value/utils/transaction_handler.go b/plugins/webapi/value/utils/transaction_handler.go index 308904d55c3e577c0d6f5cdd08b0d2fecc0590f2..15a587ead8c24e2233a4e6b78249f56146c94fb8 100644 --- a/plugins/webapi/value/utils/transaction_handler.go +++ b/plugins/webapi/value/utils/transaction_handler.go @@ -21,8 +21,8 @@ func ParseTransaction(t *transaction.Transaction) (txn Transaction) { var b []Balance for _, balance := range balances { b = append(b, Balance{ - Value: balance.Value(), - Color: balance.Color().String(), + Value: balance.Value, + Color: balance.Color.String(), }) } t := Output{