core: add getTransactionSigners method to native Ledger

This commit is contained in:
Anna Shaleva 2022-04-21 19:19:24 +03:00
parent 35d930951c
commit 47d52bd9c5
6 changed files with 237 additions and 17 deletions

View file

@ -10,6 +10,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
@ -63,6 +64,11 @@ func newLedger() *Ledger {
md = newMethodAndPrice(l.getTransactionFromBlock, 1<<16, callflag.ReadStates) md = newMethodAndPrice(l.getTransactionFromBlock, 1<<16, callflag.ReadStates)
l.AddMethod(md, desc) l.AddMethod(md, desc)
desc = newDescriptor("getTransactionSigners", smartcontract.ArrayType,
manifest.NewParameter("hash", smartcontract.Hash256Type))
md = newMethodAndPrice(l.getTransactionSigners, 1<<15, callflag.ReadStates)
l.AddMethod(md, desc)
desc = newDescriptor("getTransactionVMState", smartcontract.IntegerType, desc = newDescriptor("getTransactionVMState", smartcontract.IntegerType,
manifest.NewParameter("hash", smartcontract.Hash256Type)) manifest.NewParameter("hash", smartcontract.Hash256Type))
md = newMethodAndPrice(l.getTransactionVMState, 1<<15, callflag.ReadStates) md = newMethodAndPrice(l.getTransactionVMState, 1<<15, callflag.ReadStates)
@ -148,6 +154,15 @@ func (l *Ledger) getTransactionFromBlock(ic *interop.Context, params []stackitem
return TransactionToStackItem(block.Transactions[index]) return TransactionToStackItem(block.Transactions[index])
} }
// getTransactionSigners returns transaction signers to the SC.
func (l *Ledger) getTransactionSigners(ic *interop.Context, params []stackitem.Item) stackitem.Item {
tx, h, err := getTransactionAndHeight(ic.DAO, params[0])
if err != nil || !isTraceableBlock(ic.Chain, h) {
return stackitem.Null{}
}
return SignersToStackItem(tx.Signers)
}
// getTransactionVMState returns VM state got after transaction invocation. // getTransactionVMState returns VM state got after transaction invocation.
func (l *Ledger) getTransactionVMState(ic *interop.Context, params []stackitem.Item) stackitem.Item { func (l *Ledger) getTransactionVMState(ic *interop.Context, params []stackitem.Item) stackitem.Item {
hash, err := getUint256FromItem(params[0]) hash, err := getUint256FromItem(params[0])
@ -242,3 +257,37 @@ func TransactionToStackItem(t *transaction.Transaction) stackitem.Item {
stackitem.NewByteArray(t.Script), stackitem.NewByteArray(t.Script),
}) })
} }
// SignersToStackItem converts transaction.Signers to stackitem.Item.
func SignersToStackItem(signers []transaction.Signer) stackitem.Item {
res := make([]stackitem.Item, len(signers))
bw := io.NewBufBinWriter()
for i, s := range signers {
s.EncodeBinary(bw.BinWriter)
if bw.Err != nil {
panic(fmt.Errorf("failed to serialize signer %d to stackitem: %w", i, bw.Err))
}
contracts := make([]stackitem.Item, len(s.AllowedContracts))
for j, c := range s.AllowedContracts {
contracts[j] = stackitem.NewByteArray(c.BytesBE())
}
groups := make([]stackitem.Item, len(s.AllowedGroups))
for j, g := range s.AllowedGroups {
groups[j] = stackitem.NewByteArray(g.Bytes())
}
rules := make([]stackitem.Item, len(s.Rules))
for j, r := range s.Rules {
rules[j] = r.ToStackItem()
}
res[i] = stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(bw.Bytes()),
stackitem.NewByteArray(s.Account.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(s.Scopes))),
stackitem.NewArray(contracts),
stackitem.NewArray(groups),
stackitem.NewArray(rules),
})
bw.Reset()
}
return stackitem.NewArray(res)
}

View file

