diff --git a/CHANGELOG.md b/CHANGELOG.md index 91bc8dfb58..02f9188fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve - Add ability to rollback node's internal state during processing. - Change how unsafe protobuf state is created to prevent unnecessary copies. - Added benchmarks for process slots for Capella, Deneb, Electra +- Add helper to cast bytes to string without allocating memory. ### Changed diff --git a/beacon-chain/p2p/message_id.go b/beacon-chain/p2p/message_id.go index 9ea7091a86..28355f9197 100644 --- a/beacon-chain/p2p/message_id.go +++ b/beacon-chain/p2p/message_id.go @@ -29,7 +29,7 @@ func MsgID(genesisValidatorsRoot []byte, pmsg *pubsubpb.Message) string { // never be hit. msg := make([]byte, 20) copy(msg, "invalid") - return string(msg) + return bytesutil.UnsafeCastToString(msg) } digest, err := ExtractGossipDigest(*pmsg.Topic) if err != nil { @@ -37,7 +37,7 @@ func MsgID(genesisValidatorsRoot []byte, pmsg *pubsubpb.Message) string { // never be hit. msg := make([]byte, 20) copy(msg, "invalid") - return string(msg) + return bytesutil.UnsafeCastToString(msg) } _, fEpoch, err := forks.RetrieveForkDataFromDigest(digest, genesisValidatorsRoot) if err != nil { @@ -45,7 +45,7 @@ func MsgID(genesisValidatorsRoot []byte, pmsg *pubsubpb.Message) string { // never be hit. msg := make([]byte, 20) copy(msg, "invalid") - return string(msg) + return bytesutil.UnsafeCastToString(msg) } if fEpoch >= params.BeaconConfig().AltairForkEpoch { return postAltairMsgID(pmsg, fEpoch) @@ -54,11 +54,11 @@ func MsgID(genesisValidatorsRoot []byte, pmsg *pubsubpb.Message) string { if err != nil { combinedData := append(params.BeaconConfig().MessageDomainInvalidSnappy[:], pmsg.Data...) h := hash.Hash(combinedData) - return string(h[:20]) + return bytesutil.UnsafeCastToString(h[:20]) } combinedData := append(params.BeaconConfig().MessageDomainValidSnappy[:], decodedData...) h := hash.Hash(combinedData) - return string(h[:20]) + return bytesutil.UnsafeCastToString(h[:20]) } // Spec: @@ -93,13 +93,13 @@ func postAltairMsgID(pmsg *pubsubpb.Message, fEpoch primitives.Epoch) string { // should never happen msg := make([]byte, 20) copy(msg, "invalid") - return string(msg) + return bytesutil.UnsafeCastToString(msg) } if uint64(totalLength) > gossipPubSubSize { // this should never happen msg := make([]byte, 20) copy(msg, "invalid") - return string(msg) + return bytesutil.UnsafeCastToString(msg) } combinedData := make([]byte, 0, totalLength) combinedData = append(combinedData, params.BeaconConfig().MessageDomainInvalidSnappy[:]...) @@ -107,7 +107,7 @@ func postAltairMsgID(pmsg *pubsubpb.Message, fEpoch primitives.Epoch) string { combinedData = append(combinedData, topic...) combinedData = append(combinedData, pmsg.Data...) h := hash.Hash(combinedData) - return string(h[:20]) + return bytesutil.UnsafeCastToString(h[:20]) } totalLength, err := math.AddInt( len(params.BeaconConfig().MessageDomainValidSnappy), @@ -120,7 +120,7 @@ func postAltairMsgID(pmsg *pubsubpb.Message, fEpoch primitives.Epoch) string { // should never happen msg := make([]byte, 20) copy(msg, "invalid") - return string(msg) + return bytesutil.UnsafeCastToString(msg) } combinedData := make([]byte, 0, totalLength) combinedData = append(combinedData, params.BeaconConfig().MessageDomainValidSnappy[:]...) @@ -128,5 +128,5 @@ func postAltairMsgID(pmsg *pubsubpb.Message, fEpoch primitives.Epoch) string { combinedData = append(combinedData, topic...) combinedData = append(combinedData, decodedData...) h := hash.Hash(combinedData) - return string(h[:20]) + return bytesutil.UnsafeCastToString(h[:20]) } diff --git a/encoding/bytesutil/bytes.go b/encoding/bytesutil/bytes.go index 5faebbf526..7dbd6f5609 100644 --- a/encoding/bytesutil/bytes.go +++ b/encoding/bytesutil/bytes.go @@ -3,6 +3,7 @@ package bytesutil import ( "fmt" + "unsafe" "github.com/ethereum/go-ethereum/common/hexutil" ) @@ -145,3 +146,10 @@ func ReverseByteOrder(input []byte) []byte { } return b } + +// UnsafeCastToString casts a byte slice to a string object without performing a copy. Changes +// to byteSlice will also modify the contents of the string, so it is the caller's responsibility +// to ensure that the byte slice will not modified after the string is created. +func UnsafeCastToString(byteSlice []byte) string { + return *(*string)(unsafe.Pointer(&byteSlice)) // #nosec G103 +} diff --git a/encoding/bytesutil/bytes_test.go b/encoding/bytesutil/bytes_test.go index feffc01d31..6e7c4cfcac 100644 --- a/encoding/bytesutil/bytes_test.go +++ b/encoding/bytesutil/bytes_test.go @@ -217,6 +217,50 @@ func TestToBytes20(t *testing.T) { } } +func TestCastToString(t *testing.T) { + bSlice := []byte{'a', 'b', 'c'} + bString := bytesutil.UnsafeCastToString(bSlice) + + originalString := "abc" + + // Mutate original slice to make sure that a copy was not performed. + bSlice[0] = 'd' + assert.NotEqual(t, originalString, bString) + assert.Equal(t, "dbc", bString) +} + +func BenchmarkUnsafeCastToString(b *testing.B) { + data := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + empty := []byte{} + var nilData []byte + + b.Run("string(b)", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = string(data) + _ = string(empty) + _ = string(nilData) + } + }) + + b.Run("bytesutil.UnsafeCastToString(b)", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = bytesutil.UnsafeCastToString(data) + _ = bytesutil.UnsafeCastToString(empty) + _ = bytesutil.UnsafeCastToString(nilData) + } + }) +} + +func FuzzUnsafeCastToString(f *testing.F) { + f.Fuzz(func(t *testing.T, input []byte) { + want := string(input) + result := bytesutil.UnsafeCastToString(input) + if result != want { + t.Fatalf("input (%v) result (%s) did not match expected (%s)", input, result, want) + } + }) +} + func BenchmarkToBytes32(b *testing.B) { x := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31} for i := 0; i < b.N; i++ {