Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

*: add Copy() to transaction.Transaction and payload.P2PNotaryRequest #3407

Merged
merged 5 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions pkg/core/transaction/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@ import (
"github.com/nspcc-dev/neo-go/pkg/io"
)

// AttrValue represents a Transaction Attribute value.
type AttrValue interface {
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
io.Serializable
// toJSONMap is used for embedded json struct marshalling.
// Anonymous interface fields are not considered anonymous by
// json lib and marshaling Value together with type makes code
// harder to follow.
toJSONMap(map[string]any)
// Copy returns a deep copy of the attribute value.
Copy() AttrValue
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
}

// Attribute represents a Transaction attribute.
type Attribute struct {
Type AttrType
Value interface {
io.Serializable
// toJSONMap is used for embedded json struct marshalling.
// Anonymous interface fields are not considered anonymous by
// json lib and marshaling Value together with type makes code
// harder to follow.
toJSONMap(map[string]any)
}
Value AttrValue
}

// attrJSON is used for JSON I/O of Attribute.
Expand Down Expand Up @@ -107,3 +112,17 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error {
}
return json.Unmarshal(data, attr.Value)
}

// Copy creates a deep copy of the Attribute.
func (attr *Attribute) Copy() *Attribute {
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
if attr == nil {
return nil
}
cp := &Attribute{
Type: attr.Type,
}
if attr.Value != nil {
cp.Value = attr.Value.Copy()
}
return cp
}
37 changes: 37 additions & 0 deletions pkg/core/transaction/attribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/nspcc-dev/neo-go/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -177,3 +178,39 @@ func TestAttribute_MarshalJSON(t *testing.T) {
testserdes.MarshalUnmarshalJSON(t, attr, new(Attribute))
})
}

func TestAttribute_Copy(t *testing.T) {
originals := []*Attribute{
{Type: HighPriority},
{Type: OracleResponseT, Value: &OracleResponse{ID: 123, Code: Success, Result: []byte{1, 2, 3}}},
{Type: NotValidBeforeT, Value: &NotValidBefore{Height: 123}},
{Type: ConflictsT, Value: &Conflicts{Hash: random.Uint256()}},
{Type: NotaryAssistedT, Value: &NotaryAssisted{NKeys: 3}},
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
{Type: ReservedLowerBound, Value: &Reserved{Value: []byte{1, 2, 3, 4, 5}}},
{Type: ReservedUpperBound, Value: &Reserved{Value: []byte{1, 2, 3, 4, 5}}},
{Type: ReservedLowerBound + 5, Value: &Reserved{Value: []byte{1, 2, 3, 4, 5}}},
}
require.Nil(t, (*Attribute)(nil).Copy())

for _, original := range originals {
cp := original.Copy()
require.NotNil(t, cp)
assert.Equal(t, original.Type, cp.Type)
assert.Equal(t, original.Value, cp.Value)
if original.Value != nil {
originalValueCopy := original.Value.Copy()
switch originalVal := originalValueCopy.(type) {
case *OracleResponse:
originalVal.Result = append(originalVal.Result, 0xFF)
assert.NotEqual(t, len(original.Value.(*OracleResponse).Result), len(originalVal.Result))
case *Conflicts:
originalVal.Hash = random.Uint256()
assert.NotEqual(t, original.Value.(*Conflicts).Hash, originalVal.Hash)
case *NotaryAssisted:
originalVal.NKeys++
assert.NotEqual(t, original.Value.(*NotaryAssisted).NKeys, originalVal.NKeys)
}
assert.Equal(t, cp.Value, original.Value)
}
}
}
7 changes: 7 additions & 0 deletions pkg/core/transaction/conflicts.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@ func (c *Conflicts) EncodeBinary(w *io.BinWriter) {
func (c *Conflicts) toJSONMap(m map[string]any) {
m["hash"] = c.Hash
}

// Copy implements the AttrValue interface.
func (c *Conflicts) Copy() AttrValue {
return &Conflicts{
Hash: c.Hash,
}
}
7 changes: 7 additions & 0 deletions pkg/core/transaction/not_valid_before.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ func (n *NotValidBefore) EncodeBinary(w *io.BinWriter) {
func (n *NotValidBefore) toJSONMap(m map[string]any) {
m["height"] = n.Height
}

// Copy implements the AttrValue interface.
func (n *NotValidBefore) Copy() AttrValue {
return &NotValidBefore{
Height: n.Height,
}
}
7 changes: 7 additions & 0 deletions pkg/core/transaction/notary_assisted.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ func (n *NotaryAssisted) EncodeBinary(w *io.BinWriter) {
func (n *NotaryAssisted) toJSONMap(m map[string]any) {
m["nkeys"] = n.NKeys
}

// Copy implements the AttrValue interface.
func (n *NotaryAssisted) Copy() AttrValue {
return &NotaryAssisted{
NKeys: n.NKeys,
}
}
10 changes: 10 additions & 0 deletions pkg/core/transaction/oracle.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package transaction

import (
"bytes"
"encoding/json"
"errors"
"math"
Expand Down Expand Up @@ -116,3 +117,12 @@ func (r *OracleResponse) toJSONMap(m map[string]any) {
m["code"] = r.Code
m["result"] = r.Result
}

// Copy implements the AttrValue interface.
func (r *OracleResponse) Copy() AttrValue {
return &OracleResponse{
ID: r.ID,
Code: r.Code,
Result: bytes.Clone(r.Result),
}
}
7 changes: 7 additions & 0 deletions pkg/core/transaction/reserved.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ func (e *Reserved) EncodeBinary(w *io.BinWriter) {
func (e *Reserved) toJSONMap(m map[string]any) {
m["value"] = e.Value
}

// Copy implements the AttrValue interface.
func (e *Reserved) Copy() AttrValue {
return &Reserved{
Value: e.Value,
}
}
21 changes: 21 additions & 0 deletions pkg/core/transaction/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,24 @@ func SignersToStackItem(signers []Signer) stackitem.Item {
}
return stackitem.NewArray(res)
}

// Copy creates a deep copy of the Signer.
func (c *Signer) Copy() *Signer {
if c == nil {
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
return nil
}
cp := *c
if c.AllowedContracts != nil {
cp.AllowedContracts = make([]util.Uint160, len(c.AllowedContracts))
copy(cp.AllowedContracts, c.AllowedContracts)
}
cp.AllowedGroups = keys.PublicKeys(c.AllowedGroups).Copy()
if c.Rules != nil {
cp.Rules = make([]WitnessRule, len(c.Rules))
for i, rule := range c.Rules {
cp.Rules[i] = *rule.Copy()
}
}

return &cp
}
37 changes: 37 additions & 0 deletions pkg/core/transaction/signer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,40 @@ func TestCosignerMarshallUnmarshallJSON(t *testing.T) {
actual := &Signer{}
testserdes.MarshalUnmarshalJSON(t, expected, actual)
}

func TestSignerCopy(t *testing.T) {
pk, err := keys.NewPrivateKey()
require.NoError(t, err)
require.Nil(t, (*Signer)(nil).Copy())

original := &Signer{
Account: util.Uint160{1, 2, 3, 4, 5},
Scopes: CustomContracts | CustomGroups | Rules,
AllowedContracts: []util.Uint160{{1, 2, 3, 4}, {6, 7, 8, 9}},
AllowedGroups: keys.PublicKeys{pk.PublicKey()},
Rules: []WitnessRule{{Action: WitnessAllow, Condition: ConditionCalledByEntry{}}},
}

cp := original.Copy()
require.NotNil(t, cp, "Copied Signer should not be nil")

require.Equal(t, original.Account, cp.Account)
require.Equal(t, original.Scopes, cp.Scopes)

require.NotSame(t, original.AllowedContracts, cp.AllowedContracts)
require.Equal(t, original.AllowedContracts, cp.AllowedContracts)

require.NotSame(t, original.AllowedGroups, cp.AllowedGroups)
require.Equal(t, original.AllowedGroups, cp.AllowedGroups)

require.NotSame(t, original.Rules, cp.Rules)
require.Equal(t, original.Rules, cp.Rules)

original.AllowedContracts[0][0] = 255
original.AllowedGroups[0] = nil
original.Rules[0].Action = WitnessDeny

require.NotEqual(t, original.AllowedContracts[0][0], cp.AllowedContracts[0][0])
require.NotEqual(t, original.AllowedGroups[0], cp.AllowedGroups[0])
require.NotEqual(t, original.Rules[0].Action, cp.Rules[0].Action)
}
34 changes: 34 additions & 0 deletions pkg/core/transaction/transaction.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package transaction

import (
"bytes"
"crypto/sha256"
"encoding/json"
"errors"
Expand Down Expand Up @@ -468,3 +469,36 @@ func (t *Transaction) ToStackItem() stackitem.Item {
stackitem.NewByteArray(t.Script),
})
}

// Copy creates a deep copy of the Transaction, including all slice fields. Cached values like
// 'hashed' and 'size' are reset to ensure the copy can be modified independently of the original.
func (t *Transaction) Copy() *Transaction {
if t == nil {
return nil
}
cp := *t
if t.Attributes != nil {
cp.Attributes = make([]Attribute, len(t.Attributes))
for i, attr := range t.Attributes {
cp.Attributes[i] = *attr.Copy()
}
}
if t.Signers != nil {
cp.Signers = make([]Signer, len(t.Signers))
for i, signer := range t.Signers {
cp.Signers[i] = *signer.Copy()
}
}
if t.Scripts != nil {
cp.Scripts = make([]Witness, len(t.Scripts))
for i, script := range t.Scripts {
cp.Scripts[i] = script.Copy()
}
}
cp.Script = bytes.Clone(t.Script)

cp.hashed = false
cp.size = 0
cp.hash = util.Uint256{}
return &cp
}
66 changes: 66 additions & 0 deletions pkg/core/transaction/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
Expand Down Expand Up @@ -321,3 +322,68 @@ func BenchmarkTxHash(b *testing.B) {
_ = tx.Hash()
}
}

func TestTransaction_DeepCopy(t *testing.T) {
origTx := New([]byte{0x01, 0x02, 0x03}, 1000)
origTx.NetworkFee = 2000
origTx.SystemFee = 500
origTx.Nonce = 12345678
origTx.ValidUntilBlock = 100
origTx.Version = 1
require.Nil(t, (*Transaction)(nil).Copy())

priv, err := keys.NewPrivateKey()
require.NoError(t, err)
origTx.Signers = []Signer{
{Account: random.Uint160(), Scopes: Global, AllowedContracts: []util.Uint160{random.Uint160()}, AllowedGroups: keys.PublicKeys{priv.PublicKey()}, Rules: []WitnessRule{{Action: 0x01, Condition: ConditionCalledByEntry{}}}},
{Account: random.Uint160(), Scopes: CalledByEntry},
}
origTx.Attributes = []Attribute{
{Type: HighPriority, Value: &OracleResponse{
ID: 0,
Code: Success,
Result: []byte{4, 8, 15, 16, 23, 42},
}},
}
origTx.Scripts = []Witness{
{
InvocationScript: []byte{0x04, 0x05},
VerificationScript: []byte{0x06, 0x07},
},
}
origTxHash := origTx.Hash()
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved

copyTx := origTx.Copy()
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved

require.Equal(t, origTx.Hash(), copyTx.Hash())
require.Equal(t, origTx, copyTx)
require.Equal(t, origTx.Size(), copyTx.Size())

copyTx.NetworkFee = 3000
copyTx.Signers[0].Scopes = None
copyTx.Attributes[0].Type = NotaryAssistedT
copyTx.Scripts[0].InvocationScript[0] = 0x08
copyTx.hashed = false
modifiedCopyTxHash := copyTx.Hash()

AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
require.NotEqual(t, origTx.NetworkFee, copyTx.NetworkFee)
require.NotEqual(t, origTx.Signers[0].Scopes, copyTx.Signers[0].Scopes)
require.NotEqual(t, origTx.Attributes, copyTx.Attributes)
require.NotEqual(t, origTxHash, modifiedCopyTxHash)

require.NotEqual(t, &origTx.Scripts[0].InvocationScript[0], &copyTx.Scripts[0].InvocationScript[0])
require.NotEqual(t, &origTx.Scripts, &copyTx.Scripts)
require.Equal(t, origTx.Scripts[0].VerificationScript, copyTx.Scripts[0].VerificationScript)
require.Equal(t, origTx.Signers[0].AllowedContracts, copyTx.Signers[0].AllowedContracts)
require.Equal(t, origTx.Signers[0].AllowedGroups, copyTx.Signers[0].AllowedGroups)
origGroup := origTx.Signers[0].AllowedGroups[0]
copyGroup := copyTx.Signers[0].AllowedGroups[0]
require.True(t, origGroup.Equal(copyGroup))

copyTx.Signers[0].AllowedGroups[0] = nil
require.NotEqual(t, origTx.Signers[0].AllowedGroups[0], copyTx.Signers[0].AllowedGroups[0])

require.Equal(t, origTx.Signers[0].Rules[0], copyTx.Signers[0].Rules[0])
copyTx.Signers[0].Rules[0].Action = 0x02
require.NotEqual(t, origTx.Signers[0].Rules[0].Action, copyTx.Signers[0].Rules[0].Action)
}
10 changes: 10 additions & 0 deletions pkg/core/transaction/witness.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package transaction

import (
"bytes"

"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"
Expand Down Expand Up @@ -38,3 +40,11 @@ func (w *Witness) EncodeBinary(bw *io.BinWriter) {
func (w Witness) ScriptHash() util.Uint160 {
return hash.Hash160(w.VerificationScript)
}

// Copy creates a deep copy of the Witness.
func (w Witness) Copy() Witness {
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
return Witness{
InvocationScript: bytes.Clone(w.InvocationScript),
VerificationScript: bytes.Clone(w.VerificationScript),
}
}
Loading
Loading