Skip to content

Commit

Permalink
Fix identical outputs issue [#3422]
Browse files Browse the repository at this point in the history
Closes #3422. Fixes an issue where tokens can be lost if a SendTokens or IssueTokens transaction has identical outputs (same recipient, same amount).

Changelog: fix
  • Loading branch information
firelizzard18 committed Sep 30, 2023
1 parent c9da3d1 commit b230d2d
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 15 deletions.
2 changes: 2 additions & 0 deletions internal/core/execute/v2/block/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type Block struct {
State BlockState
Batch *database.Batch
Executor *Executor

syntheticCount uint64
}

func (b *Block) Params() execute.BlockParams { return b.BlockParams }
Expand Down
16 changes: 16 additions & 0 deletions internal/core/execute/v2/block/exec_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (
"gitlab.com/accumulatenetwork/accumulate/internal/database"
"gitlab.com/accumulatenetwork/accumulate/pkg/errors"
"gitlab.com/accumulatenetwork/accumulate/pkg/types/messaging"
"gitlab.com/accumulatenetwork/accumulate/pkg/url"
"gitlab.com/accumulatenetwork/accumulate/protocol"
"golang.org/x/exp/slog"
)

// bundle is a bundle of messages to be processed.
Expand Down Expand Up @@ -194,6 +196,20 @@ func (d *bundle) process() ([]*protocol.TransactionStatus, error) {
d.produced = append(d.produced, ctx.produced...)
}

// Check for duplicates. This is a serious error if it occurs, but returning
// an error here would effectively stall the network.
pCount := map[[32]byte]int{}
pID := map[[32]byte]*url.TxID{}
for _, m := range d.produced {
pCount[m.Message.Hash()]++
pID[m.Message.Hash()] = m.Message.ID()
}
for h, c := range pCount {
if c > 1 {
slog.ErrorCtx(d.Context, "Duplicate synthetic messages", "id", pID[h], "count", c)
}
}

// Execute produced messages immediately if and only if the producer and
// destination are in the same domain. This implementation is inefficient
// but it preserves order and its good enough for now.
Expand Down
34 changes: 29 additions & 5 deletions internal/core/execute/v2/block/msg_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"gitlab.com/accumulatenetwork/accumulate/pkg/types/messaging"
"gitlab.com/accumulatenetwork/accumulate/pkg/url"
"gitlab.com/accumulatenetwork/accumulate/protocol"
"golang.org/x/exp/slog"
)

