diff --git a/wire/leaf.go b/wire/leaf.go
index a068bf8a..b61044be 100644
--- a/wire/leaf.go
+++ b/wire/leaf.go
@@ -49,6 +49,23 @@ type LeafData struct {
 	PkScript              []byte
 }
 
+// Copy creates a deep copy of the leafdata so the original does not get modified
+// when the copy is manipulated.
+func (l *LeafData) Copy() *LeafData {
+	newL := LeafData{
+		BlockHash:             l.BlockHash,
+		OutPoint:              l.OutPoint,
+		Height:                l.Height,
+		IsCoinBase:            l.IsCoinBase,
+		Amount:                l.Amount,
+		ReconstructablePkType: l.ReconstructablePkType,
+		PkScript:              make([]byte, len(l.PkScript)),
+	}
+
+	copy(newL.PkScript, l.PkScript)
+	return &newL
+}
+
 func (l LeafData) MarshalJSON() ([]byte, error) {
 	s := struct {
 		BlockHash             string `json:"blockhash"`
diff --git a/wire/leaf_test.go b/wire/leaf_test.go
index 2c938798..5c6e6233 100644
--- a/wire/leaf_test.go
+++ b/wire/leaf_test.go
@@ -633,3 +633,42 @@ func TestLeafHash(t *testing.T) {
 		}
 	}
 }
