Skip to content
Snippets Groups Projects
Unverified Commit 16bd8d85 authored by Hans Moog's avatar Hans Moog Committed by GitHub
Browse files

Double spend check (#606)


* Fix: added a check for doublespends

* Fix: added a check for double spends

* Fix: fixed iport cycle

* Feat: replaced error with error variable

* Feat: store tangle in factory

* Update dapps/faucet/packages/faucet.go

Co-authored-by: default avatarWolfgang Welz <welzwo@gmail.com>

* makes send tx web API handler synced

Co-authored-by: default avatarHans Moog <hm@mkjc.net>
Co-authored-by: default avatarWolfgang Welz <welzwo@gmail.com>
Co-authored-by: default avatarLuca Moser <moser.luca@gmail.com>
parent fa88a081
Branches
Tags
No related merge requests found
package faucet
import (
"errors"
"fmt"
"sync"
"time"
......@@ -16,13 +15,6 @@ import (
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet"
"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
"github.com/iotaledger/goshimmer/plugins/issuer"
"github.com/iotaledger/hive.go/events"
)
var (
// ErrFundingTxNotBookedInTime is returned when a funding transaction didn't get booked
// by this node in the maximum defined await time for it to get booked.
ErrFundingTxNotBookedInTime = errors.New("funding transaction didn't get booked in time")
)
// New creates a new faucet using the given seed and tokensPerRequest config.
......@@ -80,7 +72,10 @@ func (f *Faucet) SendFunds(msg *message.Message) (m *message.Message, txID strin
}
// prepare value payload with value factory
payload := valuetransfers.ValueObjectFactory().IssueTransaction(tx)
payload, err := valuetransfers.ValueObjectFactory().IssueTransaction(tx)
if err != nil {
return nil, "", fmt.Errorf("failed to issue transaction: %w", err)
}
// attach to message layer
msg, err = issuer.IssuePayload(payload)
......@@ -91,42 +86,13 @@ func (f *Faucet) SendFunds(msg *message.Message) (m *message.Message, txID strin
// block for a certain amount of time until we know that the transaction
// actually got booked by this node itself
// TODO: replace with an actual more reactive way
bookedInTime := f.awaitTransactionBooked(tx.ID(), f.maxTxBookedAwaitTime)
if !bookedInTime {
return nil, "", fmt.Errorf("%w: tx %s", ErrFundingTxNotBookedInTime, tx.ID().String())
if err := valuetransfers.AwaitTransactionToBeBooked(tx.ID(), f.maxTxBookedAwaitTime); err != nil {
return nil, "", fmt.Errorf("%w: tx %s", err, tx.ID().String())
}
return msg, tx.ID().String(), nil
}
// awaitTransactionBooked awaits maxAwait for the given transaction to get booked.
func (f *Faucet) awaitTransactionBooked(txID transaction.ID, maxAwait time.Duration) bool {
booked := make(chan struct{}, 1)
// exit is used to let the caller exit if for whatever
// reason the same transaction gets booked multiple times
exit := make(chan struct{})
defer close(exit)
closure := events.NewClosure(func(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, decisionPending bool) {
defer cachedTransaction.Release()
defer cachedTransactionMetadata.Release()
if cachedTransaction.Unwrap().ID() != txID {
return
}
select {
case booked <- struct{}{}:
case <-exit:
}
})
valuetransfers.Tangle().Events.TransactionBooked.Attach(closure)
defer valuetransfers.Tangle().Events.TransactionBooked.Detach(closure)
select {
case <-time.After(maxAwait):
return false
case <-booked:
return true
}
}
// collectUTXOsForFunding iterates over the faucet's UTXOs until the token threshold is reached.
// this function also returns the remainder balance for the given outputs.
func (f *Faucet) collectUTXOsForFunding() (outputIds []transaction.OutputID, addrsIndices map[uint64]struct{}, remainder int64) {
......
package valuetransfers
import (
"errors"
"os"
"sync"
"time"
......@@ -10,6 +11,7 @@ import (
valuepayload "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tipmanager"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
messageTangle "github.com/iotaledger/goshimmer/packages/binary/messagelayer/tangle"
"github.com/iotaledger/goshimmer/packages/shutdown"
......@@ -44,6 +46,10 @@ func init() {
}
var (
// ErrTransactionWasNotBookedInTime is returned if a transaction did not get booked
// within the defined await time.
ErrTransactionWasNotBookedInTime = errors.New("transaction could not be booked in time")
// app is the "plugin" instance of the value-transfers application.
app *node.Plugin
appOnce sync.Once
......@@ -217,7 +223,35 @@ func TipManager() *tipmanager.TipManager {
// ValueObjectFactory returns the ValueObjectFactory singleton.
func ValueObjectFactory() *tangle.ValueObjectFactory {
valueObjectFactoryOnce.Do(func() {
valueObjectFactory = tangle.NewValueObjectFactory(TipManager())
valueObjectFactory = tangle.NewValueObjectFactory(Tangle(), TipManager())
})
return valueObjectFactory
}
// AwaitTransactionToBeBooked awaits maxAwait for the given transaction to get booked.
func AwaitTransactionToBeBooked(txID transaction.ID, maxAwait time.Duration) error {
booked := make(chan struct{}, 1)
// exit is used to let the caller exit if for whatever
// reason the same transaction gets booked multiple times
exit := make(chan struct{})
defer close(exit)
closure := events.NewClosure(func(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, decisionPending bool) {
defer cachedTransaction.Release()
defer cachedTransactionMetadata.Release()
if cachedTransaction.Unwrap().ID() != txID {
return
}
select {
case booked <- struct{}{}:
case <-exit:
}
})
Tangle().Events.TransactionBooked.Attach(closure)
defer Tangle().Events.TransactionBooked.Detach(closure)
select {
case <-time.After(maxAwait):
return ErrTransactionWasNotBookedInTime
case <-booked:
return nil
}
}
......@@ -11,4 +11,7 @@ var (
// ErrPayloadInvalid represents an error type that is triggered when an invalid payload is detected.
ErrPayloadInvalid = errors.New("payload invalid")
// ErrDoubleSpendForbidden represents an error that is triggered when a user tries to issue a double spend.
ErrDoubleSpendForbidden = errors.New("it is not allowed to issue a double spend")
)
......@@ -9,13 +9,15 @@ import (
// ValueObjectFactory acts as a factory to create new value objects.
type ValueObjectFactory struct {
tangle *Tangle
tipManager *tipmanager.TipManager
Events *ValueObjectFactoryEvents
}
// NewValueObjectFactory creates a new ValueObjectFactory.
func NewValueObjectFactory(tipManager *tipmanager.TipManager) *ValueObjectFactory {
func NewValueObjectFactory(tangle *Tangle, tipManager *tipmanager.TipManager) *ValueObjectFactory {
return &ValueObjectFactory{
tangle: tangle,
tipManager: tipManager,
Events: &ValueObjectFactoryEvents{
ValueObjectConstructed: events.NewEvent(valueObjectConstructedEvent),
......@@ -25,13 +27,27 @@ func NewValueObjectFactory(tipManager *tipmanager.TipManager) *ValueObjectFactor
// IssueTransaction creates a new value object including tip selection and returns it.
// It also triggers the ValueObjectConstructed event once it's done.
func (v *ValueObjectFactory) IssueTransaction(tx *transaction.Transaction) *payload.Payload {
func (v *ValueObjectFactory) IssueTransaction(tx *transaction.Transaction) (valueObject *payload.Payload, err error) {
parent1, parent2 := v.tipManager.Tips()
valueObject := payload.New(parent1, parent2, tx)
// check if the tx that is supposed to be issued is a double spend
tx.Inputs().ForEach(func(outputId transaction.OutputID) bool {
v.tangle.TransactionOutput(outputId).Consume(func(output *Output) {
if output.ConsumerCount() >= 1 {
err = ErrDoubleSpendForbidden
}
})
return err == nil
})
if err != nil {
return
}
valueObject = payload.New(parent1, parent2, tx)
v.Events.ValueObjectConstructed.Trigger(valueObject)
return valueObject
return
}
// ValueObjectFactoryEvents represent events happening on a ValueObjectFactory.
......
......@@ -104,7 +104,7 @@ func (inputs *Inputs) Bytes() (bytes []byte) {
// ForEach iterates through the referenced Outputs and calls the consumer function for every Output. The iteration can
// be aborted by returning false in the consumer.
func (inputs *Inputs) ForEach(consumer func(outputId OutputID) bool) bool {
func (inputs *Inputs) ForEach(consumer func(outputID OutputID) bool) bool {
return inputs.OrderedMap.ForEach(func(key, value interface{}) bool {
return value.(*orderedmap.OrderedMap).ForEach(func(key, value interface{}) bool {
return consumer(value.(OutputID))
......
......@@ -2,6 +2,8 @@ package sendtransaction
import (
"net/http"
"sync"
"time"
"github.com/iotaledger/goshimmer/dapps/valuetransfers"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
......@@ -9,8 +11,16 @@ import (
"github.com/labstack/echo"
)
var (
sendTxMu sync.Mutex
maxBookedAwaitTime = 5 * time.Second
)
// Handler sends a transaction.
func Handler(c echo.Context) error {
sendTxMu.Lock()
defer sendTxMu.Unlock()
var request Request
if err := c.Bind(&request); err != nil {
return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
......@@ -28,12 +38,18 @@ func Handler(c echo.Context) error {
}
// Prepare value payload and send the message to tangle
payload := valuetransfers.ValueObjectFactory().IssueTransaction(tx)
payload, err := valuetransfers.ValueObjectFactory().IssueTransaction(tx)
if err != nil {
return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
}
_, err = issuer.IssuePayload(payload)
if err != nil {
return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
}
if err := valuetransfers.AwaitTransactionToBeBooked(tx.ID(), maxBookedAwaitTime); err != nil {
return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
}
return c.JSON(http.StatusOK, Response{TransactionID: tx.ID().String()})
}
......
......@@ -66,8 +66,11 @@ func Handler(c echo.Context) error {
tx := transaction.New(inputs, outputs)
// Prepare value payload and send the message to tangle
payload := valuetransfers.ValueObjectFactory().IssueTransaction(tx)
_, err := issuer.IssuePayload(payload)
payload, err := valuetransfers.ValueObjectFactory().IssueTransaction(tx)
if err != nil {
return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
}
_, err = issuer.IssuePayload(payload)
if err != nil {
return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment