Skip to content

Commit

Permalink
*: add Copy() to transaction.Transaction and payload.P2PNotaryRequest
Browse files Browse the repository at this point in the history
Add a method that makes a deep copy of all fields and resets size/hash
caches.

Close #3288

Signed-off-by: Ekaterina Pavlova <[email protected]>
  • Loading branch information
AliceInHunterland committed Apr 27, 2024
1 parent 1292a00 commit 3422ed3
Show file tree
Hide file tree
Showing 19 changed files with 509 additions and 0 deletions.
16 changes: 16 additions & 0 deletions pkg/core/transaction/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type AttrValue interface {
// 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
}

// Attribute represents a Transaction attribute.
Expand Down Expand Up @@ -110,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 {
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}},
{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,
}
}
31 changes: 31 additions & 0 deletions pkg/core/transaction/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,34 @@ 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 {
return nil
}
cp := *c
if c.AllowedContracts != nil {
cp.AllowedContracts = make([]util.Uint160, len(c.AllowedContracts))
copy(cp.AllowedContracts, c.AllowedContracts)
}

if c.AllowedGroups != nil {
cp.AllowedGroups = make([]*keys.PublicKey, len(c.AllowedGroups))
for i, g := range c.AllowedGroups {
if g != nil {
cp.AllowedGroups[i] = new(keys.PublicKey)
*cp.AllowedGroups[i] = *g
}
}
}

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()

copyTx := origTx.Copy()

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()

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 {
return Witness{
InvocationScript: bytes.Clone(w.InvocationScript),
VerificationScript: bytes.Clone(w.VerificationScript),
}
}
Loading

0 comments on commit 3422ed3

Please sign in to comment.