diff --git a/internal/core/execute/v2/block/msg_block_anchor.go b/internal/core/execute/v2/block/msg_block_anchor.go index 1cdfea3ec..0921bb021 100644 --- a/internal/core/execute/v2/block/msg_block_anchor.go +++ b/internal/core/execute/v2/block/msg_block_anchor.go @@ -7,6 +7,8 @@ package block import ( + "strings" + "gitlab.com/accumulatenetwork/accumulate/internal/core" "gitlab.com/accumulatenetwork/accumulate/internal/core/execute/v2/chain" "gitlab.com/accumulatenetwork/accumulate/internal/database" @@ -27,11 +29,9 @@ type BlockAnchor struct{} type blockAnchorContext struct { *TransactionContext - sequenced *messaging.SequencedMessage - + sequenced *messaging.SequencedMessage blockAnchor *messaging.BlockAnchor - - signer protocol.Signer2 + signer protocol.Signer2 } func (x BlockAnchor) Validate(batch *database.Batch, ctx *MessageContext) (*protocol.TransactionStatus, error) { @@ -136,18 +136,12 @@ func (x BlockAnchor) check(ctx *MessageContext, batch *database.Batch) (*blockAn // Resolve placeholders txn := txnMsg.Transaction - signed := seq.Hash() if txn.Body.Type() == protocol.TransactionTypeRemote && ctx.GetActiveGlobals().ExecutorVersion.V2BaikonurEnabled() { var err error txn, err = ctx.getTransaction(batch, txn.ID().Hash()) if err != nil { return nil, errors.UnknownError.WithFormat("load transaction: %w", err) } - - // Recalculate the hash with the full transaction - seq2 := seq.Copy() - seq2.Message = &messaging.TransactionMessage{Transaction: txn} - signed = seq2.Hash() } // Verify the transaction is an anchor @@ -159,11 +153,6 @@ func (x BlockAnchor) check(ctx *MessageContext, batch *database.Batch) (*blockAn return nil, errors.InternalError.WithFormat("sequence is missing source") } - // Basic validation - if !anchor.Signature.Verify(nil, signed[:]) { - return nil, errors.Unauthenticated.WithFormat("invalid signature") - } - // Verify the signer is a validator of this partition partition, ok := protocol.ParsePartitionUrl(seq.Source) if !ok { @@ -180,12 +169,46 @@ func (x BlockAnchor) check(ctx *MessageContext, batch *database.Batch) (*blockAn return nil, errors.Unauthorized.WithFormat("key is not an active validator for %s", partition) } - return &blockAnchorContext{ + // Basic validation + ctx2 := &blockAnchorContext{ TransactionContext: ctx.txnWith(txn), sequenced: seq, blockAnchor: anchor, signer: signer, - }, nil + } + err := x.checkSignature(ctx2) + if err != nil { + return nil, err + } + + return ctx2, nil +} + +func (x BlockAnchor) checkSignature(ctx *blockAnchorContext) error { + // Recalculate the hash in case the transaction was originally a remote + // transaction + txn := &messaging.TransactionMessage{Transaction: ctx.transaction} + seq := *ctx.sequenced + seq.Message = txn + if hash := seq.Hash(); ctx.blockAnchor.Signature.Verify(nil, hash[:]) { + return nil + } + + // Allow reusing signatures from the DN + part, _ := protocol.ParsePartitionUrl(ctx.transaction.Header.Principal) + if ctx.GetActiveGlobals().ExecutorVersion.V2VandenbergEnabled() && + ctx.transaction.Body.Type() == protocol.TransactionTypeDirectoryAnchor && + !strings.EqualFold(part, protocol.Directory) { + + seq.Destination = protocol.DnUrl() + txn.Transaction = txn.Transaction.Copy() + txn.Transaction.Header.Principal = protocol.DnUrl().JoinPath(ctx.transaction.Header.Principal.Path) + if hash := seq.Hash(); ctx.blockAnchor.Signature.Verify(nil, hash[:]) { + return nil + } + } + + return errors.Unauthenticated.WithFormat("invalid signature") } func (x BlockAnchor) txnIsReady(batch *database.Batch, ctx *blockAnchorContext) (bool, error) { diff --git a/test/e2e/net_anchors_test.go b/test/e2e/net_anchors_test.go index 555afab53..27b43cd1a 100644 --- a/test/e2e/net_anchors_test.go +++ b/test/e2e/net_anchors_test.go @@ -7,13 +7,18 @@ package e2e import ( + "context" "testing" "github.com/stretchr/testify/require" "gitlab.com/accumulatenetwork/accumulate/pkg/build" + "gitlab.com/accumulatenetwork/accumulate/pkg/types/messaging" + "gitlab.com/accumulatenetwork/accumulate/pkg/url" . "gitlab.com/accumulatenetwork/accumulate/protocol" . "gitlab.com/accumulatenetwork/accumulate/test/harness" + . "gitlab.com/accumulatenetwork/accumulate/test/helpers" "gitlab.com/accumulatenetwork/accumulate/test/simulator" + acctesting "gitlab.com/accumulatenetwork/accumulate/test/testing" ) // TestDropInitialAnchor is a simple test that simulates adverse network @@ -55,3 +60,99 @@ func TestDropInitialAnchor(t *testing.T) { require.NoError(t, err) require.Equal(t, 123, int(account.Balance.Int64())) } + +// TestReuseDirectoryAnchorSignatures verifies that anchor signatures the DN sends to +// itself can be reused for DN anchors sent to a BVN. +func TestReuseDirectoryAnchorSignatures(t *testing.T) { + const numVal, numNode = 3, 1 + + var trapBlock uint64 + var dnSigs []KeySignature + var toBVN *messaging.SequencedMessage + anchorTrap := func(ctx context.Context, env *messaging.Envelope) (send bool, err error) { + if trapBlock == 0 || len(env.Messages) != 1 { + return true, nil + } + + // Is the envelope a DN anchor signature for the target block? + blk, ok := env.Messages[0].(*messaging.BlockAnchor) + if !ok { + return true, nil + } + seq, ok := blk.Anchor.(*messaging.SequencedMessage) + if !ok { + return true, nil + } + txn, ok := seq.Message.(*messaging.TransactionMessage) + if !ok { + return true, nil + } + body, ok := txn.Transaction.Body.(*DirectoryAnchor) + if !ok || body.MinorBlockIndex != trapBlock { + return true, nil + } + + // Capture signatures sent to the DN but don't drop them + if DnUrl().Equal(seq.Destination) { + dnSigs = append(dnSigs, blk.Signature) + return true, nil + } + + // Let the first signature sent to the BVN through and drop the rest + if toBVN == nil { + toBVN = seq + return true, nil + } + return false, nil + } + + // Initialize + sim := NewSim(t, + simulator.SimpleNetwork(t.Name(), numVal, numNode), + simulator.Genesis(GenesisTime), + simulator.CaptureDispatchedMessages(anchorTrap), + ) + + // Do something + alice := url.MustParse("alice") + aliceKey := acctesting.GenerateKey(alice) + MakeIdentity(t, sim.DatabaseFor(alice), alice, aliceKey[32:]) + CreditCredits(t, sim.DatabaseFor(alice), alice.JoinPath("book", "1"), 1e9) + + st := sim.BuildAndSubmitTxnSuccessfully( + build.Transaction().For(alice, "book", "1").BurnCredits(1). + SignWith(alice, "book", "1").Version(1).Timestamp(1).PrivateKey(aliceKey)) + + // Trap anchors + trapBlock = sim.S.BlockIndex(Directory) + sim.StepUntil( + True(func(*Harness) bool { + return toBVN != nil && len(dnSigs) == numVal*numNode + })) + + // Verify the anchor sent to the BVN is pending + txn := toBVN.Message.(*messaging.TransactionMessage).Transaction + sim.StepUntil( + Txn(txn.ID()).IsPending()) + + // Take the anchors sent to the DN and send them to the BVN to resolve the + // anchor + for _, sig := range dnSigs { + sim.SubmitSuccessfully(&messaging.Envelope{ + Messages: []messaging.Message{ + &messaging.BlockAnchor{ + Signature: sig, + Anchor: toBVN, + }, + }, + }) + } + + // Verify the anchor completes + sim.StepUntil( + Txn(txn.ID()).Completes()) + + // Verify the user transaction completes + sim.StepUntil( + Txn(st.TxID).Completes()) +} diff --git a/test/simulator/factory.go b/test/simulator/factory.go index 3d2da6b8b..6b845c45c 100644 --- a/test/simulator/factory.go +++ b/test/simulator/factory.go @@ -586,7 +586,9 @@ func (f *nodeFactory) makeCoreApp() *consensus.Node { RunTask: execOpts.BackgroundTaskLauncher, DropInitialAnchor: f.dropInitialAnchor, EnableAnchorHealing: &enableAnchorHealing, - Intercept: f.interceptDispatchedMessages, + + // Setting Intercept is not necessary because the dispatcher will + // intercept messages } err := conductor.Start(f.getEventBus()) if err != nil {