diff --git a/dapps/faucet/packages/faucet.go b/dapps/faucet/packages/faucet.go index dd40e51fffa73802939bc5881ffed6fd8089dc6b..d4e5788eb386969a6251c05a3272b32b7a275586 100644 --- a/dapps/faucet/packages/faucet.go +++ b/dapps/faucet/packages/faucet.go @@ -1,7 +1,6 @@ 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) { diff --git a/dapps/valuetransfers/dapp.go b/dapps/valuetransfers/dapp.go index e16014d0ac8a4aebb036418385108c67ea88275d..60adff59bb18cc52bec8690144c53a0dd2654bb8 100644 --- a/dapps/valuetransfers/dapp.go +++ b/dapps/valuetransfers/dapp.go @@ -1,6 +1,7 @@ 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 + } +} diff --git a/dapps/valuetransfers/packages/tangle/errors.go b/dapps/valuetransfers/packages/tangle/errors.go index 3ada3b8ae0f57de974ed9bcc4a11012a6db3b5c4..935661ea0a2d659f9d37c6edad5ed3260af28983 100644 --- a/dapps/valuetransfers/packages/tangle/errors.go +++ b/dapps/valuetransfers/packages/tangle/errors.go @@ -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") ) diff --git a/dapps/valuetransfers/packages/tangle/factory.go b/dapps/valuetransfers/packages/tangle/factory.go index 6720e5d2f9c4361a3a4947706db17e848f16ffd1..106ee55a5b4ff1108e84e18d896dac6f2321e96b 100644 --- a/dapps/valuetransfers/packages/tangle/factory.go +++ b/dapps/valuetransfers/packages/tangle/factory.go @@ -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. diff --git a/dapps/valuetransfers/packages/transaction/inputs.go b/dapps/valuetransfers/packages/transaction/inputs.go index bfc8ea13980fa6ead451063438495904ab2e51b9..f5071c62cebbed30e2410292a82166732d098a73 100644 --- a/dapps/valuetransfers/packages/transaction/inputs.go +++ b/dapps/valuetransfers/packages/transaction/inputs.go @@ -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)) diff --git a/plugins/webapi/value/sendtransaction/handler.go b/plugins/webapi/value/sendtransaction/handler.go index dda59ab8c3d80fbe5c3b69a08dd82254acc81b6c..56bc66b2a8c549ae5800a501485e640d7153f640 100644 --- a/plugins/webapi/value/sendtransaction/handler.go +++ b/plugins/webapi/value/sendtransaction/handler.go @@ -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()}) } diff --git a/plugins/webapi/value/testsendtxn/handler.go b/plugins/webapi/value/testsendtxn/handler.go index 222e919ede5d6b2390bd0540475b5f0cde0be4f4..15b7a7673bb58b16fe452f9d22412e827006f531 100644 --- a/plugins/webapi/value/testsendtxn/handler.go +++ b/plugins/webapi/value/testsendtxn/handler.go @@ -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()}) }