// MessageContext is the context in which a message is processed.
Expand Down Expand Up @@ -151,14 +152,37 @@ func (m *MessageContext) queueAdditional(msg messaging.Message) {

// didProduce queues a produced synthetic message for dispatch.
func (m *MessageContext) didProduce(batch *database.Batch, dest *url.URL, msg messaging.Message) error {
if dest == nil {
panic("nil destination for produced message")
}
m.produced = append(m.produced, &ProducedMessage{
p := &ProducedMessage{
Producer: m.message.ID(),
Destination: dest,
Message: msg,
})
}

// Add an index to synthetic transactions to differentiate otherwise
// identical messages
m.syntheticCount++
switch msg := msg.(type) {
case *messaging.TransactionMessage:
if !m.GetActiveGlobals().ExecutorVersion.V2BaikonurEnabled() {
break
}

// SyntheticForwardTransaction is the only synthetic transaction type
// that does not satisfy this interface, but they are not used in v2
txn, ok := msg.Transaction.Body.(protocol.SyntheticTransaction)
if !ok {
slog.ErrorCtx(m.Context, "Synthetic transaction is not synthetic", "id", msg.ID())
break
}

txn.SetIndex(m.syntheticCount)
p.Message = msg.Copy() // Clear memoized hashes
}

if dest == nil {
panic("nil destination for produced message")
}
m.produced = append(m.produced, p)

err := batch.Message(m.message.Hash()).Produced().Add(msg.ID())
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/core/execute/v2/block/msg_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,9 @@ func (x TransactionMessage) executeTransaction(batch *database.Batch, ctx *Trans

func (x TransactionMessage) postProcess(batch *database.Batch, ctx *TransactionContext, state *chain.ProcessTransactionState, delivered bool) error {
// Calculate refunds
var swos []protocol.SynthTxnWithOrigin
var swos []protocol.SyntheticTransaction
for _, newTxn := range state.ProducedTxns {
if swo, ok := newTxn.Body.(protocol.SynthTxnWithOrigin); ok {
if swo, ok := newTxn.Body.(protocol.SyntheticTransaction); ok {
swos = append(swos, swo)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/core/execute/v2/block/synthetic.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (x *Executor) produceSynthetic(batch *database.Batch, produced []*ProducedM
// transaction. setSyntheticOrigin sets the refund amount for each synthetic
// transaction, spreading the potential refund across all produced synthetic
// transactions.
func (x *Executor) setSyntheticOrigin(batch *database.Batch, from *protocol.Transaction, produced []protocol.SynthTxnWithOrigin) error {
func (x *Executor) setSyntheticOrigin(batch *database.Batch, from *protocol.Transaction, produced []protocol.SyntheticTransaction) error {
for _, swo := range produced {
swo.SetCause(from.ID().Hash(), from.ID().Account())
}
Expand Down
2 changes: 1 addition & 1 deletion internal/core/execute/v2/block/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ func (x *TransactionContext) recordFailedTransaction(batch *database.Batch, deli
}

// If this transaction is a synthetic transaction, send a refund
if swo, ok := delivery.Transaction.Body.(protocol.SynthTxnWithOrigin); ok {
if swo, ok := delivery.Transaction.Body.(protocol.SyntheticTransaction); ok {
init, refundAmount := swo.GetRefund()
if refundAmount > 0 {
refund := new(protocol.SyntheticDepositCredits)
Expand Down
16 changes: 15 additions & 1 deletion protocol/synthetic_origin.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 The Accumulate Authors
// Copyright 2023 The Accumulate Authors
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
Expand All @@ -8,15 +8,25 @@ package protocol

import "gitlab.com/accumulatenetwork/accumulate/pkg/url"

type SyntheticTransaction interface {
SynthTxnWithOrigin
TransactionBody
}

type SynthTxnWithOrigin interface {
GetCause() (cause [32]byte, source *url.URL)
SetCause(cause [32]byte, source *url.URL)
GetRefund() (initiator *url.URL, refund Fee)
SetRefund(initiator *url.URL, refund Fee)
SetIndex(index uint64)
}

var _ SynthTxnWithOrigin = (*SyntheticOrigin)(nil)
var _ SynthTxnWithOrigin = (*SyntheticCreateIdentity)(nil)
var _ SynthTxnWithOrigin = (*SyntheticWriteData)(nil)
var _ SynthTxnWithOrigin = (*SyntheticDepositTokens)(nil)
var _ SynthTxnWithOrigin = (*SyntheticDepositCredits)(nil)
var _ SynthTxnWithOrigin = (*SyntheticBurnTokens)(nil)

func (so *SyntheticOrigin) Source() *url.URL {
if so.Cause == nil {
Expand All @@ -41,3 +51,7 @@ func (so *SyntheticOrigin) SetRefund(initiator *url.URL, refund Fee) {
so.Initiator = initiator
so.FeeRefund = uint64(refund)
}

func (so *SyntheticOrigin) SetIndex(index uint64) {
so.Index = index
}
4 changes: 3 additions & 1 deletion protocol/synthetic_transactions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ SyntheticOrigin:
- name: FeeRefund
description: is portion of the cause's fee that will be refunded if this transaction fails
type: uint
- name: Index
type: uint

SyntheticCreateIdentity:
union: { type: transaction }
Expand Down Expand Up @@ -52,7 +54,7 @@ SyntheticDepositTokens:
- name: IsRefund
type: bool


SyntheticDepositCredits:
union: { type: transaction }
embeddings:
Expand Down
4 changes: 0 additions & 4 deletions protocol/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,6 @@ func (t *Transaction) GetAdditionalAuthorities() []*url.URL {
return nil
}

type SyntheticTransaction interface {
TransactionBody
}

func (tx *SendTokens) AddRecipient(to *url.URL, amount *big.Int) {
recipient := new(TokenRecipient)
recipient.Url = to
Expand Down
Loading

0 comments on commit b230d2d

Please sign in to comment.