Skip to content

Commit

Permalink
Merge pull request #3407 from nspcc-dev/copy-transaction
Browse files Browse the repository at this point in the history
*: add Copy() to transaction.Transaction and payload.P2PNotaryRequest
  • Loading branch information
AnnaShaleva authored May 1, 2024
2 parents 198af7f + 8da7c0a commit b54fcbc
Show file tree
Hide file tree
Showing 22 changed files with 526 additions and 29 deletions.
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 {
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
}

// 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 {
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,
}
}
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 {
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()

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 b54fcbc

Please sign in to comment.