@ -6,6 +6,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
@ -172,3 +174,38 @@ func TestLedger_GetBlock(t *testing.T) {
ledgerInvoker.Invoke(t, stackitem.Null{}, "getBlock", b.Hash()) ledgerInvoker.Invoke(t, stackitem.Null{}, "getBlock", b.Hash())
}) })
} }
func TestLedger_GetTransactionSigners(t *testing.T) {
c := newLedgerClient(t)
e := c.Executor
ledgerInvoker := c.WithSigners(c.Committee)
txHash := ledgerInvoker.Invoke(t, e.Chain.BlockHeight(), "currentIndex")
t.Run("good", func(t *testing.T) {
s := &transaction.Signer{
Account: c.CommitteeHash,
Scopes: transaction.Global,
}
bw := io.NewBufBinWriter()
s.EncodeBinary(bw.BinWriter)
require.NoError(t, bw.Err)
expected := stackitem.NewArray([]stackitem.Item{
stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(bw.Bytes()),
stackitem.NewByteArray(s.Account.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(s.Scopes))),
stackitem.NewArray([]stackitem.Item{}),
stackitem.NewArray([]stackitem.Item{}),
stackitem.NewArray([]stackitem.Item{}),
}),
})
ledgerInvoker.Invoke(t, expected, "getTransactionSigners", txHash)
})
t.Run("unknown transaction", func(t *testing.T) {
ledgerInvoker.Invoke(t, stackitem.Null{}, "getTransactionSigners", util.Uint256{1, 2, 3})
})
t.Run("not a hash", func(t *testing.T) {
ledgerInvoker.InvokeFail(t, "expected []byte of size 32", "getTransactionSigners", []byte{1, 2, 3})
})
}

View file