+
+// TestLeafDataCopy tests that modifying the leafdata copy does not modify the original.
+func TestLeafDataCopy(t *testing.T) {
+	ld := LeafData{
+		BlockHash: *newHashFromStr("00000000000172ff8a4e14441512072bacaf8d38b995a3fcd2f8435efc61717d"),
+		OutPoint: OutPoint{
+			Hash:  *newHashFromStr("061bb0bf3a1b9df13773da06bf92920394887a9c2b8b8772ac06be4e077df5eb"),
+			Index: 10,
+		},
+		Amount:     200000,
+		PkScript:   hexToBytes("a914e8d74935cfa223f9750a32b18d609cba17a5c3fe87"),
+		Height:     1599255,
+		IsCoinBase: false,
+	}
+
+	ldOrig := LeafData{
+		BlockHash: *newHashFromStr("00000000000172ff8a4e14441512072bacaf8d38b995a3fcd2f8435efc61717d"),
+		OutPoint: OutPoint{
+			Hash:  *newHashFromStr("061bb0bf3a1b9df13773da06bf92920394887a9c2b8b8772ac06be4e077df5eb"),
+			Index: 10,
+		},
+		Amount:     200000,
+		PkScript:   hexToBytes("a914e8d74935cfa223f9750a32b18d609cba17a5c3fe87"),
+		Height:     1599255,
+		IsCoinBase: false,
+	}
+
+	ldCopy := ld.Copy()
+	ldCopy.OutPoint.Index = 7777
+	ldCopy.OutPoint.Hash[31] = 0x17
+	ldCopy.PkScript[0] = 0x77
+	if reflect.DeepEqual(ldCopy, ld) {
+		t.Fatalf("ldCopy and ld are same")
+	}
+
+	if !reflect.DeepEqual(ld, ldOrig) {
+		t.Fatalf("ld and ldOrig are different")
+	}
+}
diff --git a/wire/message.go b/wire/message.go
index d6447768..5aa4309c 100644
--- a/wire/message.go
+++ b/wire/message.go
@@ -38,6 +38,7 @@ const (
 	CmdNotFound     = "notfound"
 	CmdBlock        = "block"
 	CmdTx           = "tx"
+	CmdUtreexoTx    = "utreexotx"
 	CmdGetHeaders   = "getheaders"
 	CmdHeaders      = "headers"
 	CmdPing         = "ping"
@@ -131,6 +132,9 @@ func makeEmptyMessage(command string) (Message, error) {
 	case CmdTx:
 		msg = &MsgTx{}
 
+	case CmdUtreexoTx:
+		msg = &MsgUtreexoTx{}
+
 	case CmdPing:
 		msg = &MsgPing{}
 
diff --git a/wire/message_test.go b/wire/message_test.go
index 7965e561..1f04703a 100644
--- a/wire/message_test.go
+++ b/wire/message_test.go
@@ -57,6 +57,7 @@ func TestMessage(t *testing.T) {
 	msgGetData := NewMsgGetData()
 	msgNotFound := NewMsgNotFound()
 	msgTx := NewMsgTx(1)
+	msgUtreexoTx := NewMsgUtreexoTx(1)
 	msgPing := NewMsgPing(123123)
 	msgPong := NewMsgPong(123123)
 	msgGetHeaders := NewMsgGetHeaders()
@@ -94,6 +95,7 @@ func TestMessage(t *testing.T) {
 		{msgGetData, msgGetData, pver, MainNet, 25},
 		{msgNotFound, msgNotFound, pver, MainNet, 25},
 		{msgTx, msgTx, pver, MainNet, 34},
+		{msgUtreexoTx, msgUtreexoTx, pver, MainNet, 37},
 		{msgPing, msgPing, pver, MainNet, 32},
 		{msgPong, msgPong, pver, MainNet, 32},
 		{msgGetHeaders, msgGetHeaders, pver, MainNet, 61},
diff --git a/wire/msgutreexotx.go b/wire/msgutreexotx.go
new file mode 100644
index 00000000..7852ba5b
--- /dev/null
+++ b/wire/msgutreexotx.go
@@ -0,0 +1,114 @@
+// Copyright (c) 2024 The btcsuite developers
+// Use of this source code is governed by an ISC
+// license that can be found in the LICENSE file.
+
+package wire
+
+import (
+	"io"
+)
+
+// MsgUtreexoTx implements the Message interface and represents a bitcoin utreexo
+// tx message. It is used to deliver transaction information in response to a getdata
+// message (MsgGetData) for a given transaction with the utreexo proof to verify the
+// transaction.
+//
+// Use the AddTxIn and AddTxOut functions to build up the list of transaction
+// inputs and outputs.
+type MsgUtreexoTx struct {
+	// MsgTx is the underlying Bitcoin transaction message.
+	MsgTx
+
+	// UData is the underlying utreexo data.
+	UData
+}
+
+// Copy creates a deep copy of a transaction so that the original does not get
+// modified when the copy is manipulated.
+func (msg *MsgUtreexoTx) Copy() *MsgUtreexoTx {
+	msgTx := msg.MsgTx.Copy()
+	newTx := MsgUtreexoTx{
+		MsgTx: *msgTx,
+		UData: *msg.UData.Copy(),
+	}
+
+	return &newTx
+}
+
+// BtcDecode decodes r using the bitcoin protocol encoding into the receiver.
+// This is part of the Message interface implementation.
+// See Deserialize for decoding transactions stored to disk, such as in a
+// database, as opposed to decoding transactions from the wire.
+func (msg *MsgUtreexoTx) BtcDecode(r io.Reader, pver uint32, enc MessageEncoding) error {
+	// Decode the MsgTx.
+	var msgTx MsgTx
+	err := msgTx.BtcDecode(r, pver, enc)
+	if err != nil {
+		return err
+	}
+	msg.MsgTx = msgTx
+
+	// Decode the utreexo data.
+	ud := new(UData)
+	ud.LeafDatas = nil
+	err = ud.Deserialize(r)
+	if err != nil {
+		return err
+	}
+	msg.UData = *ud
+
+	return nil
+}
+
+// Deserialize decodes a transaction from r into the receiver using a format
+// that is suitable for long-term storage such as a database while respecting
+// the Version field in the transaction.  This function differs from BtcDecode
+// in that BtcDecode decodes from the bitcoin wire protocol as it was sent
+// across the network.  The wire encoding can technically differ depending on
+// the protocol version and doesn't even really need to match the format of a
+// stored transaction at all.  As of the time this comment was written, the
+// encoded transaction is the same in both instances, but there is a distinct
+// difference and separating the two allows the API to be flexible enough to
+// deal with changes.
+func (msg *MsgUtreexoTx) Deserialize(r io.Reader) error {
+	// At the current time, there is no difference between the wire encoding
+	// at protocol version 0 and the stable long-term storage format.  As
+	// a result, make use of BtcDecode.
+	return msg.BtcDecode(r, 0, WitnessEncoding)
+}
+
+// BtcEncode encodes the receiver to w using the bitcoin protocol encoding.
+// This is part of the Message interface implementation.
+// See Serialize for encoding transactions to be stored to disk, such as in a
+// database, as opposed to encoding transactions for the wire.
+func (msg *MsgUtreexoTx) BtcEncode(w io.Writer, pver uint32, enc MessageEncoding) error {
+	// Encode the msgTx.
+	err := msg.MsgTx.BtcEncode(w, pver, enc)
+	if err != nil {
+		return err
+	}
+
+	// Encode the utreexo data.
+	return msg.UData.Serialize(w)
+}
+
+// Command returns the protocol command string for the message.  This is part
+// of the Message interface implementation.
+func (msg *MsgUtreexoTx) Command() string {
+	return CmdUtreexoTx
+}
+
+// MaxPayloadLength returns the maximum length the payload can be for the
+// receiver.  This is part of the Message interface implementation.
+func (msg *MsgUtreexoTx) MaxPayloadLength(pver uint32) uint32 {
+	return MaxBlockPayload
+}
+
+// NewMsgUtreexoTx returns a new bitcoin utreexotx message that conforms to the
+// Message interface. The return instance has a default tx message and the udata
+// is initialized to the default values.
+func NewMsgUtreexoTx(version int32) *MsgUtreexoTx {
+	return &MsgUtreexoTx{
+		MsgTx: *NewMsgTx(1),
+	}
+}
diff --git a/wire/udata.go b/wire/udata.go
index d5a59c9a..bb0801b6 100644
--- a/wire/udata.go
+++ b/wire/udata.go
@@ -23,6 +23,28 @@ type UData struct {
 	LeafDatas []LeafData
 }
 
+// Copy creates a deep copy of the utreexo data so the original does not get modified
+// when the copy is manipulated.
+func (ud *UData) Copy() *UData {
+	proofCopy := utreexo.Proof{
+		Targets: make([]uint64, len(ud.AccProof.Targets)),
+		Proof:   make([]utreexo.Hash, len(ud.AccProof.Proof)),
+	}
+	copy(proofCopy.Targets, ud.AccProof.Targets)
+	copy(proofCopy.Proof, ud.AccProof.Proof)
+
+	newUD := UData{
+		AccProof:  proofCopy,
+		LeafDatas: make([]LeafData, len(ud.LeafDatas)),
+	}
+
+	for i := range newUD.LeafDatas {
+		newUD.LeafDatas[i] = *ud.LeafDatas[i].Copy()
+	}
+
+	return &newUD
+}
+
 // StxosHashes returns the hash of all stxos in this UData.  The hashes returned
 // here represent the hash commitments of the stxos.
 func (ud *UData) StxoHashes() []utreexo.Hash {
diff --git a/wire/udata_test.go b/wire/udata_test.go
index 3fa4821d..7fed4cc3 100644
--- a/wire/udata_test.go
+++ b/wire/udata_test.go
@@ -631,3 +631,47 @@ func TestGenerateUData(t *testing.T) {
 		t.Fatal(err)
 	}
 }
+
+// TestUDataCopy tests that modifying the leafdata copy does not modify the original.
+func TestUDataCopy(t *testing.T) {
+	// New forest object.
+	p := utreexo.NewAccumulator()
+
+	// Create hashes to add from the stxo data.
+	testDatas := getTestDatas()
+	addHashes := make([]utreexo.Leaf, 0, len(testDatas[0].leavesPerBlock))
+	for i, ld := range testDatas[0].leavesPerBlock {
+		addHashes = append(addHashes, utreexo.Leaf{
+			Hash: ld.LeafHash(),
+			// Just half and half.
+			Remember: i%2 == 0,
+		})
+	}
+	// Add to the accumulator.
+	err := p.Modify(addHashes, nil, utreexo.Proof{})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Generate Proof.
+	ud, err := GenerateUData(testDatas[0].leavesPerBlock, &p)
+	if err != nil {
+		t.Fatal(err)
+	}
+	udOrig, err := GenerateUData(testDatas[0].leavesPerBlock, &p)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	udCopy := ud.Copy()
+	udCopy.AccProof.Targets[0] = 1 << 17
+	udCopy.LeafDatas[0].Amount = 55
+
+	if reflect.DeepEqual(udCopy, ud) {
+		t.Fatalf("udCopy and ud are same")
+	}
+
+	if !reflect.DeepEqual(ud, udOrig) {
+		t.Fatalf("ud and udOrig are different")
+	}
+}