diff --git a/chain/chaindb.go b/chain/chaindb.go index 669d1f6c5..8b755ebb3 100644 --- a/chain/chaindb.go +++ b/chain/chaindb.go @@ -521,7 +521,7 @@ func (cdb *ChainDB) dropBlock(dropNo types.BlockNo) error { } // remove receipt - cdb.deleteReceipts(&dbTx, dropBlock.BlockHash(), dropBlock.BlockNo()) + cdb.deleteReceiptsAndOperations(&dbTx, dropBlock.BlockHash(), dropBlock.BlockNo()) // remove (hash/block) dbTx.Delete(dropBlock.BlockHash()) @@ -666,6 +666,11 @@ func (cdb *ChainDB) checkExistReceipts(blockHash []byte, blockNo types.BlockNo) return true } +func (cdb *ChainDB) getInternalOperations(blockNo types.BlockNo) string { + data := cdb.store.Get(dbkey.InternalOps(blockNo)) + return string(data) +} + type ChainTree struct { Tree []ChainInfo } @@ -692,18 +697,35 @@ func (cdb *ChainDB) GetChainTree() ([]byte, error) { return jsonBytes, nil } -func (cdb *ChainDB) writeReceipts(blockHash []byte, blockNo types.BlockNo, receipts *types.Receipts) { +func (cdb *ChainDB) writeReceiptsAndOperations(block *types.Block, receipts *types.Receipts, internalOps string) { + hasReceipts := len(receipts.Get()) != 0 + hasInternalOps := len(internalOps) != 0 + + if !hasReceipts && !hasInternalOps { + return + } + dbTx := cdb.store.NewTx() defer dbTx.Discard() - val, _ := gob.Encode(receipts) - dbTx.Set(dbkey.Receipts(blockHash, blockNo), val) + blockHash := block.BlockHash() + blockNo := block.BlockNo() + + if hasReceipts { + val, _ := gob.Encode(receipts) + dbTx.Set(dbkey.Receipts(blockHash, blockNo), val) + } + + if hasInternalOps { + dbTx.Set(dbkey.InternalOps(blockNo), []byte(internalOps)) + } dbTx.Commit() } -func (cdb *ChainDB) deleteReceipts(dbTx *db.Transaction, blockHash []byte, blockNo types.BlockNo) { +func (cdb *ChainDB) deleteReceiptsAndOperations(dbTx *db.Transaction, blockHash []byte, blockNo types.BlockNo) { (*dbTx).Delete(dbkey.Receipts(blockHash, blockNo)) + (*dbTx).Delete(dbkey.InternalOps(blockNo)) } func (cdb *ChainDB) writeReorgMarker(marker *ReorgMarker) error { diff --git a/chain/chainhandle.go b/chain/chainhandle.go index d28e4e742..097fe63f6 100644 --- a/chain/chainhandle.go +++ b/chain/chainhandle.go @@ -278,6 +278,20 @@ func (cs *ChainService) listEvents(filter *types.FilterInfo) ([]*types.Event, er return events, nil } +func (cs *ChainService) getInternalOperations(blockNo types.BlockNo) (string, error) { + blockInMainChain, err := cs.cdb.GetBlockByNo(blockNo) + if err != nil { + return "", &ErrNoBlock{blockNo} + } + + block, err := cs.cdb.getBlock(blockInMainChain.BlockHash()) + if !bytes.Equal(block.BlockHash(), blockInMainChain.BlockHash()) { + return "", errors.New("internal operations not found") + } + + return cs.cdb.getInternalOperations(blockNo), nil +} + type chainProcessor struct { *ChainService block *types.Block // starting block @@ -811,9 +825,7 @@ func (cs *ChainService) executeBlock(bstate *state.BlockState, block *types.Bloc return err } - if len(ex.BlockState.Receipts().Get()) != 0 { - cs.cdb.writeReceipts(block.BlockHash(), block.BlockNo(), ex.BlockState.Receipts()) - } + cs.cdb.writeReceiptsAndOperations(block, ex.BlockState.Receipts(), ex.BlockState.InternalOps()) cs.notifyEvents(block, ex.BlockState) @@ -1007,11 +1019,12 @@ func executeTx(execCtx context.Context, ccc consensus.ChainConsensusCluster, cdb var txFee *big.Int var rv string + var internalOps string var events []*types.Event switch txBody.Type { case types.TxType_NORMAL, types.TxType_TRANSFER, types.TxType_CALL, types.TxType_MULTICALL, types.TxType_DEPLOY, types.TxType_REDEPLOY: - rv, events, txFee, err = contract.Execute(execCtx, bs, cdb, tx.GetTx(), sender, receiver, bi, executionMode, false) + rv, events, internalOps, txFee, err = contract.Execute(execCtx, bs, cdb, tx.GetTx(), sender, receiver, bi, executionMode, false) sender.SubBalance(txFee) case types.TxType_GOVERNANCE: txFee = new(big.Int).SetUint64(0) @@ -1039,7 +1052,7 @@ func executeTx(execCtx context.Context, ccc consensus.ChainConsensusCluster, cdb } return types.ErrNotAllowedFeeDelegation } - rv, events, txFee, err = contract.Execute(execCtx, bs, cdb, tx.GetTx(), sender, receiver, bi, executionMode, true) + rv, events, _, txFee, err = contract.Execute(execCtx, bs, cdb, tx.GetTx(), sender, receiver, bi, executionMode, true) receiver.SubBalance(txFee) } @@ -1094,6 +1107,10 @@ func executeTx(execCtx context.Context, ccc consensus.ChainConsensusCluster, cdb } bs.BpReward.Add(&bs.BpReward, txFee) + if len(internalOps) > 0 { + bs.AddInternalOps(internalOps) + } + receipt := types.NewReceipt(receiver.ID(), status, rv) receipt.FeeUsed = txFee.Bytes() receipt.TxHash = tx.GetHash() diff --git a/chain/chainservice.go b/chain/chainservice.go index 686bbfc50..5965153ec 100644 --- a/chain/chainservice.go +++ b/chain/chainservice.go @@ -182,6 +182,7 @@ type IChainHandler interface { getReceipt(txHash []byte) (*types.Receipt, error) getReceipts(blockHash []byte) (*types.Receipts, error) getReceiptsByNo(blockNo types.BlockNo) (*types.Receipts, error) + getInternalOperations(blockNo types.BlockNo) (string, error) getAccountVote(addr []byte) (*types.AccountVoteInfo, error) getVotes(id string, n uint32) (*types.VoteList, error) getStaking(addr []byte) (*types.Staking, error) @@ -296,7 +297,7 @@ func NewChainService(cfg *cfg.Config) *ChainService { contract.TraceBlockNo = cfg.Blockchain.StateTrace contract.SetStateSQLMaxDBSize(cfg.SQL.MaxDbSize) contract.StartLStateFactory((cfg.Blockchain.NumWorkers+2)*(int(contract.MaxCallDepth(cfg.Hardfork.Version(math.MaxUint64)))+2), cfg.Blockchain.NumLStateClosers, cfg.Blockchain.CloseLimit) - contract.InitContext(cfg.Blockchain.NumWorkers + 2) + contract.InitContext(cfg.Blockchain.NumWorkers + 2, cfg.RPC.LogInternalOperations) // For a strict governance transaction validation. types.InitGovernance(cs.ConsensusType(), cs.IsPublic()) @@ -444,6 +445,7 @@ func (cs *ChainService) Receive(context actor.Context) { *message.GetReceipt, *message.GetReceipts, *message.GetReceiptsByNo, + *message.GetInternalOperations, *message.GetABI, *message.GetQuery, *message.GetStateQuery, @@ -812,6 +814,12 @@ func (cw *ChainWorker) Receive(context actor.Context) { Receipts: receipts, Err: err, }) + case *message.GetInternalOperations: + operations, err := cw.getInternalOperations(msg.BlockNo) + context.Respond(message.GetInternalOperationsRsp{ + Operations: operations, + Err: err, + }) case *message.GetABI: sdb = cw.sdb.OpenNewStateDB(cw.sdb.GetRoot()) address, err := getAddressNameResolved(sdb, msg.Contract) diff --git a/chain/reorg.go b/chain/reorg.go index 41babf9b4..d0e27c803 100644 --- a/chain/reorg.go +++ b/chain/reorg.go @@ -491,7 +491,7 @@ func (reorg *reorganizer) rollback() error { func (reorg *reorganizer) deleteOldReceipts() { dbTx := reorg.cs.cdb.NewTx() for _, blk := range reorg.oldBlocks { - reorg.cs.cdb.deleteReceipts(&dbTx, blk.GetHash(), blk.BlockNo()) + reorg.cs.cdb.deleteReceiptsAndOperations(&dbTx, blk.GetHash(), blk.BlockNo()) } dbTx.Commit() } diff --git a/cmd/aergocli/cmd/internal_operations.go b/cmd/aergocli/cmd/internal_operations.go new file mode 100644 index 000000000..d1b9ea1af --- /dev/null +++ b/cmd/aergocli/cmd/internal_operations.go @@ -0,0 +1,73 @@ +/** + * @file + * @copyright defined in aergo/LICENSE.txt + */ + +package cmd + +import ( + "context" + "encoding/json" + "log" + + "github.com/aergoio/aergo/v2/internal/enc/base58" + aergorpc "github.com/aergoio/aergo/v2/types" + "github.com/spf13/cobra" +) + +func init() { + operationsCmd := &cobra.Command{ + Use: "operations tx_hash", + Short: "Get internal operations for a transaction", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + // Decode the transaction hash + txHash, err := base58.Decode(args[0]) + if err != nil { + log.Fatal(err) + } + + // Retrieve the receipt to get the block height + receipt, err := client.GetReceipt(context.Background(), &aergorpc.SingleBytes{Value: txHash}) + if err != nil { + log.Fatal(err) + } + + // Extract block height from the receipt + blockHeight := receipt.BlockNo + + // Use block height to get internal operations + msg, err := client.GetInternalOperations(context.Background(), &aergorpc.BlockNumberParam{BlockNo: blockHeight}) + if err != nil { + log.Fatal(err) + } + + // Extract the internal operations for the specific transaction + var operations []map[string]interface{} + if err := json.Unmarshal([]byte(msg.Value), &operations); err != nil { + log.Fatal(err) + } + + // Print the internal operations for the specific transaction + var ops string + for _, op := range operations { + if op["txhash"] == args[0] { + // Remove the txhash field + delete(op, "txhash") + // Marshal the operation to JSON for better readability + opJSON, err := json.MarshalIndent(op, "", " ") + if err != nil { + log.Fatal(err) + } + ops = string(opJSON) + } + } + if ops == "" { + cmd.Println("No internal operations found for this transaction.") + } else { + cmd.Println(ops) + } + }, + } + rootCmd.AddCommand(operationsCmd) +} diff --git a/config/types.go b/config/types.go index f5e6944e6..212ed9b72 100644 --- a/config/types.go +++ b/config/types.go @@ -53,6 +53,8 @@ type RPCConfig struct { NSKey string `mapstructure:"nskey" description:"Private Key file for RPC or REST API"` NSCACert string `mapstructure:"nscacert" description:"CA Certificate file for RPC or REST API"` NSAllowCORS bool `mapstructure:"nsallowcors" description:"Allow CORS to RPC or REST API"` + // Internal operations + LogInternalOperations bool `mapstructure:"internal_operations" description:"Log internal operations"` } // Web3Config defines configurations for web3 service @@ -206,6 +208,7 @@ nscert = "{{.RPC.NSCert}}" nskey = "{{.RPC.NSKey}}" nscacert = "{{.RPC.NSCACert}}" nsallowcors = {{.RPC.NSAllowCORS}} +internal_operations = {{.RPC.LogInternalOperations}} [p2p] # Set address and port to which the inbound peers connect, and don't set loopback address or private network unless used in local network diff --git a/contract/contract.go b/contract/contract.go index 0002018f4..e24bf03fe 100644 --- a/contract/contract.go +++ b/contract/contract.go @@ -47,7 +47,7 @@ func Execute( bi *types.BlockHeaderInfo, executionMode int, isFeeDelegation bool, -) (rv string, events []*types.Event, usedFee *big.Int, err error) { +) (rv string, events []*types.Event, internalOps string, usedFee *big.Int, err error) { var ( txBody = tx.GetBody() @@ -106,9 +106,9 @@ func Execute( // execute the transaction if receiver.IsDeploy() { - rv, events, ctrFee, err = Create(contractState, txPayload, receiver.ID(), ctx) + rv, events, internalOps, ctrFee, err = Create(contractState, txPayload, receiver.ID(), ctx) } else { - rv, events, ctrFee, err = Call(contractState, txPayload, receiver.ID(), ctx) + rv, events, internalOps, ctrFee, err = Call(contractState, txPayload, receiver.ID(), ctx) } // close the trace file @@ -118,7 +118,7 @@ func Execute( // check if the execution fee is negative if ctrFee != nil && ctrFee.Sign() < 0 { - return "", events, usedFee, ErrVmStart + return "", events, internalOps, usedFee, ErrVmStart } // add the execution fee to the total fee usedFee.Add(usedFee, ctrFee) @@ -126,19 +126,19 @@ func Execute( // check if the execution failed if err != nil { if isSystemError(err) { - return "", events, usedFee, err + return "", events, internalOps, usedFee, err } - return "", events, usedFee, newVmError(err) + return "", events, internalOps, usedFee, newVmError(err) } // check for sufficient balance for fee if isFeeDelegation { if receiver.Balance().Cmp(usedFee) < 0 { - return "", events, usedFee, newVmError(types.ErrInsufficientBalance) + return "", events, internalOps, usedFee, newVmError(types.ErrInsufficientBalance) } } else { if sender.Balance().Cmp(usedFee) < 0 { - return "", events, usedFee, newVmError(types.ErrInsufficientBalance) + return "", events, internalOps, usedFee, newVmError(types.ErrInsufficientBalance) } } @@ -146,12 +146,12 @@ func Execute( // save the contract state err = statedb.StageContractState(contractState, bs.StateDB) if err != nil { - return "", events, usedFee, err + return "", events, internalOps, usedFee, err } } // return the result - return rv, events, usedFee, nil + return rv, events, internalOps, usedFee, nil } // check if the tx is valid and if the code should be executed diff --git a/contract/internal_operations.go b/contract/internal_operations.go new file mode 100644 index 000000000..b88ba76b2 --- /dev/null +++ b/contract/internal_operations.go @@ -0,0 +1,205 @@ +package contract + +import ( + "encoding/json" + "sync" + + "github.com/aergoio/aergo/v2/internal/enc/base58" +) + +type InternalOperation struct { + Id int64 `json:"-"` + Operation string `json:"op"` + Amount string `json:"amount,omitempty"` + Args []string `json:"args"` + Result string `json:"result,omitempty"` + Call *InternalCall `json:"call,omitempty"` +} + +type InternalCall struct { + Contract string `json:"contract,omitempty"` + Function string `json:"function,omitempty"` + Args []interface{} `json:"args,omitempty"` + Amount string `json:"amount,omitempty"` + Operations []InternalOperation `json:"operations,omitempty"` +} + +type InternalOperations struct { + TxHash string `json:"txhash"` + Call InternalCall `json:"call"` +} + +var ( + opsLock sync.Mutex + nextOpId int64 +) + +func doNotLog(ctx *vmContext) bool { + if logInternalOperations == false { + return true + } + return ctx.isQuery +} + +func getCurrentCall(ctx *vmContext, callDepth int32) *InternalCall { + var depth int32 = 1 + opCall := &ctx.internalOpsCall + for { + if opCall == nil { + ctrLgr.Printf("no call found at depth %d", depth) + break + } + if depth == callDepth { + return opCall + } + if len(opCall.Operations) == 0 { + ctrLgr.Printf("no operations found at depth %d", depth) + break + } + opCall = opCall.Operations[len(opCall.Operations)-1].Call + depth++ + } + return nil +} + +func logOperation(ctx *vmContext, amount string, operation string, args ...string) int64 { + if doNotLog(ctx) { + return 0 + } + + ctrLgr.Printf("logOperation: depth: %d, amount: %s, operation: %s, args: %v", ctx.callDepth, amount, operation, args) + + opsLock.Lock() + defer opsLock.Unlock() + + nextOpId++ + if nextOpId > 1000000000000000000 { + nextOpId = 1 + } + + op := InternalOperation{ + Id: nextOpId, + Operation: operation, + Amount: amount, + Args: args, + } + + opCall := getCurrentCall(ctx, ctx.callDepth) + if opCall == nil { + ctrLgr.Printf("no call found") + return 0 + } + // add the operation to the list + opCall.Operations = append(opCall.Operations, op) + + return op.Id +} + +func logOperationResult(ctx *vmContext, operationId int64, result string) { + if doNotLog(ctx) { + return + } + + ctrLgr.Printf("logOperationResult: depth: %d, operationId: %d, result: %s", ctx.callDepth, operationId, result) + + opsLock.Lock() + defer opsLock.Unlock() + + // try with the last and the previous call depth + for callDepth := ctx.callDepth; callDepth >= ctx.callDepth - 1; callDepth-- { + opCall := getCurrentCall(ctx, callDepth) + if opCall == nil { + continue + } + for i := range opCall.Operations { + if opCall.Operations[i].Id == operationId { + opCall.Operations[i].Result = result + return + } + } + } + + ctrLgr.Printf("no operation found with ID %d to store result", operationId) +} + +func logInternalCall(ctx *vmContext, contract string, function string, args []interface{}, amount string) error { + if doNotLog(ctx) { + return nil + } + + ctrLgr.Printf("logInternalCall: depth: %d, contract: %s, function: %s, args: %s, amount: %s", ctx.callDepth, contract, function, argsToJson(args), amount) + + opCall := getCurrentCall(ctx, ctx.callDepth-1) + if opCall == nil { + ctrLgr.Printf("no call found") + return nil + } + + // get the last operation + op := &opCall.Operations[len(opCall.Operations)-1] + + // add this call to the last operation + op.Call = &InternalCall{ + Contract: contract, + Function: function, + Args: args, + Amount: amount, + } + + return nil +} + +func logFirstCall(ctx *vmContext, contract string, function string, args []interface{}, amount string) { + ctrLgr.Printf("logFirstCall: depth: %d, contract: %s, function: %s, args: %s, amount: %s", ctx.callDepth, contract, function, argsToJson(args), amount) + ctx.internalOpsCall.Contract = contract + ctx.internalOpsCall.Function = function + ctx.internalOpsCall.Args = args + ctx.internalOpsCall.Amount = amount +} + +func logCall(ctx *vmContext, contract string, function string, args []interface{}, amount string) { + if amount == "0" { + amount = "" + } + if ctx.internalOpsCall.Contract == "" { + logFirstCall(ctx, contract, function, args, amount) + } else { + logInternalCall(ctx, contract, function, args, amount) + } +} + +func argsToJson(argsList []interface{}) (string) { + if argsList == nil { + return "" + } + args, err := json.Marshal(argsList) + if err != nil { + return "" + } + return string(args) +} + +func getInternalOperations(ctx *vmContext) string { + if doNotLog(ctx) { + return "" + } + if ctx.internalOpsCall.Contract == "" { + return "" + } + + opsLock.Lock() + defer opsLock.Unlock() + + internalOps := InternalOperations{ + TxHash: base58.Encode(ctx.txHash), + Call: ctx.internalOpsCall, + } + + data, err := json.Marshal(internalOps) + if err != nil { + ctrLgr.Fatal().Err(err).Msg("Failed to marshal operations") + return "" + } + + return string(data) +} diff --git a/contract/vm.go b/contract/vm.go index 11d5985a5..29f9cc457 100644 --- a/contract/vm.go +++ b/contract/vm.go @@ -67,6 +67,7 @@ var ( lastQueryIndex int querySync sync.Mutex currentForkVersion int32 + logInternalOperations bool ) type ChainAccessor interface { @@ -102,6 +103,7 @@ type vmContext struct { gasLimit uint64 remainedGas uint64 execCtx context.Context + internalOpsCall InternalCall } type executor struct { @@ -111,6 +113,7 @@ type executor struct { numArgs C.int ci *types.CallInfo fname string + amount *big.Int ctx *vmContext jsonRet string isView bool @@ -130,9 +133,10 @@ func init() { lastQueryIndex = ChainService } -func InitContext(numCtx int) { +func InitContext(numCtx int, logInternalOps bool) { maxContext = numCtx contexts = make([]*vmContext, maxContext) + logInternalOperations = logInternalOps } func NewVmContext( @@ -404,6 +408,7 @@ func newExecutor( ce.numArgs = C.int(len(ci.Args) + 1) } ce.ci = ci + ce.amount = amount return ce } @@ -558,11 +563,11 @@ func (ce *executor) call(instLimit C.int, target *LState) (ret C.int) { ce.err = ce.preErr return 0 } + contract := types.EncodeAddress(ce.ctx.curContract.contractId) if ce.isAutoload { if loaded := vmAutoload(ce.L, ce.fname); !loaded { if ce.fname != constructor { - ce.err = errors.New(fmt.Sprintf("contract autoload failed %s : %s", - types.EncodeAddress(ce.ctx.curContract.contractId), ce.fname)) + ce.err = errors.New(fmt.Sprintf("contract autoload failed %s : %s", contract, ce.fname)) } return 0 } @@ -574,10 +579,10 @@ func (ce *executor) call(instLimit C.int, target *LState) (ret C.int) { } ce.processArgs() if ce.err != nil { - ctrLgr.Debug().Err(ce.err).Stringer("contract", - types.LogAddr(ce.ctx.curContract.contractId)).Msg("invalid argument") + ctrLgr.Debug().Err(ce.err).Str("contract", contract).Msg("invalid argument") return 0 } + logCall(ce.ctx, contract, ce.fname, ce.ci.Args, ce.amount.String()) ce.setCountHook(instLimit) nRet := C.int(0) cErrMsg := C.vm_pcall(ce.L, ce.numArgs, &nRet) @@ -593,10 +598,7 @@ func (ce *executor) call(instLimit C.int, target *LState) (ret C.int) { ce.err = errors.New(errMsg) } } - ctrLgr.Debug().Err(ce.err).Stringer( - "contract", - types.LogAddr(ce.ctx.curContract.contractId), - ).Msg("contract is failed") + ctrLgr.Debug().Err(ce.err).Str("contract", contract).Msg("contract is failed") return 0 } if target == nil { @@ -611,15 +613,11 @@ func (ce *executor) call(instLimit C.int, target *LState) (ret C.int) { if c2ErrMsg := C.vm_copy_result(ce.L, target, nRet); c2ErrMsg != nil { errMsg := C.GoString(c2ErrMsg) ce.err = errors.New(errMsg) - ctrLgr.Debug().Err(ce.err).Stringer( - "contract", - types.LogAddr(ce.ctx.curContract.contractId), - ).Msg("failed to move results") + ctrLgr.Debug().Err(ce.err).Str("contract", contract).Msg("failed to move results") } } if ce.ctx.traceFile != nil { - address := types.EncodeAddress(ce.ctx.curContract.contractId) - codeFile := fmt.Sprintf("%s%s%s.code", os.TempDir(), string(os.PathSeparator), address) + codeFile := fmt.Sprintf("%s%s%s.code", os.TempDir(), string(os.PathSeparator), contract) if _, err := os.Stat(codeFile); os.IsNotExist(err) { f, err := os.OpenFile(codeFile, os.O_WRONLY|os.O_CREATE, 0644) if err == nil { @@ -628,7 +626,7 @@ func (ce *executor) call(instLimit C.int, target *LState) (ret C.int) { } } _, _ = ce.ctx.traceFile.WriteString(fmt.Sprintf("contract %s used fee: %s\n", - address, ce.ctx.usedFee().String())) + contract, ce.ctx.usedFee().String())) } return nRet } @@ -825,7 +823,7 @@ func Call( contractState *statedb.ContractState, payload, contractAddress []byte, ctx *vmContext, -) (string, []*types.Event, *big.Int, error) { +) (string, []*types.Event, string, *big.Int, error) { var err error var ci types.CallInfo @@ -850,7 +848,7 @@ func Call( err = fmt.Errorf("not found contract %s", addr) } if err != nil { - return "", nil, ctx.usedFee(), err + return "", nil, "", ctx.usedFee(), err } if ctrLgr.IsDebugEnabled() { @@ -870,6 +868,8 @@ func Call( vmLogger.Trace().Int64("execµs", vmExecTime).Stringer("txHash", types.LogBase58(ce.ctx.txHash)).Msg("tx execute time in vm") } + internalOps := getInternalOperations(ctx) + // check if there is an error err = ce.err if err != nil { @@ -894,14 +894,14 @@ func Call( types.EncodeAddress(contractAddress), types.ToAccountID(contractAddress))) } // return the error - return "", ce.getEvents(), ctx.usedFee(), err + return "", ce.getEvents(), internalOps, ctx.usedFee(), err } // save the state of the contract err = ce.commitCalledContract() if err != nil { ctrLgr.Error().Err(err).Str("contract", types.EncodeAddress(contractAddress)).Msg("commit state") - return "", ce.getEvents(), ctx.usedFee(), err + return "", ce.getEvents(), internalOps, ctx.usedFee(), err } // log the result @@ -922,7 +922,7 @@ func Call( } // return the result - return ce.jsonRet, ce.getEvents(), ctx.usedFee(), nil + return ce.jsonRet, ce.getEvents(), internalOps, ctx.usedFee(), nil } func setRandomSeed(ctx *vmContext) { @@ -994,10 +994,10 @@ func Create( contractState *statedb.ContractState, payload, contractAddress []byte, ctx *vmContext, -) (string, []*types.Event, *big.Int, error) { +) (string, []*types.Event, string, *big.Int, error) { if len(payload) == 0 { - return "", nil, ctx.usedFee(), errors.New("contract code is required") + return "", nil, "", ctx.usedFee(), errors.New("contract code is required") } if ctrLgr.IsDebugEnabled() { @@ -1007,13 +1007,13 @@ func Create( // save the contract code bytecode, args, err := setContract(contractState, contractAddress, payload, ctx) if err != nil { - return "", nil, ctx.usedFee(), err + return "", nil, "", ctx.usedFee(), err } // set the creator err = contractState.SetData(dbkey.CreatorMeta(), []byte(types.EncodeAddress(ctx.curContract.sender))) if err != nil { - return "", nil, ctx.usedFee(), err + return "", nil, "", ctx.usedFee(), err } // get the arguments for the constructor @@ -1022,7 +1022,7 @@ func Create( err = getCallInfo(&ci.Args, args, contractAddress) if err != nil { errMsg, _ := json.Marshal("constructor call error:" + err.Error()) - return string(errMsg), nil, ctx.usedFee(), nil + return string(errMsg), nil, "", ctx.usedFee(), nil } } @@ -1031,7 +1031,7 @@ func Create( if ctx.blockInfo.ForkVersion < 2 { // create a sql database for the contract if db := luaGetDbHandle(ctx.service); db == nil { - return "", nil, ctx.usedFee(), newVmError(errors.New("can't open a database connection")) + return "", nil, "", ctx.usedFee(), newVmError(errors.New("can't open a database connection")) } } @@ -1039,11 +1039,13 @@ func Create( ce := newExecutor(bytecode, contractAddress, ctx, &ci, ctx.curContract.amount, true, false, contractState) defer ce.close() - if err == nil { + if ce.err == nil { // call the constructor ce.call(callMaxInstLimit, nil) } + internalOps := getInternalOperations(ctx) + // check if the call failed err = ce.err if err != nil { @@ -1069,7 +1071,7 @@ func Create( types.EncodeAddress(contractAddress), types.ToAccountID(contractAddress))) } // return the error - return "", ce.getEvents(), ctx.usedFee(), err + return "", ce.getEvents(), internalOps, ctx.usedFee(), err } // commit the state @@ -1077,7 +1079,7 @@ func Create( if err != nil { ctrLgr.Debug().Msg("constructor is failed") ctrLgr.Error().Err(err).Msg("commit state") - return "", ce.getEvents(), ctx.usedFee(), err + return "", ce.getEvents(), internalOps, ctx.usedFee(), err } // write the trace @@ -1098,7 +1100,7 @@ func Create( } // return the result - return ce.jsonRet, ce.getEvents(), ctx.usedFee(), nil + return ce.jsonRet, ce.getEvents(), internalOps, ctx.usedFee(), nil } func allocContextSlot(ctx *vmContext) { diff --git a/contract/vm_callback.go b/contract/vm_callback.go index 943ce6024..634f8b5b2 100644 --- a/contract/vm_callback.go +++ b/contract/vm_callback.go @@ -208,25 +208,34 @@ func minusCallCount(ctx *vmContext, curCount, deduc C.int) C.int { //export luaCallContract func luaCallContract(L *LState, service C.int, contractId *C.char, fname *C.char, args *C.char, - amount *C.char, gas uint64) (C.int, *C.char) { + amount *C.char, gas uint64) (ret C.int, errormsg *C.char) { + contractAddress := C.GoString(contractId) fnameStr := C.GoString(fname) argsStr := C.GoString(args) + amountStr := C.GoString(amount) ctx := contexts[service] if ctx == nil { return -1, C.CString("[Contract.LuaCallContract] contract state not found") } + opId := logOperation(ctx, amountStr, "call", contractAddress, fnameStr, argsStr) + defer func() { + if errormsg != nil { + logOperationResult(ctx, opId, C.GoString(errormsg)) + } + }() + // get the contract address - contractAddress := C.GoString(contractId) cid, err := getAddressNameResolved(contractAddress, ctx.bs) if err != nil { return -1, C.CString("[Contract.LuaCallContract] invalid contractId: " + err.Error()) } aid := types.ToAccountID(cid) + contractAddress = types.EncodeAddress(cid) // read the amount for the contract call - amountBig, err := transformAmount(C.GoString(amount), ctx.blockInfo.ForkVersion) + amountBig, err := transformAmount(amountStr, ctx.blockInfo.ForkVersion) if err != nil { return -1, C.CString("[Contract.LuaCallContract] invalid amount: " + err.Error()) } @@ -240,7 +249,7 @@ func luaCallContract(L *LState, service C.int, contractId *C.char, fname *C.char // check if the contract exists bytecode := getContractCode(cs.ctrState, ctx.bs) if bytecode == nil { - return -1, C.CString("[Contract.LuaCallContract] cannot find contract " + C.GoString(contractId)) + return -1, C.CString("[Contract.LuaCallContract] cannot find contract " + contractAddress) } prevContractInfo := ctx.curContract @@ -258,6 +267,10 @@ func luaCallContract(L *LState, service C.int, contractId *C.char, fname *C.char // create a new executor with the remaining gas on the child LState ce := newExecutor(bytecode, cid, ctx, &ci, amountBig, false, false, cs.ctrState) defer func() { + // save the result if the call was successful + if ce.preErr == nil && ce.err == nil { + logOperationResult(ctx, opId, ce.jsonRet) + } // close the executor, closes also the child LState ce.close() // set the remaining gas on the parent LState @@ -302,7 +315,7 @@ func luaCallContract(L *LState, service C.int, contractId *C.char, fname *C.char // execute the contract call defer setInstCount(ctx, L, ce.L) - ret := ce.call(minusCallCount(ctx, C.vm_instcount(L), luaCallCountDeduc), L) + ret = ce.call(minusCallCount(ctx, C.vm_instcount(L), luaCallCountDeduc), L) // check if the contract call failed if ce.err != nil { @@ -334,7 +347,7 @@ func luaCallContract(L *LState, service C.int, contractId *C.char, fname *C.char //export luaDelegateCallContract func luaDelegateCallContract(L *LState, service C.int, contractId *C.char, - fname *C.char, args *C.char, gas uint64) (C.int, *C.char) { + fname *C.char, args *C.char, gas uint64) (ret C.int, errormsg *C.char) { contractIdStr := C.GoString(contractId) fnameStr := C.GoString(fname) argsStr := C.GoString(args) @@ -348,6 +361,13 @@ func luaDelegateCallContract(L *LState, service C.int, contractId *C.char, var cid []byte var err error + opId := logOperation(ctx, "", "delegate-call", contractIdStr, fnameStr, argsStr) + defer func() { + if errormsg != nil { + logOperationResult(ctx, opId, C.GoString(errormsg)) + } + }() + // get the contract address if contractIdStr == "multicall" { isMultiCall = true @@ -359,6 +379,7 @@ func luaDelegateCallContract(L *LState, service C.int, contractId *C.char, if err != nil { return -1, C.CString("[Contract.LuaDelegateCallContract] invalid contractId: " + err.Error()) } + contractIdStr = types.EncodeAddress(cid) } aid := types.ToAccountID(cid) @@ -401,6 +422,10 @@ func luaDelegateCallContract(L *LState, service C.int, contractId *C.char, // create a new executor with the remaining gas on the child LState ce := newExecutor(bytecode, cid, ctx, &ci, zeroBig, false, false, contractState) defer func() { + // save the result if the call was successful + if ce.preErr == nil && ce.err == nil { + logOperationResult(ctx, opId, ce.jsonRet) + } // close the executor, closes also the child LState ce.close() // set the remaining gas on the parent LState @@ -422,7 +447,7 @@ func luaDelegateCallContract(L *LState, service C.int, contractId *C.char, // execute the contract call defer setInstCount(ctx, L, ce.L) - ret := ce.call(minusCallCount(ctx, C.vm_instcount(L), luaCallCountDeduc), L) + ret = ce.call(minusCallCount(ctx, C.vm_instcount(L), luaCallCountDeduc), L) // check if the contract call failed if ce.err != nil { @@ -469,15 +494,24 @@ func getAddressNameResolved(account string, bs *state.BlockState) ([]byte, error } //export luaSendAmount -func luaSendAmount(L *LState, service C.int, contractId *C.char, amount *C.char) *C.char { +func luaSendAmount(L *LState, service C.int, contractId *C.char, amount *C.char) (errormsg *C.char) { + contractAddress := C.GoString(contractId) + amountStr := C.GoString(amount) ctx := contexts[service] if ctx == nil { return C.CString("[Contract.LuaSendAmount] contract state not found") } + opId := logOperation(ctx, amountStr, "send", contractAddress) + defer func() { + if errormsg != nil { + logOperationResult(ctx, opId, C.GoString(errormsg)) + } + }() + // read the amount to be sent - amountBig, err := transformAmount(C.GoString(amount), ctx.blockInfo.ForkVersion) + amountBig, err := transformAmount(amountStr, ctx.blockInfo.ForkVersion) if err != nil { return C.CString("[Contract.LuaSendAmount] invalid amount: " + err.Error()) } @@ -488,10 +522,11 @@ func luaSendAmount(L *LState, service C.int, contractId *C.char, amount *C.char) } // get the receiver account - cid, err := getAddressNameResolved(C.GoString(contractId), ctx.bs) + cid, err := getAddressNameResolved(contractAddress, ctx.bs) if err != nil { return C.CString("[Contract.LuaSendAmount] invalid contractId: " + err.Error()) } + contractAddress = types.EncodeAddress(cid) // get the receiver state aid := types.ToAccountID(cid) @@ -522,7 +557,7 @@ func luaSendAmount(L *LState, service C.int, contractId *C.char, amount *C.char) // get the contract code bytecode := getContractCode(cs.ctrState, ctx.bs) if bytecode == nil { - return C.CString("[Contract.LuaSendAmount] cannot find contract:" + C.GoString(contractId)) + return C.CString("[Contract.LuaSendAmount] cannot find contract:" + contractAddress) } // get the remaining gas from the parent LState @@ -530,6 +565,10 @@ func luaSendAmount(L *LState, service C.int, contractId *C.char, amount *C.char) // create a new executor with the remaining gas on the child LState ce := newExecutor(bytecode, cid, ctx, &ci, amountBig, false, false, cs.ctrState) defer func() { + // save the result if the call was successful + if ce.preErr == nil && ce.err == nil { + logOperationResult(ctx, opId, ce.jsonRet) + } // close the executor, closes also the child LState ce.close() // set the remaining gas on the parent LState @@ -1115,10 +1154,11 @@ func luaDeployContract( contract *C.char, args *C.char, amount *C.char, -) (C.int, *C.char) { +) (ret C.int, errormsg *C.char) { - argsStr := C.GoString(args) contractStr := C.GoString(contract) + argsStr := C.GoString(args) + amountStr := C.GoString(amount) ctx := contexts[service] if ctx == nil { @@ -1129,6 +1169,13 @@ func luaDeployContract( } bs := ctx.bs + opId := logOperation(ctx, amountStr, "deploy", contractStr, argsStr) + defer func() { + if errormsg != nil { + logOperationResult(ctx, opId, C.GoString(errormsg)) + } + }() + // contract code var codeABI []byte var sourceCode []byte @@ -1195,7 +1242,7 @@ func luaDeployContract( ctx.callState[newContract.AccountID()] = cs // read the amount transferred to the contract - amountBig, err := transformAmount(C.GoString(amount), ctx.blockInfo.ForkVersion) + amountBig, err := transformAmount(amountStr, ctx.blockInfo.ForkVersion) if err != nil { return -1, C.CString("[Contract.LuaDeployContract]value not proper format:" + err.Error()) } @@ -1258,6 +1305,10 @@ func luaDeployContract( // create a new executor with the remaining gas on the child LState ce := newExecutor(bytecode, newContract.ID(), ctx, &ci, amountBig, true, false, contractState) defer func() { + // save the result if the call was successful + if ce.preErr == nil && ce.err == nil { + logOperationResult(ctx, opId, ce.jsonRet) + } // close the executor, which will close the child LState ce.close() // set the remaining gas on the parent LState @@ -1279,7 +1330,7 @@ func luaDeployContract( senderState.SetNonce(senderState.Nonce() + 1) addr := C.CString(types.EncodeAddress(newContract.ID())) - ret := C.int(1) + ret = C.int(1) if ce != nil { // run the constructor @@ -1331,7 +1382,9 @@ func luaRandomInt(min, max, service C.int) C.int { } //export luaEvent -func luaEvent(L *LState, service C.int, eventName *C.char, args *C.char) *C.char { +func luaEvent(L *LState, service C.int, name *C.char, args *C.char) *C.char { + eventName := C.GoString(name) + eventArgs := C.GoString(args) ctx := contexts[service] if ctx.isQuery == true || ctx.nestedView > 0 { return C.CString("[Contract.Event] event not permitted in query") @@ -1339,10 +1392,10 @@ func luaEvent(L *LState, service C.int, eventName *C.char, args *C.char) *C.char if ctx.eventCount >= maxEventCnt(ctx) { return C.CString(fmt.Sprintf("[Contract.Event] exceeded the maximum number of events(%d)", maxEventCnt(ctx))) } - if len(C.GoString(eventName)) > maxEventNameSize { + if len(eventName) > maxEventNameSize { return C.CString(fmt.Sprintf("[Contract.Event] exceeded the maximum length of event name(%d)", maxEventNameSize)) } - if len(C.GoString(args)) > maxEventArgSize { + if len(eventArgs) > maxEventArgSize { return C.CString(fmt.Sprintf("[Contract.Event] exceeded the maximum length of event args(%d)", maxEventArgSize)) } ctx.events = append( @@ -1350,11 +1403,12 @@ func luaEvent(L *LState, service C.int, eventName *C.char, args *C.char) *C.char &types.Event{ ContractAddress: ctx.curContract.contractId, EventIdx: ctx.eventCount, - EventName: C.GoString(eventName), - JsonArgs: C.GoString(args), + EventName: eventName, + JsonArgs: eventArgs, }, ) ctx.eventCount++ + logOperation(ctx, "", "event", eventName, eventArgs) return nil } @@ -1456,7 +1510,7 @@ func luaNameResolve(L *LState, service C.int, name_or_address *C.char) *C.char { } //export luaGovernance -func luaGovernance(L *LState, service C.int, gType C.char, arg *C.char) *C.char { +func luaGovernance(L *LState, service C.int, gType C.char, arg *C.char) (errormsg *C.char) { ctx := contexts[service] if ctx == nil { @@ -1469,6 +1523,7 @@ func luaGovernance(L *LState, service C.int, gType C.char, arg *C.char) *C.char var amountBig *big.Int var payload []byte + var opId int64 switch gType { case 'S', 'U': @@ -1479,17 +1534,27 @@ func luaGovernance(L *LState, service C.int, gType C.char, arg *C.char) *C.char } if gType == 'S' { payload = []byte(fmt.Sprintf(`{"Name":"%s"}`, types.Opstake.Cmd())) + opId = logOperation(ctx, "", "stake", amountBig.String()) } else { payload = []byte(fmt.Sprintf(`{"Name":"%s"}`, types.Opunstake.Cmd())) + opId = logOperation(ctx, "", "unstake", amountBig.String()) } case 'V': amountBig = zeroBig payload = []byte(fmt.Sprintf(`{"Name":"%s","Args":%s}`, types.OpvoteBP.Cmd(), C.GoString(arg))) + opId = logOperation(ctx, "", "vote", C.GoString(arg)) case 'D': amountBig = zeroBig payload = []byte(fmt.Sprintf(`{"Name":"%s","Args":%s}`, types.OpvoteDAO.Cmd(), C.GoString(arg))) + opId = logOperation(ctx, "", "voteDAO", C.GoString(arg)) } + defer func() { + if errormsg != nil { + logOperationResult(ctx, opId, C.GoString(errormsg)) + } + }() + cid := []byte(types.AergoSystem) aid := types.ToAccountID(cid) scsState, err := getContractState(ctx, cid) diff --git a/contract/vm_direct/vm_direct.go b/contract/vm_direct/vm_direct.go index 98cadc329..151a4d07a 100644 --- a/contract/vm_direct/vm_direct.go +++ b/contract/vm_direct/vm_direct.go @@ -135,7 +135,7 @@ func LoadDummyChainEx(chainType ChainType) (*DummyChain, error) { contract.SetStateSQLMaxDBSize(1024) contract.StartLStateFactory(lStateMaxSize, config.GetDefaultNumLStateClosers(), 1) - contract.InitContext(3) + contract.InitContext(3, false) // To pass the governance tests. types.InitGovernance("dpos", true) @@ -482,10 +482,12 @@ func executeTx( var txFee *big.Int var rv string + var internalOps string var events []*types.Event + switch txBody.Type { case types.TxType_NORMAL, types.TxType_REDEPLOY, types.TxType_TRANSFER, types.TxType_CALL, types.TxType_DEPLOY: - rv, events, txFee, err = contract.Execute(execCtx, bs, cdb, tx.GetTx(), sender, receiver, bi, executionMode, false) + rv, events, internalOps, txFee, err = contract.Execute(execCtx, bs, cdb, tx.GetTx(), sender, receiver, bi, executionMode, false) sender.SubBalance(txFee) case types.TxType_GOVERNANCE: txFee = new(big.Int).SetUint64(0) @@ -512,7 +514,7 @@ func executeTx( } return types.ErrNotAllowedFeeDelegation } - rv, events, txFee, err = contract.Execute(execCtx, bs, cdb, tx.GetTx(), sender, receiver, bi, executionMode, true) + rv, events, _, txFee, err = contract.Execute(execCtx, bs, cdb, tx.GetTx(), sender, receiver, bi, executionMode, true) receiver.SubBalance(txFee) } @@ -567,6 +569,10 @@ func executeTx( } bs.BpReward.Add(&bs.BpReward, txFee) + if len(internalOps) > 0 { + bs.AddInternalOps(internalOps) + } + receipt := types.NewReceipt(receiver.ID(), status, rv) receipt.FeeUsed = txFee.Bytes() receipt.TxHash = tx.GetHash() diff --git a/contract/vm_dummy/vm_dummy.go b/contract/vm_dummy/vm_dummy.go index b867941ad..2244e617f 100644 --- a/contract/vm_dummy/vm_dummy.go +++ b/contract/vm_dummy/vm_dummy.go @@ -130,7 +130,7 @@ func LoadDummyChain(opts ...DummyChainOptions) (*DummyChain, error) { contract.LoadTestDatabase(dataPath) // sql database contract.SetStateSQLMaxDBSize(1024) contract.StartLStateFactory(lStateMaxSize, config.GetDefaultNumLStateClosers(), 1) - contract.InitContext(3) + contract.InitContext(3, false) bc.HardforkVersion = 2 @@ -485,7 +485,7 @@ func (l *luaTxDeploy) Constructor(args string) *luaTxDeploy { } func contractFrame(l luaTxContract, bs *state.BlockState, cdb contract.ChainAccessor, receiptTx db.Transaction, - run func(s, c *state.AccountState, id types.AccountID, cs *statedb.ContractState) (string, []*types.Event, *big.Int, error)) error { + run func(s, c *state.AccountState, id types.AccountID, cs *statedb.ContractState) (string, []*types.Event, string, *big.Int, error)) error { creatorId := types.ToAccountID(l.sender()) creatorState, err := state.GetAccountState(l.sender(), bs.StateDB) @@ -539,7 +539,7 @@ func contractFrame(l luaTxContract, bs *state.BlockState, cdb contract.ChainAcce return err } - rv, events, cFee, err := run(creatorState, contractState, contractId, eContractState) + rv, events, _, cFee, err := run(creatorState, contractState, contractId, eContractState) if cFee != nil { usedFee.Add(usedFee, cFee) @@ -600,20 +600,20 @@ func (l *luaTxDeploy) run(execCtx context.Context, bs *state.BlockState, bc *Dum l._payload = util.NewLuaCodePayload(byteCode, payload.Args()) } return contractFrame(l, bs, bc, receiptTx, - func(sender, contractV *state.AccountState, contractId types.AccountID, eContractState *statedb.ContractState) (string, []*types.Event, *big.Int, error) { + func(sender, contractV *state.AccountState, contractId types.AccountID, eContractState *statedb.ContractState) (string, []*types.Event, string, *big.Int, error) { contractV.State().SqlRecoveryPoint = 1 ctx := contract.NewVmContext(execCtx, bs, nil, sender, contractV, eContractState, sender.ID(), l.Hash(), bi, "", true, false, contractV.State().SqlRecoveryPoint, contract.BlockFactory, l.amount(), math.MaxUint64, false, false) - rv, events, ctrFee, err := contract.Create(eContractState, l.payload(), l.recipient(), ctx) + rv, events, internalOps, ctrFee, err := contract.Create(eContractState, l.payload(), l.recipient(), ctx) if err != nil { - return "", nil, ctrFee, err + return "", nil, internalOps, ctrFee, err } err = statedb.StageContractState(eContractState, bs.StateDB) if err != nil { - return "", nil, ctrFee, err + return "", nil, internalOps, ctrFee, err } - return rv, events, ctrFee, nil + return rv, events, internalOps, ctrFee, nil }, ) } @@ -674,23 +674,23 @@ func (l *luaTxCall) Fail(expectedErr string) *luaTxCall { func (l *luaTxCall) run(execCtx context.Context, bs *state.BlockState, bc *DummyChain, bi *types.BlockHeaderInfo, receiptTx db.Transaction) error { err := contractFrame(l, bs, bc, receiptTx, - func(sender, contractV *state.AccountState, contractId types.AccountID, eContractState *statedb.ContractState) (string, []*types.Event, *big.Int, error) { + func(sender, contractV *state.AccountState, contractId types.AccountID, eContractState *statedb.ContractState) (string, []*types.Event, string, *big.Int, error) { ctx := contract.NewVmContext(execCtx, bs, bc, sender, contractV, eContractState, sender.ID(), l.Hash(), bi, "", true, false, contractV.State().SqlRecoveryPoint, contract.BlockFactory, l.amount(), math.MaxUint64, l.feeDelegate, l.multiCall) - rv, events, ctrFee, err := contract.Call(eContractState, l.payload(), l.recipient(), ctx) + rv, events, internalOps, ctrFee, err := contract.Call(eContractState, l.payload(), l.recipient(), ctx) if err != nil { - return "", nil, ctrFee, err + return "", nil, internalOps, ctrFee, err } if !ctx.IsMultiCall() { err = statedb.StageContractState(eContractState, bs.StateDB) if err != nil { - return "", nil, ctrFee, err + return "", nil, internalOps, ctrFee, err } } - return rv, events, ctrFee, nil + return rv, events, internalOps, ctrFee, nil }, ) if l.expectedErr != "" { diff --git a/rpc/grpcserver.go b/rpc/grpcserver.go index 5fcaf3eb9..d0d5b089a 100644 --- a/rpc/grpcserver.go +++ b/rpc/grpcserver.go @@ -1069,6 +1069,22 @@ func (rpc *AergoRPCService) GetReceipt(ctx context.Context, in *types.SingleByte return rsp.Receipt, rsp.Err } +func (rpc *AergoRPCService) GetInternalOperations(ctx context.Context, in *types.BlockNumberParam) (*types.SingleBytes, error) { + if err := rpc.checkAuth(ctx, ReadBlockChain); err != nil { + return nil, err + } + result, err := rpc.hub.RequestFuture(message.ChainSvc, + &message.GetInternalOperations{BlockNo: in.BlockNo}, defaultActorTimeout, "rpc.(*AergoRPCService).GetInternalOperations").Result() + if err != nil { + return nil, err + } + rsp, ok := result.(message.GetInternalOperationsRsp) + if !ok { + return nil, status.Errorf(codes.Internal, "internal type (%v) error", reflect.TypeOf(result)) + } + return &types.SingleBytes{Value: []byte(rsp.Operations)}, rsp.Err +} + func (rpc *AergoRPCService) GetABI(ctx context.Context, in *types.SingleBytes) (*types.ABI, error) { if err := rpc.checkAuth(ctx, ReadBlockChain); err != nil { return nil, err diff --git a/state/block.go b/state/block.go index 91d28f8ad..2cfd9a80e 100644 --- a/state/block.go +++ b/state/block.go @@ -1,6 +1,7 @@ package state import ( + "strings" "math/big" "github.com/aergoio/aergo/v2/consensus" @@ -15,6 +16,7 @@ type BlockState struct { *statedb.StateDB BpReward big.Int // final bp reward, increment when tx executes receipts types.Receipts + internalOps []string CCProposal *consensus.ConfChangePropose prevBlockHash []byte consensus []byte // Consensus Header @@ -80,6 +82,17 @@ func (bs *BlockState) SetConsensus(ch []byte) { bs.consensus = ch } +func (bs *BlockState) AddInternalOps(txops string) { + bs.internalOps = append(bs.internalOps, txops) +} + +func (bs *BlockState) InternalOps() string { + if bs == nil || len(bs.internalOps) == 0 { + return "" + } + return "[" + strings.Join(bs.internalOps, ",") + "]" +} + func (bs *BlockState) AddReceipt(r *types.Receipt) error { if len(r.Events) > 0 { rBloom := bloom.New(types.BloomBitBits, types.BloomHashKNum) diff --git a/tests/common.sh b/tests/common.sh index 90094c7f4..44512064e 100644 --- a/tests/common.sh +++ b/tests/common.sh @@ -21,8 +21,12 @@ stop_nodes() { if [ "$consensus" == "sbp" ]; then kill $pid + # wait until the node is stopped + wait $pid else kill $pid1 $pid2 $pid3 + # wait until all nodes are stopped + wait $pid1 $pid2 $pid3 fi } @@ -94,6 +98,8 @@ get_receipt() { # do not stop on errors set +e + # wait for a total of (0.4 * 100) = 40 seconds + while true; do output=$(../bin/aergocli receipt get $txhash 2>&1 > receipt.json) @@ -102,7 +108,7 @@ get_receipt() { if [[ $output == *"tx not found"* ]]; then sleep 0.4 counter=$((counter+1)) - if [ $counter -gt 10 ]; then + if [ $counter -gt 100 ]; then echo "Error: tx not found: $txhash" exit 1 fi @@ -118,6 +124,26 @@ get_receipt() { set -e } +get_internal_operations() { + txhash=$1 + # do not stop on errors + set +e + + output=$(../bin/aergocli operations $txhash --port $query_port 2>&1 > internal_operations.json) + + #echo "output: $output" + + if [[ $output == *"No internal operations found for this transaction"* ]]; then + echo -n "" > internal_operations.json + elif [[ -n $output ]]; then + echo "Error: $output" + exit 1 + fi + + # stop on errors + set -e +} + assert_equals() { local var="$1" local expected="$2" diff --git a/tests/config-node3.toml b/tests/config-node3.toml index 888aca265..9ee111abb 100644 --- a/tests/config-node3.toml +++ b/tests/config-node3.toml @@ -10,6 +10,7 @@ nstls = false nscert = "" nskey = "" nsallowcors = false +internal_operations = true [p2p] netprotocoladdr = "127.0.0.1" diff --git a/tests/config-sbp.toml b/tests/config-sbp.toml index 10b3304d7..03b1e22a7 100644 --- a/tests/config-sbp.toml +++ b/tests/config-sbp.toml @@ -18,6 +18,7 @@ nscert = "" nskey = "" nscacert = "" nsallowcors = false +internal_operations = true [p2p] # Set address and port to which the inbound peers connect, and don't set loopback address or private network unless used in local network diff --git a/tests/run_tests.sh b/tests/run_tests.sh index c87aed23b..9603d0e86 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -148,6 +148,7 @@ check ./test-contract-deploy.sh check ./test-pcall-events.sh check ./test-transaction-types.sh check ./test-name-service.sh +check ./test-internal-operations.sh # change the hardfork version set_version 4 @@ -165,6 +166,7 @@ check ./test-transaction-types.sh check ./test-name-service.sh check ./test-multicall.sh check ./test-disabled-functions.sh +check ./test-internal-operations.sh # terminate the server process echo "" diff --git a/tests/test-internal-operations.sh b/tests/test-internal-operations.sh new file mode 100755 index 000000000..e55d8db4a --- /dev/null +++ b/tests/test-internal-operations.sh @@ -0,0 +1,371 @@ +set -e +source common.sh + +fork_version=$1 + + +cat > test-args.lua << EOF +function do_call(...) + local args = {...} + return contract.call(unpack(args)) +end + +function do_delegate_call(...) + local args = {...} + return contract.delegatecall(unpack(args)) +end + +function do_multicall(script) + return contract.delegatecall("multicall", script) +end + +function do_event(...) + local args = {...} + return contract.event(unpack(args)) +end + +function do_deploy(...) + local args = {...} + return contract.deploy(unpack(args)) +end + +function do_send(...) + local args = {...} + return contract.send(unpack(args)) +end + +function constructor(...) + local args = {...} + contract.event("constructor", unpack(args)) +end + +function default() + contract.event("aergo received", system.getAmount()) + return system.getAmount() +end + +abi.register(do_call, do_delegate_call, do_multicall, do_event, do_deploy, do_send, constructor) +abi.payable(default) +EOF + + +echo "-- deploy test contract --" + +deploy test-args.lua +rm test-args.lua + +get_receipt $txhash + +status=$(cat receipt.json | jq .status | sed 's/"//g') +test_args_address=$(cat receipt.json | jq .contractAddress | sed 's/"//g') + +assert_equals "$status" "CREATED" + +get_internal_operations $txhash +internal_operations=$(cat internal_operations.json) + +assert_equals "$internal_operations" '{ + "call": { + "contract": "'$test_args_address'", + "function": "constructor", + "operations": [ + { + "args": [ + "constructor", + "[]" + ], + "op": "event" + } + ] + } +}' + + +echo "-- call test contract --" + +#txhash=$(../bin/aergocli --keystore . --password bmttest \ +# contract call AmPpcKvToDCUkhT1FJjdbNvR4kNDhLFJGHkSqfjWe3QmHm96qv4R \ +# $test_args_address do_event '["test",123,4.56,"test",true,{"_bignum":"1234567890"}]' | jq .hash | sed 's/"//g') + +txhash=$(../bin/aergocli --keystore . --password bmttest \ + contract call AmPpcKvToDCUkhT1FJjdbNvR4kNDhLFJGHkSqfjWe3QmHm96qv4R \ + $test_args_address do_call '["'$test_args_address'","do_event","test",123,4.56,"test",true,{"_bignum":"1234567890"}]' | jq .hash | sed 's/"//g') + +get_receipt $txhash + +status=$(cat receipt.json | jq .status | sed 's/"//g') +ret=$(cat receipt.json | jq .ret | sed 's/"//g') +gasUsed=$(cat receipt.json | jq .gasUsed | sed 's/"//g') + +assert_equals "$status" "SUCCESS" + +get_internal_operations $txhash +internal_operations=$(cat internal_operations.json) + +assert_equals "$internal_operations" '{ + "call": { + "args": [ + "'$test_args_address'", + "do_event", + "test", + 123, + 4.56, + "test", + true, + { + "_bignum": "1234567890" + } + ], + "contract": "'$test_args_address'", + "function": "do_call", + "operations": [ + { + "args": [ + "'$test_args_address'", + "do_event", + "[\"test\",123,4.56,\"test\",true,{\"_bignum\":\"1234567890\"}]" + ], + "call": { + "args": [ + "test", + 123, + 4.56, + "test", + true, + { + "_bignum": "1234567890" + } + ], + "contract": "'$test_args_address'", + "function": "do_event", + "operations": [ + { + "args": [ + "test", + "[123,4.56,\"test\",true,{\"_bignum\":\"1234567890\"}]" + ], + "op": "event" + } + ] + }, + "op": "call" + } + ] + } +}' + + + +echo "-- deploy ARC1 factory --" + +deploy ../contract/vm_dummy/test_files/arc1-factory.lua + +get_receipt $txhash + +status=$(cat receipt.json | jq .status | sed 's/"//g') +arc1_address=$(cat receipt.json | jq .contractAddress | sed 's/"//g') + +assert_equals "$status" "CREATED" + + +echo "-- deploy ARC2 factory --" + +deploy ../contract/vm_dummy/test_files/arc2-factory.lua + +get_receipt $txhash + +status=$(cat receipt.json | jq .status | sed 's/"//g') +arc2_address=$(cat receipt.json | jq .contractAddress | sed 's/"//g') + +assert_equals "$status" "CREATED" + + +echo "-- deploy caller contract --" + +get_deploy_args ../contract/vm_dummy/test_files/token-deployer.lua + +txhash=$(../bin/aergocli --keystore . --password bmttest \ + contract deploy AmPpcKvToDCUkhT1FJjdbNvR4kNDhLFJGHkSqfjWe3QmHm96qv4R \ + $deploy_args "[\"$arc1_address\",\"$arc2_address\"]" | jq .hash | sed 's/"//g') + +get_receipt $txhash + +status=$(cat receipt.json | jq .status | sed 's/"//g') +deployer_address=$(cat receipt.json | jq .contractAddress | sed 's/"//g') + +assert_equals "$status" "CREATED" + +get_internal_operations $txhash + +internal_operations=$(cat internal_operations.json) + +assert_equals "$internal_operations" '{ + "call": { + "args": [ + "'$arc1_address'", + "'$arc2_address'" + ], + "contract": "'$deployer_address'", + "function": "constructor" + } +}' + + + +echo "-- transfer 1 aergo to the contract --" + +txhash=$(../bin/aergocli --keystore . --password bmttest \ + sendtx --from AmPpcKvToDCUkhT1FJjdbNvR4kNDhLFJGHkSqfjWe3QmHm96qv4R --to $test_args_address --amount 1aergo \ + | jq .hash | sed 's/"//g') + +get_receipt $txhash + +status=$(cat receipt.json | jq .status | sed 's/"//g') +ret=$(cat receipt.json | jq .ret | sed 's/"//g') + +assert_equals "$status" "SUCCESS" +assert_equals "$ret" "1000000000000000000" + + +get_internal_operations $txhash + +internal_operations=$(cat internal_operations.json) + +assert_equals "$internal_operations" '{ + "call": { + "amount": "1000000000000000000", + "contract": "'$test_args_address'", + "function": "default", + "operations": [ + { + "args": [ + "aergo received", + "[\"1000000000000000000\"]" + ], + "op": "event" + } + ] + } +}' + + +if [ "$fork_version" -lt "4" ]; then + exit 0 # composable transactions are only available from hard fork 4 +fi + +echo "-- multicall --" + +script='[ + ["send","'$test_args_address'","0.125 aergo"], + ["get aergo balance","'$test_args_address'"], + ["to string"], + ["store result as","amount"], + ["call","'$test_args_address'","do_send","%my account address%","0.125 aergo"], + ["return","%amount%"] +]' + +txhash=$(../bin/aergocli --keystore . --password bmttest \ + contract multicall AmPpcKvToDCUkhT1FJjdbNvR4kNDhLFJGHkSqfjWe3QmHm96qv4R "$script" | jq .hash | sed 's/"//g') + +get_receipt $txhash + +status=$(cat receipt.json | jq .status | sed 's/"//g') +ret=$(cat receipt.json | jq .ret | sed 's/"//g') + +assert_equals "$status" "SUCCESS" + +if [ "$consensus" == "sbp" ]; then + assert_equals "$ret" "20001125000000000000000" +else + assert_equals "$ret" "1125000000000000000" +fi + +get_internal_operations $txhash + +internal_operations=$(cat internal_operations.json) + +assert_equals "$internal_operations" '{ + "call": { + "args": [ + [ + [ + "send", + "'$test_args_address'", + "0.125 aergo" + ], + [ + "get aergo balance", + "'$test_args_address'" + ], + [ + "to string" + ], + [ + "store result as", + "amount" + ], + [ + "call", + "'$test_args_address'", + "do_send", + "%my account address%", + "0.125 aergo" + ], + [ + "return", + "%amount%" + ] + ] + ], + "contract": "AmPpcKvToDCUkhT1FJjdbNvR4kNDhLFJGHkSqfjWe3QmHm96qv4R", + "function": "execute", + "operations": [ + { + "amount": "0.125 aergo", + "args": [ + "'$test_args_address'" + ], + "call": { + "amount": "125000000000000000", + "contract": "'$test_args_address'", + "function": "default", + "operations": [ + { + "args": [ + "aergo received", + "[\"125000000000000000\"]" + ], + "op": "event" + } + ] + }, + "op": "send" + }, + { + "args": [ + "'$test_args_address'", + "do_send", + "[\"AmPpcKvToDCUkhT1FJjdbNvR4kNDhLFJGHkSqfjWe3QmHm96qv4R\",\"0.125 aergo\"]" + ], + "call": { + "args": [ + "AmPpcKvToDCUkhT1FJjdbNvR4kNDhLFJGHkSqfjWe3QmHm96qv4R", + "0.125 aergo" + ], + "contract": "'$test_args_address'", + "function": "do_send", + "operations": [ + { + "amount": "0.125 aergo", + "args": [ + "AmPpcKvToDCUkhT1FJjdbNvR4kNDhLFJGHkSqfjWe3QmHm96qv4R" + ], + "op": "send" + } + ] + }, + "op": "call" + } + ] + } +}' diff --git a/types/dbkey/key.go b/types/dbkey/key.go index b25be2cdc..bc15f1740 100644 --- a/types/dbkey/key.go +++ b/types/dbkey/key.go @@ -25,6 +25,10 @@ func Receipts(blockHash []byte, blockNo types.BlockNo) []byte { return key } +func InternalOps(blockNo types.BlockNo) []byte { + return append([]byte(internalOpsPrefix), types.BlockNoToBytes(blockNo)...) +} + //---------------------------------------------------------------------------------// // metadata diff --git a/types/dbkey/schema.go b/types/dbkey/schema.go index 311481c58..6bb883cb1 100644 --- a/types/dbkey/schema.go +++ b/types/dbkey/schema.go @@ -9,6 +9,7 @@ const ( // chain const ( receiptsPrefix = "r" + internalOpsPrefix = "i" ) // metadata diff --git a/types/message/blockchainmsg.go b/types/message/blockchainmsg.go index df88cd5fc..462e91606 100644 --- a/types/message/blockchainmsg.go +++ b/types/message/blockchainmsg.go @@ -93,6 +93,14 @@ type GetReceiptsByNo struct { } type GetReceiptsByNoRsp GetReceiptsRsp +type GetInternalOperations struct { + BlockNo types.BlockNo +} +type GetInternalOperationsRsp struct { + Operations string + Err error +} + type GetABI struct { Contract []byte }