diff --git a/pkg/network/payload/extensible.go b/pkg/network/payload/extensible.go new file mode 100644 index 000000000..7df9ffd33 --- /dev/null +++ b/pkg/network/payload/extensible.go @@ -0,0 +1,126 @@ +package payload + +import ( + "errors" + "math" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +const ( + maxExtensibleCategorySize = 32 + maxExtensibleDataSize = math.MaxUint16 +) + +// Extensible represents payload containing arbitrary data. +type Extensible struct { + // Network represents network magic. + Network netmode.Magic + // Category is payload type. + Category string + // ValidBlockStart is starting height for payload to be valid. + ValidBlockStart uint32 + // ValidBlockEnd is height after which payload becomes invalid. + ValidBlockEnd uint32 + // Sender is payload sender or signer. + Sender util.Uint160 + // Data is custom payload data. + Data []byte + // Witness is payload witness. + Witness transaction.Witness + + hash util.Uint256 + signedHash util.Uint256 + signedpart []byte +} + +var errInvalidPadding = errors.New("invalid padding") + +// NewExtensible creates new extensible payload. +func NewExtensible(network netmode.Magic) *Extensible { + return &Extensible{Network: network} +} + +func (e *Extensible) encodeBinaryUnsigned(w *io.BinWriter) { + w.WriteString(e.Category) + w.WriteU32LE(e.ValidBlockStart) + w.WriteU32LE(e.ValidBlockEnd) + w.WriteBytes(e.Sender[:]) + w.WriteVarBytes(e.Data) +} + +// EncodeBinary implements io.Serializable. +func (e *Extensible) EncodeBinary(w *io.BinWriter) { + e.encodeBinaryUnsigned(w) + w.WriteB(1) + e.Witness.EncodeBinary(w) +} + +func (e *Extensible) decodeBinaryUnsigned(r *io.BinReader) { + e.Category = r.ReadString(maxExtensibleCategorySize) + e.ValidBlockStart = r.ReadU32LE() + e.ValidBlockEnd = r.ReadU32LE() + r.ReadBytes(e.Sender[:]) + e.Data = r.ReadVarBytes(maxExtensibleDataSize) +} + +// DecodeBinary implements io.Serializable. +func (e *Extensible) DecodeBinary(r *io.BinReader) { + e.decodeBinaryUnsigned(r) + if r.ReadB() != 1 { + if r.Err != nil { + return + } + r.Err = errInvalidPadding + return + } + e.Witness.DecodeBinary(r) +} + +// GetSignedPart implements crypto.Verifiable. +func (e *Extensible) GetSignedPart() []byte { + if e.signedpart == nil { + e.updateSignedPart() + } + return e.signedpart +} + +// GetSignedHash implements crypto.Verifiable. +func (e *Extensible) GetSignedHash() util.Uint256 { + if e.signedHash.Equals(util.Uint256{}) { + e.createHash() + } + return e.signedHash +} + +// Hash returns payload hash. +func (e *Extensible) Hash() util.Uint256 { + if e.hash.Equals(util.Uint256{}) { + e.createHash() + } + return e.hash +} + +// createHash creates hashes of the payload. +func (e *Extensible) createHash() { + b := e.GetSignedPart() + e.updateHashes(b) +} + +// updateHashes updates hashes based on the given buffer which should +// be a signable data slice. +func (e *Extensible) updateHashes(b []byte) { + e.signedHash = hash.Sha256(b) + e.hash = hash.Sha256(e.signedHash.BytesBE()) +} + +// updateSignedPart updates serialized message if needed. +func (e *Extensible) updateSignedPart() { + w := io.NewBufBinWriter() + e.encodeBinaryUnsigned(w.BinWriter) + e.signedpart = w.Bytes() +} diff --git a/pkg/network/payload/extensible_test.go b/pkg/network/payload/extensible_test.go new file mode 100644 index 000000000..846ed676c --- /dev/null +++ b/pkg/network/payload/extensible_test.go @@ -0,0 +1,71 @@ +package payload + +import ( + "errors" + gio "io" + "testing" + + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/stretchr/testify/require" +) + +func TestExtensible_Serializable(t *testing.T) { + expected := &Extensible{ + Category: "test", + ValidBlockStart: 12, + ValidBlockEnd: 1234, + Sender: random.Uint160(), + Data: random.Bytes(4), + Witness: transaction.Witness{ + InvocationScript: random.Bytes(3), + VerificationScript: random.Bytes(3), + }, + } + + testserdes.EncodeDecodeBinary(t, expected, new(Extensible)) + + t.Run("invalid", func(t *testing.T) { + w := io.NewBufBinWriter() + expected.encodeBinaryUnsigned(w.BinWriter) + unsigned := w.Bytes() + + t.Run("unexpected EOF", func(t *testing.T) { + err := testserdes.DecodeBinary(unsigned, new(Extensible)) + require.True(t, errors.Is(err, gio.EOF)) + }) + t.Run("invalid padding", func(t *testing.T) { + err := testserdes.DecodeBinary(append(unsigned, 42), new(Extensible)) + require.True(t, errors.Is(err, errInvalidPadding)) + }) + }) +} + +func TestExtensible_Hashes(t *testing.T) { + getExtensiblePair := func() (*Extensible, *Extensible) { + p1 := NewExtensible(netmode.UnitTestNet) + p1.Data = []byte{1, 2, 3} + p2 := NewExtensible(netmode.UnitTestNet) + p2.Data = []byte{3, 2, 1} + return p1, p2 + } + + t.Run("GetSignedPart", func(t *testing.T) { + p1, p2 := getExtensiblePair() + require.NotEqual(t, p1.GetSignedPart(), p2.GetSignedPart()) + require.NotEqual(t, p1.GetSignedPart(), p2.GetSignedPart()) + }) + t.Run("GetSignedHash", func(t *testing.T) { + p1, p2 := getExtensiblePair() + require.NotEqual(t, p1.GetSignedHash(), p2.GetSignedHash()) + require.NotEqual(t, p1.GetSignedHash(), p2.GetSignedHash()) + }) + t.Run("Hash", func(t *testing.T) { + p1, p2 := getExtensiblePair() + require.NotEqual(t, p1.Hash(), p2.Hash()) + require.NotEqual(t, p1.Hash(), p2.Hash()) + }) +}