@ -3,10 +3,12 @@ package transaction
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"math/big"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
) )
//go:generate stringer -type=WitnessConditionType -linecomment //go:generate stringer -type=WitnessConditionType -linecomment
@ -50,6 +52,8 @@ type WitnessCondition interface {
// DecodeBinarySpecific decodes type-specific binary data from the given // DecodeBinarySpecific decodes type-specific binary data from the given
// reader (not including type data). // reader (not including type data).
DecodeBinarySpecific(*io.BinReader, int) DecodeBinarySpecific(*io.BinReader, int)
// ToStackItem converts WitnessCondition to stackitem.Item.
ToStackItem() stackitem.Item
json.Marshaler json.Marshaler
} }
@ -129,6 +133,12 @@ func (c *ConditionBoolean) MarshalJSON() ([]byte, error) {
return json.Marshal(aux) return json.Marshal(aux)
} }
// ToStackItem implements WitnessCondition interface allowing to convert
// to stackitem.Item.
func (c *ConditionBoolean) ToStackItem() stackitem.Item {
return condToStackItem(c.Type(), bool(*c))
}
// Type implements WitnessCondition interface and returns condition type. // Type implements WitnessCondition interface and returns condition type.
func (c *ConditionNot) Type() WitnessConditionType { func (c *ConditionNot) Type() WitnessConditionType {
return WitnessNot return WitnessNot
@ -166,6 +176,12 @@ func (c *ConditionNot) MarshalJSON() ([]byte, error) {
return json.Marshal(aux) return json.Marshal(aux)
} }
// ToStackItem implements WitnessCondition interface allowing to convert
// to stackitem.Item.
func (c *ConditionNot) ToStackItem() stackitem.Item {
return condToStackItem(c.Type(), c.Condition)
}
// Type implements WitnessCondition interface and returns condition type. // Type implements WitnessCondition interface and returns condition type.
func (c *ConditionAnd) Type() WitnessConditionType { func (c *ConditionAnd) Type() WitnessConditionType {
return WitnessAnd return WitnessAnd
@ -242,6 +258,12 @@ func (c *ConditionAnd) MarshalJSON() ([]byte, error) {
return arrayToJSON(c, []WitnessCondition(*c)) return arrayToJSON(c, []WitnessCondition(*c))
} }
// ToStackItem implements WitnessCondition interface allowing to convert
// to stackitem.Item.
func (c *ConditionAnd) ToStackItem() stackitem.Item {
return condToStackItem(c.Type(), []WitnessCondition(*c))
}
// Type implements WitnessCondition interface and returns condition type. // Type implements WitnessCondition interface and returns condition type.
func (c *ConditionOr) Type() WitnessConditionType { func (c *ConditionOr) Type() WitnessConditionType {
return WitnessOr return WitnessOr
@ -282,6 +304,12 @@ func (c *ConditionOr) MarshalJSON() ([]byte, error) {
return arrayToJSON(c, []WitnessCondition(*c)) return arrayToJSON(c, []WitnessCondition(*c))
} }
// ToStackItem implements WitnessCondition interface allowing to convert
// to stackitem.Item.
func (c *ConditionOr) ToStackItem() stackitem.Item {
return condToStackItem(c.Type(), []WitnessCondition(*c))
}
// Type implements WitnessCondition interface and returns condition type. // Type implements WitnessCondition interface and returns condition type.
func (c *ConditionScriptHash) Type() WitnessConditionType { func (c *ConditionScriptHash) Type() WitnessConditionType {
return WitnessScriptHash return WitnessScriptHash
@ -314,6 +342,12 @@ func (c *ConditionScriptHash) MarshalJSON() ([]byte, error) {
return json.Marshal(aux) return json.Marshal(aux)
} }
// ToStackItem implements WitnessCondition interface allowing to convert
// to stackitem.Item.
func (c *ConditionScriptHash) ToStackItem() stackitem.Item {
return condToStackItem(c.Type(), util.Uint160(*c))
}
// Type implements WitnessCondition interface and returns condition type. // Type implements WitnessCondition interface and returns condition type.
func (c *ConditionGroup) Type() WitnessConditionType { func (c *ConditionGroup) Type() WitnessConditionType {
return WitnessGroup return WitnessGroup
@ -346,6 +380,12 @@ func (c *ConditionGroup) MarshalJSON() ([]byte, error) {
return json.Marshal(aux) return json.Marshal(aux)
} }
// ToStackItem implements WitnessCondition interface allowing to convert
// to stackitem.Item.
func (c *ConditionGroup) ToStackItem() stackitem.Item {
return condToStackItem(c.Type(), keys.PublicKey(*c))
}
// Type implements WitnessCondition interface and returns condition type. // Type implements WitnessCondition interface and returns condition type.
func (c ConditionCalledByEntry) Type() WitnessConditionType { func (c ConditionCalledByEntry) Type() WitnessConditionType {
return WitnessCalledByEntry return WitnessCalledByEntry
@ -376,6 +416,12 @@ func (c ConditionCalledByEntry) MarshalJSON() ([]byte, error) {
return json.Marshal(aux) return json.Marshal(aux)
} }
// ToStackItem implements WitnessCondition interface allowing to convert
// to stackitem.Item.
func (c ConditionCalledByEntry) ToStackItem() stackitem.Item {
return condToStackItem(c.Type(), nil)
}
// Type implements WitnessCondition interface and returns condition type. // Type implements WitnessCondition interface and returns condition type.
func (c *ConditionCalledByContract) Type() WitnessConditionType { func (c *ConditionCalledByContract) Type() WitnessConditionType {
return WitnessCalledByContract return WitnessCalledByContract
@ -408,6 +454,12 @@ func (c *ConditionCalledByContract) MarshalJSON() ([]byte, error) {
return json.Marshal(aux) return json.Marshal(aux)
} }
// ToStackItem implements WitnessCondition interface allowing to convert
// to stackitem.Item.
func (c *ConditionCalledByContract) ToStackItem() stackitem.Item {
return condToStackItem(c.Type(), util.Uint160(*c))
}
// Type implements WitnessCondition interface and returns condition type. // Type implements WitnessCondition interface and returns condition type.
func (c *ConditionCalledByGroup) Type() WitnessConditionType { func (c *ConditionCalledByGroup) Type() WitnessConditionType {
return WitnessCalledByGroup return WitnessCalledByGroup
@ -440,6 +492,12 @@ func (c *ConditionCalledByGroup) MarshalJSON() ([]byte, error) {
return json.Marshal(aux) return json.Marshal(aux)
} }
// ToStackItem implements WitnessCondition interface allowing to convert
// to stackitem.Item.
func (c *ConditionCalledByGroup) ToStackItem() stackitem.Item {
return condToStackItem(c.Type(), keys.PublicKey(*c))
}
// DecodeBinaryCondition decodes and returns condition from the given binary stream. // DecodeBinaryCondition decodes and returns condition from the given binary stream.
func DecodeBinaryCondition(r *io.BinReader) WitnessCondition { func DecodeBinaryCondition(r *io.BinReader) WitnessCondition {
return decodeBinaryCondition(r, MaxConditionNesting) return decodeBinaryCondition(r, MaxConditionNesting)
@ -573,3 +631,29 @@ func unmarshalConditionJSON(data []byte, maxDepth int) (WitnessCondition, error)
} }
return res, nil return res, nil
} }
func condToStackItem(typ WitnessConditionType, c interface{}) stackitem.Item {
res := make([]stackitem.Item, 0, 2)
res = append(res, stackitem.NewBigInteger(big.NewInt(int64(typ))))
switch typ {
case WitnessBoolean:
res = append(res, stackitem.NewBool(c.(bool)))
case WitnessNot:
res = append(res, c.(WitnessCondition).ToStackItem())
case WitnessAnd, WitnessOr:
v := c.([]WitnessCondition)
operands := make([]stackitem.Item, len(v))
for i, op := range v {
operands[i] = op.ToStackItem()
}
res = append(res, stackitem.NewArray(operands))
case WitnessScriptHash, WitnessCalledByContract:
res = append(res, stackitem.NewByteArray(c.(util.Uint160).BytesBE()))
case WitnessGroup, WitnessCalledByGroup:
g := c.(keys.PublicKey)
res = append(res, stackitem.NewByteArray((&g).Bytes()))
case WitnessCalledByEntry:
// No additional item should be added.
}
return stackitem.NewArray(res)
}

View file

@ -8,6 +8,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -30,10 +32,14 @@ func (c InvalidCondition) MarshalJSON() ([]byte, error) {
} }
return json.Marshal(aux) return json.Marshal(aux)
} }
func (c InvalidCondition) ToStackItem() stackitem.Item {
panic("invalid")
}
type condCase struct { type condCase struct {
condition WitnessCondition condition WitnessCondition
success bool success bool
expectedStackItem []stackitem.Item
} }
func TestWitnessConditionSerDes(t *testing.T) { func TestWitnessConditionSerDes(t *testing.T) {
@ -41,19 +47,25 @@ func TestWitnessConditionSerDes(t *testing.T) {
pk, err := keys.NewPrivateKey() pk, err := keys.NewPrivateKey()
require.NoError(t, err) require.NoError(t, err)
var cases = []condCase{ var cases = []condCase{
{(*ConditionBoolean)(&someBool), true}, {(*ConditionBoolean)(&someBool), true, []stackitem.Item{stackitem.Make(WitnessBoolean), stackitem.Make(someBool)}},
{&ConditionNot{(*ConditionBoolean)(&someBool)}, true}, {&ConditionNot{(*ConditionBoolean)(&someBool)}, true, []stackitem.Item{stackitem.Make(WitnessNot), stackitem.NewArray([]stackitem.Item{stackitem.Make(WitnessBoolean), stackitem.Make(someBool)})}},
{&ConditionAnd{(*ConditionBoolean)(&someBool), (*ConditionBoolean)(&someBool)}, true}, {&ConditionAnd{(*ConditionBoolean)(&someBool), (*ConditionBoolean)(&someBool)}, true, []stackitem.Item{stackitem.Make(WitnessAnd), stackitem.Make([]stackitem.Item{
{&ConditionOr{(*ConditionBoolean)(&someBool), (*ConditionBoolean)(&someBool)}, true}, stackitem.NewArray([]stackitem.Item{stackitem.Make(WitnessBoolean), stackitem.Make(someBool)}),
{&ConditionScriptHash{1, 2, 3}, true}, stackitem.NewArray([]stackitem.Item{stackitem.Make(WitnessBoolean), stackitem.Make(someBool)}),
{(*ConditionGroup)(pk.PublicKey()), true}, })}},
{ConditionCalledByEntry{}, true}, {&ConditionOr{(*ConditionBoolean)(&someBool), (*ConditionBoolean)(&someBool)}, true, []stackitem.Item{stackitem.Make(WitnessOr), stackitem.Make([]stackitem.Item{
{&ConditionCalledByContract{1, 2, 3}, true}, stackitem.NewArray([]stackitem.Item{stackitem.Make(WitnessBoolean), stackitem.Make(someBool)}),
{(*ConditionCalledByGroup)(pk.PublicKey()), true}, stackitem.NewArray([]stackitem.Item{stackitem.Make(WitnessBoolean), stackitem.Make(someBool)}),
{InvalidCondition{}, false}, })}},
{&ConditionAnd{}, false}, {&ConditionScriptHash{1, 2, 3}, true, []stackitem.Item{stackitem.Make(WitnessScriptHash), stackitem.Make(util.Uint160{1, 2, 3}.BytesBE())}},
{&ConditionOr{}, false}, {(*ConditionGroup)(pk.PublicKey()), true, []stackitem.Item{stackitem.Make(WitnessGroup), stackitem.Make(pk.PublicKey().Bytes())}},
{&ConditionNot{&ConditionNot{&ConditionNot{(*ConditionBoolean)(&someBool)}}}, false}, {ConditionCalledByEntry{}, true, []stackitem.Item{stackitem.Make(WitnessCalledByEntry)}},
{&ConditionCalledByContract{1, 2, 3}, true, []stackitem.Item{stackitem.Make(WitnessCalledByContract), stackitem.Make(util.Uint160{1, 2, 3}.BytesBE())}},
{(*ConditionCalledByGroup)(pk.PublicKey()), true, []stackitem.Item{stackitem.Make(WitnessCalledByGroup), stackitem.Make(pk.PublicKey().Bytes())}},
{InvalidCondition{}, false, nil},
{&ConditionAnd{}, false, nil},
{&ConditionOr{}, false, nil},
{&ConditionNot{&ConditionNot{&ConditionNot{(*ConditionBoolean)(&someBool)}}}, false, nil},
} }
var maxSubCondAnd = &ConditionAnd{} var maxSubCondAnd = &ConditionAnd{}
var maxSubCondOr = &ConditionAnd{} var maxSubCondOr = &ConditionAnd{}
@ -61,8 +73,8 @@ func TestWitnessConditionSerDes(t *testing.T) {
*maxSubCondAnd = append(*maxSubCondAnd, (*ConditionBoolean)(&someBool)) *maxSubCondAnd = append(*maxSubCondAnd, (*ConditionBoolean)(&someBool))
*maxSubCondOr = append(*maxSubCondOr, (*ConditionBoolean)(&someBool)) *maxSubCondOr = append(*maxSubCondOr, (*ConditionBoolean)(&someBool))
} }
cases = append(cases, condCase{maxSubCondAnd, false}) cases = append(cases, condCase{maxSubCondAnd, false, nil})
cases = append(cases, condCase{maxSubCondOr, false}) cases = append(cases, condCase{maxSubCondOr, false, nil})
t.Run("binary", func(t *testing.T) { t.Run("binary", func(t *testing.T) {
for i, c := range cases { for i, c := range cases {
w := io.NewBufBinWriter() w := io.NewBufBinWriter()
@ -94,6 +106,15 @@ func TestWitnessConditionSerDes(t *testing.T) {
require.Equal(t, c.condition, res) require.Equal(t, c.condition, res)
} }
}) })
t.Run("stackitem", func(t *testing.T) {
for i, c := range cases[1:] {
if c.expectedStackItem != nil {
expected := stackitem.NewArray(c.expectedStackItem)
actual := c.condition.ToStackItem()
assert.Equal(t, expected, actual, i)
}
}
})
} }
func TestWitnessConditionZeroDeser(t *testing.T) { func TestWitnessConditionZeroDeser(t *testing.T) {

View file

@ -3,8 +3,10 @@ package transaction
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"math/big"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
) )
//go:generate stringer -type=WitnessAction -linecomment //go:generate stringer -type=WitnessAction -linecomment
@ -84,3 +86,11 @@ func (w *WitnessRule) UnmarshalJSON(data []byte) error {
w.Condition = cond w.Condition = cond
return nil return nil
} }
// ToStackItem implements Convertible interface.
func (w *WitnessRule) ToStackItem() stackitem.Item {
return stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(w.Action))),
w.Condition.ToStackItem(),
})
}

View file

@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/nspcc-dev/neo-go/internal/testserdes" "github.com/nspcc-dev/neo-go/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -54,3 +55,21 @@ func TestWitnessRuleBadJSON(t *testing.T) {
require.Errorf(t, err, "case %d, json %s", i, cases[i]) require.Errorf(t, err, "case %d, json %s", i, cases[i])
} }
} }
func TestWitnessRule_ToStackItem(t *testing.T) {
var b bool
for _, act := range []WitnessAction{WitnessDeny, WitnessAllow} {
expected := stackitem.NewArray([]stackitem.Item{
stackitem.Make(int64(act)),
stackitem.Make([]stackitem.Item{
stackitem.Make(WitnessBoolean),
stackitem.Make(b),
}),
})
actual := (&WitnessRule{
Action: act,
Condition: (*ConditionBoolean)(&b),
}).ToStackItem()
require.Equal(t, expected, actual, act)
}
}