diff --git a/dapps/valuetransfers/packages/consensus/fcob.go b/dapps/valuetransfers/packages/consensus/fcob.go index d5b86548b57dc8c2eeb850a6ab5d7aa6cd564e44..1d2a98aa55f4a4299b015386b24a7bdf1f33e7fb 100644 --- a/dapps/valuetransfers/packages/consensus/fcob.go +++ b/dapps/valuetransfers/packages/consensus/fcob.go @@ -3,6 +3,7 @@ package consensus import ( "time" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" "github.com/iotaledger/hive.go/events" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" @@ -138,9 +139,10 @@ func (fcob *FCOB) setFinalized(cachedTransactionMetadata *tangle.CachedTransacti // onFork triggers a voting process whenever a Transaction gets forked into a new Branch. The initial opinion is derived // from the preferred flag that was set using the FCOB rule. -func (fcob *FCOB) onFork(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata) { +func (fcob *FCOB) onFork(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, cachedTargetBranch *branchmanager.CachedBranch, conflictingInputs []transaction.OutputID) { defer cachedTransaction.Release() defer cachedTransactionMetadata.Release() + defer cachedTargetBranch.Release() transactionMetadata := cachedTransactionMetadata.Unwrap() if transactionMetadata == nil { diff --git a/dapps/valuetransfers/packages/tangle/tangle.go b/dapps/valuetransfers/packages/tangle/tangle.go index 104194ff524c394dfd7bf17933fb5b06414d2eba..9a52491bd76f88d858c4f4729109efaa1c96b3f3 100644 --- a/dapps/valuetransfers/packages/tangle/tangle.go +++ b/dapps/valuetransfers/packages/tangle/tangle.go @@ -4,6 +4,7 @@ import ( "container/list" "errors" "fmt" + "log" "math" "time" @@ -77,8 +78,10 @@ func (tangle *Tangle) AttachPayload(payload *payload.Payload) { // AttachPayloadSync is the worker function that stores the payload and calls the corresponding storage events. func (tangle *Tangle) AttachPayloadSync(payloadToStore *payload.Payload) { // store the payload models or abort if we have seen the payload already + log.Println("storing payload", payloadToStore.ID()) cachedPayload, cachedPayloadMetadata, payloadStored := tangle.storePayload(payloadToStore) if !payloadStored { + log.Println("could not store payload", payloadToStore.ID()) return } defer cachedPayload.Release() @@ -235,7 +238,7 @@ func (tangle *Tangle) Fork(transactionID transaction.ID, conflictingInputs []tra } // trigger events + set result - tangle.Events.Fork.Trigger(cachedTransaction, cachedTransactionMetadata) + tangle.Events.Fork.Trigger(cachedTransaction, cachedTransactionMetadata, cachedTargetBranch, conflictingInputs) forked = true return @@ -944,6 +947,7 @@ func (tangle *Tangle) storeTransactionModels(solidPayload *payload.Payload) (cac })} if transactionIsNew { + log.Println("stored transaction", cachedTransaction.Unwrap().ID()) cachedTransactionMetadata = &CachedTransactionMetadata{CachedObject: tangle.transactionMetadataStorage.Store(NewTransactionMetadata(solidPayload.Transaction().ID()))} // store references to the consumed outputs @@ -953,6 +957,7 @@ func (tangle *Tangle) storeTransactionModels(solidPayload *payload.Payload) (cac return true }) } else { + log.Println("transaction was already stored", cachedTransaction.Unwrap().ID()) cachedTransactionMetadata = &CachedTransactionMetadata{CachedObject: tangle.transactionMetadataStorage.Load(solidPayload.Transaction().ID().Bytes())} } diff --git a/plugins/webapi/value/gettransactionbyid/handler.go b/plugins/webapi/value/gettransactionbyid/handler.go index 3bd214df64635497b423d0ee0319f303e5987ef3..8e68d9ef71c06550995bee9f09d104b88568f0f7 100644 --- a/plugins/webapi/value/gettransactionbyid/handler.go +++ b/plugins/webapi/value/gettransactionbyid/handler.go @@ -1,20 +1,20 @@ package gettransactionbyid import ( + "log" "net/http" "github.com/iotaledger/goshimmer/dapps/valuetransfers" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" "github.com/iotaledger/goshimmer/plugins/webapi/value/utils" "github.com/labstack/echo" - "github.com/labstack/gommon/log" ) // Handler gets the transaction by id. func Handler(c echo.Context) error { txnID, err := transaction.IDFromBase58(c.QueryParam("txnID")) if err != nil { - log.Info(err) + log.Println(err) return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) } @@ -22,16 +22,19 @@ func Handler(c echo.Context) error { cachedTxnMetaObj := valuetransfers.Tangle.TransactionMetadata(txnID) defer cachedTxnMetaObj.Release() if !cachedTxnMetaObj.Exists() { + log.Println("transaction meta doesn't exist for", txnID) return c.JSON(http.StatusNotFound, Response{Error: "Transaction not found"}) } cachedTxnObj := valuetransfers.Tangle.Transaction(txnID) defer cachedTxnObj.Release() if !cachedTxnObj.Exists() { + log.Println("transaction doesn't exist for", txnID) return c.JSON(http.StatusNotFound, Response{Error: "Transaction not found"}) } txn := utils.ParseTransaction(cachedTxnObj.Unwrap()) txnMeta := cachedTxnMetaObj.Unwrap() + txnMeta.Preferred() return c.JSON(http.StatusOK, Response{ Transaction: txn, InclusionState: utils.InclusionState{ @@ -41,6 +44,7 @@ func Handler(c echo.Context) error { Solid: txnMeta.Solid(), Rejected: txnMeta.Rejected(), Finalized: txnMeta.Finalized(), + Preferred: txnMeta.Preferred(), }, }) } diff --git a/plugins/webapi/value/sendtransaction/handler.go b/plugins/webapi/value/sendtransaction/handler.go index b1dfa63d99ed7f3b85b4ac1432f4e9e4ab1ecc61..bbf8bd6782964c0b8d6af3e55a94c7106c954b12 100644 --- a/plugins/webapi/value/sendtransaction/handler.go +++ b/plugins/webapi/value/sendtransaction/handler.go @@ -1,37 +1,39 @@ package sendtransaction import ( + "log" "net/http" "github.com/iotaledger/goshimmer/dapps/valuetransfers" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" "github.com/iotaledger/goshimmer/plugins/issuer" "github.com/labstack/echo" - "github.com/labstack/gommon/log" ) // Handler sends a transaction. func Handler(c echo.Context) error { var request Request if err := c.Bind(&request); err != nil { - log.Info(err.Error()) + log.Println(err.Error()) return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) } // prepare transaction tx, _, err := transaction.FromBytes(request.TransactionBytes) if err != nil { - log.Info(err.Error()) + log.Println(err.Error()) return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) } // Prepare value payload and send the message to tangle payload := valuetransfers.ValueObjectFactory().IssueTransaction(tx) + log.Println("issued transaction") _, err = issuer.IssuePayload(payload) if err != nil { - log.Info(err.Error()) + log.Println(err.Error()) return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) } + log.Println("issued payload") return c.JSON(http.StatusOK, Response{TransactionID: tx.ID().String()}) } diff --git a/plugins/webapi/value/utils/transaction_handler.go b/plugins/webapi/value/utils/transaction_handler.go index 26acd8d9e25001a6428128348d3ae4cd84ec3b12..3c50c38d3d5ffbd0ccb7dcb9647697c10959401d 100644 --- a/plugins/webapi/value/utils/transaction_handler.go +++ b/plugins/webapi/value/utils/transaction_handler.go @@ -70,4 +70,5 @@ type InclusionState struct { Solid bool `json:"solid,omitempty"` Rejected bool `json:"rejected,omitempty"` Finalized bool `json:"finalized,omitempty"` + Preferred bool `json:"preferred,omitempty"` } diff --git a/tools/integration-tests/assets/entrypoint.sh b/tools/integration-tests/assets/entrypoint.sh index 7e5f060f00c41ae1b7f58e452fddc87a7909221d..9d690630ac57a3be7f712d57beeca177d7fc6f5b 100644 --- a/tools/integration-tests/assets/entrypoint.sh +++ b/tools/integration-tests/assets/entrypoint.sh @@ -6,4 +6,4 @@ chmod 777 /assets/* echo "assets:" ls /assets echo "running tests..." -go test ./tests/"${TEST_NAME}" -run TestConsensusConflicts -v -timeout 30m +go test ./tests/"${TEST_NAME}" -run TestConsensusFiftyFiftyOpinionSplit -v -timeout 30m diff --git a/tools/integration-tests/tester/framework/framework.go b/tools/integration-tests/tester/framework/framework.go index f246f21f916d7129195b1013975f6ab5cdc40812..4c19cc4ac861bf931af96574c19e1b94b2ee70dc 100644 --- a/tools/integration-tests/tester/framework/framework.go +++ b/tools/integration-tests/tester/framework/framework.go @@ -80,7 +80,7 @@ func (f *Framework) CreateNetwork(name string, peers int, minimumNeighbors int, // create peers/GoShimmer nodes for i := 0; i < peers; i++ { config := GoShimmerConfig{ - Bootstrap: i == 0, + Bootstrap: true, BootstrapInitialIssuanceTimePeriodSec: bootstrapInitialIssuanceTimePeriodSec, Faucet: i == 0, } @@ -128,7 +128,7 @@ func (f *Framework) CreateNetworkWithPartitions(name string, peers, partitions, // create peers/GoShimmer nodes for i := 0; i < peers; i++ { - config := GoShimmerConfig{Bootstrap: i == 0} + config := GoShimmerConfig{Bootstrap: true} if _, err = network.CreatePeer(config); err != nil { return nil, err } diff --git a/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go b/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go index 842e4780dec2cbb242828166b9e3edf29159560d..ceecb87bb82980a67b32125fb3e45dc053be19d6 100644 --- a/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go +++ b/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go @@ -18,14 +18,15 @@ import ( "github.com/stretchr/testify/require" ) -// TestConsensusConflicts issues valid conflicting value objects and makes sure that -// the conflicts are resolved via FPC. -func TestConsensusConflicts(t *testing.T) { +// TestConsensusFiftyFiftyOpinionSplit spawns two network partitions with their own peers, +// then issues valid value objects spending the genesis in both, deletes the partitions (and lets them merge) +// and then checks that the conflicts are resolved via FPC. +func TestConsensusFiftyFiftyOpinionSplit(t *testing.T) { n, err := f.CreateNetworkWithPartitions("consensus_TestConsensusConflicts", 8, 2, 4) require.NoError(t, err) defer tests.ShutdownNetwork(t, n) - time.Sleep(10 * time.Second) + time.Sleep(5 * time.Second) // split the network for i, partition := range n.Partitions() { @@ -58,7 +59,7 @@ func TestConsensusConflicts(t *testing.T) { transaction.NewInputs(genesisOutputID), transaction.NewOutputs(map[address.Address][]*balance.Balance{ destAddr: { - {Value: genesisBalance / 2, Color: balance.ColorIOTA}, + {Value: genesisBalance, Color: balance.ColorIOTA}, }, })) tx = tx.Sign(signaturescheme.ED25519(*genesisWallet.Seed().KeyPair(0))) @@ -72,7 +73,7 @@ func TestConsensusConflicts(t *testing.T) { assert.NoError(t, err) // check that the transaction is actually available on all the peers of the partition - missing, err := tests.AwaitTransactionAvailability(partition.Peers(), []string{txID}, 4*time.Second) + missing, err := tests.AwaitTransactionAvailability(partition.Peers(), []string{txID}, 15*time.Second) if err != nil { assert.NoError(t, err, "transactions should have been available in partition") for p, missingOnPeer := range missing { @@ -90,6 +91,22 @@ func TestConsensusConflicts(t *testing.T) { // sleep the avg. network delay so both partitions prefer their own first seen transaction time.Sleep(valuetransfers.AverageNetworkDelay) + // check that each partition is preferring its corresponding transaction + log.Println("checking that each partition likes its corresponding transaction before the conflict:") + for i, partition := range n.Partitions() { + tests.CheckTransactions(t, partition.Peers(), map[string]*tests.ExpectedTransaction{ + conflictingTxIDs[i]: nil, + }, true, tests.ExpectedInclusionState{ + Confirmed: tests.False(), + Finalized: tests.False(), + Conflict: tests.False(), + Solid: tests.True(), + Rejected: tests.False(), + Liked: tests.True(), + Preferred: tests.True(), + }) + } + // merge back the partitions log.Println("merging partitions...") assert.NoError(t, n.DeletePartitions(), "merging the network partitions should work") @@ -125,13 +142,42 @@ func TestConsensusConflicts(t *testing.T) { } } + // check that the transactions are marked as conflicting tests.CheckTransactions(t, n.Peers(), expectations, true, tests.ExpectedInclusionState{ - Confirmed: tests.False(), - Finalized: tests.False(), - // should be part of a conflict set - Conflict: tests.True(), - Solid: tests.True(), - Rejected: tests.False(), - Liked: tests.False(), + Finalized: tests.True(), + Conflict: tests.True(), + Solid: tests.True(), }) + + // now all transactions must be finalized and at most one must be confirmed + var confirmedOverConflictSet int + for _, conflictingTx := range conflictingTxIDs { + var rejected, confirmed int + for _, p := range n.Peers() { + tx, err := p.GetTransactionByID(conflictingTx) + assert.NoError(t, err) + if tx.InclusionState.Confirmed { + confirmed++ + continue + } + if tx.InclusionState.Rejected { + rejected++ + } + } + + if rejected != 0 { + assert.Len(t, n.Peers(), rejected, "the rejected count for %s should be equal to the amount of peers", conflictingTx) + } + if confirmed != 0 { + assert.Len(t, n.Peers(), confirmed, "the confirmed count for %s should be equal to the amount of peers", conflictingTx) + confirmedOverConflictSet++ + } + + assert.False(t, rejected == 0 && confirmed == 0, "a transaction must either be rejected or confirmed") + } + + // there must only be one confirmed transaction out of the conflict set + if confirmedOverConflictSet != 0 { + assert.Equal(t, 1, confirmedOverConflictSet, "only one transaction can be confirmed out of the conflict set. %d of %d are confirmed", confirmedOverConflictSet, len(conflictingTxIDs)) + } } diff --git a/tools/integration-tests/tester/tests/testutil.go b/tools/integration-tests/tester/tests/testutil.go index 65ff862860e9d0421e9d2a8b8f18ed5c7eca32c8..d0b6e75f4270b3bcc15ed012bd6130a78047bcbf 100644 --- a/tools/integration-tests/tester/tests/testutil.go +++ b/tools/integration-tests/tester/tests/testutil.go @@ -383,6 +383,8 @@ type ExpectedInclusionState struct { Rejected *bool // The optional liked state to check against. Liked *bool + // The optional preferred state to check against. + Preferred *bool } // True returns a pointer to a true bool. @@ -426,30 +428,33 @@ func CheckTransactions(t *testing.T, peers []*framework.Peer, transactionIDs map // check inclusion state if expectedInclusionState.Confirmed != nil { - assert.Equal(t, *expectedInclusionState.Confirmed, resp.InclusionState.Confirmed, "confirmed state doesn't match") + assert.Equal(t, *expectedInclusionState.Confirmed, resp.InclusionState.Confirmed, "confirmed state doesn't match - %s", txId) } if expectedInclusionState.Conflict != nil { - assert.Equal(t, *expectedInclusionState.Conflict, resp.InclusionState.Conflict, "conflict state doesn't match") + assert.Equal(t, *expectedInclusionState.Conflict, resp.InclusionState.Conflict, "conflict state doesn't match - %s", txId) } if expectedInclusionState.Solid != nil { - assert.Equal(t, *expectedInclusionState.Solid, resp.InclusionState.Solid, "solid state doesn't match") + assert.Equal(t, *expectedInclusionState.Solid, resp.InclusionState.Solid, "solid state doesn't match - %s", txId) } if expectedInclusionState.Rejected != nil { - assert.Equal(t, *expectedInclusionState.Rejected, resp.InclusionState.Rejected, "rejected state doesn't match") + assert.Equal(t, *expectedInclusionState.Rejected, resp.InclusionState.Rejected, "rejected state doesn't match - %s", txId) } if expectedInclusionState.Liked != nil { - assert.Equal(t, *expectedInclusionState.Liked, resp.InclusionState.Liked, "liked state doesn't match") + assert.Equal(t, *expectedInclusionState.Liked, resp.InclusionState.Liked, "liked state doesn't match - %s", txId) + } + if expectedInclusionState.Preferred != nil { + assert.Equal(t, *expectedInclusionState.Preferred, resp.InclusionState.Preferred, "preferred state doesn't match - %s", txId) } if expectedTransaction != nil { if expectedTransaction.Inputs != nil { - assert.Equal(t, *expectedTransaction.Inputs, resp.Transaction.Inputs, "inputs do not match") + assert.Equal(t, *expectedTransaction.Inputs, resp.Transaction.Inputs, "inputs do not match - %s", txId) } if expectedTransaction.Outputs != nil { - assert.Equal(t, *expectedTransaction.Outputs, resp.Transaction.Outputs, "outputs do not match") + assert.Equal(t, *expectedTransaction.Outputs, resp.Transaction.Outputs, "outputs do not match - %s", txId) } if expectedTransaction.Signature != nil { - assert.Equal(t, *expectedTransaction.Signature, resp.Transaction.Signature, "signatures do not match") + assert.Equal(t, *expectedTransaction.Signature, resp.Transaction.Signature, "signatures do not match - %s", txId) } } }