From 515c03daba411361518b168f63774878b6893c18 Mon Sep 17 00:00:00 2001 From: otherview Date: Wed, 28 Aug 2024 15:51:33 +0100 Subject: [PATCH 01/13] Adding transaction_call endpoint --- api/api.go | 2 +- api/transactions/transactions.go | 103 ++++++++++++++++-- api/transactions/transactions_test.go | 119 ++++++++++++++++++++- api/transactions/types.go | 144 ++++++++++++++++++++++++-- runtime/resolved_tx.go | 36 +++++++ runtime/runtime.go | 128 +++++++++++++++++++++++ 6 files changed, 515 insertions(+), 17 deletions(-) diff --git a/api/api.go b/api/api.go index 0ac399295..b666e89ad 100644 --- a/api/api.go +++ b/api/api.go @@ -81,7 +81,7 @@ func New( } blocks.New(repo, bft). Mount(router, "/blocks") - transactions.New(repo, txPool). + transactions.New(repo, stater, txPool, bft). Mount(router, "/transactions") debug.New(repo, stater, forkConfig, callGasLimit, allowCustomTracer, bft, allowedTracers). Mount(router, "/debug") diff --git a/api/transactions/transactions.go b/api/transactions/transactions.go index 85404cf21..322641094 100644 --- a/api/transactions/transactions.go +++ b/api/transactions/transactions.go @@ -6,6 +6,7 @@ package transactions import ( + "fmt" "net/http" "github.com/ethereum/go-ethereum/common/hexutil" @@ -13,20 +14,31 @@ import ( "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/vechain/thor/v2/api/utils" + "github.com/vechain/thor/v2/bft" + "github.com/vechain/thor/v2/block" "github.com/vechain/thor/v2/chain" + "github.com/vechain/thor/v2/runtime" + "github.com/vechain/thor/v2/state" "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/txpool" + "github.com/vechain/thor/v2/xenv" ) +const maxTxSize = 64 * 1024 + type Transactions struct { - repo *chain.Repository - pool *txpool.TxPool + repo *chain.Repository + pool *txpool.TxPool + stater *state.Stater + bft bft.Finalizer } -func New(repo *chain.Repository, pool *txpool.TxPool) *Transactions { +func New(repo *chain.Repository, stater *state.Stater, pool *txpool.TxPool, bft bft.Finalizer) *Transactions { return &Transactions{ - repo, - pool, + repo: repo, + stater: stater, + pool: pool, + bft: bft, } } @@ -76,7 +88,7 @@ func (t *Transactions) getTransactionByID(txID thor.Bytes32, head thor.Bytes32, if t.repo.IsNotFound(err) { if allowPending { if pending := t.pool.Get(txID); pending != nil { - return convertTransaction(pending, nil), nil + return ConvertTransaction(pending, nil), nil } } return nil, nil @@ -88,7 +100,7 @@ func (t *Transactions) getTransactionByID(txID thor.Bytes32, head thor.Bytes32, if err != nil { return nil, err } - return convertTransaction(tx, summary.Header), nil + return ConvertTransaction(tx, summary.Header), nil } // GetTransactionReceiptByID get tx's receipt @@ -114,6 +126,7 @@ func (t *Transactions) getTransactionReceiptByID(txID thor.Bytes32, head thor.By return convertReceipt(receipt, summary.Header, tx) } + func (t *Transactions) handleSendTransaction(w http.ResponseWriter, req *http.Request) error { var rawTx *RawTx if err := utils.ParseJSON(req.Body, &rawTx); err != nil { @@ -214,6 +227,78 @@ func (t *Transactions) parseHead(head string) (thor.Bytes32, error) { return h, nil } +func (t *Transactions) txCall( + txCallMsg *Transaction, + header *block.Header, + st *state.State, +) (*CallReceipt, error) { + callAddr := txCallMsg.Origin + if callAddr.String() == (thor.Address{}).String() { + return nil, fmt.Errorf("no Origin address specified") + } + + // todo handle the txCallMsg.Delegator + txCallData, err := convertToTxTransaction(txCallMsg) + if err != nil { + return nil, fmt.Errorf("unable to convert transaction: %w", err) + } + + // validation from the mempool + // TODO add more validations that are mandatory + switch { + case txCallMsg.ChainTag != t.repo.ChainTag(): + return nil, fmt.Errorf("chain tag mismatch") + case txCallMsg.Size > maxTxSize: + return nil, fmt.Errorf("size too large") + } + if err = txCallData.TestFeatures(header.TxsFeatures()); err != nil { + return nil, err + } + + signer, _ := header.Signer() + rt := runtime.New(t.repo.NewChain(header.ParentID()), st, + &xenv.BlockContext{ + Beneficiary: header.Beneficiary(), + Signer: signer, + Number: header.Number(), + Time: header.Timestamp(), + GasLimit: header.GasLimit(), + TotalScore: header.TotalScore(), + }, + thor.NoFork) + + receipt, err := rt.CallTransaction(txCallData, &callAddr, nil) // TODO hook delegator + if err != nil { + // TODO add some metric here + return convertErrorCallReceipt(err, txCallMsg, &callAddr) + } + + return convertCallReceipt(receipt, txCallMsg, &callAddr) +} + +func (t *Transactions) handleCallTransaction(w http.ResponseWriter, req *http.Request) error { + txCallMsg := &Transaction{} + if err := utils.ParseJSON(req.Body, &txCallMsg); err != nil { + return utils.BadRequest(errors.WithMessage(err, "body")) + } + revision, err := utils.ParseRevision(req.URL.Query().Get("revision"), true) + if err != nil { + return utils.BadRequest(errors.WithMessage(err, "revision")) + } + summary, st, err := utils.GetSummaryAndState(revision, t.repo, t.bft, t.stater) + if err != nil { + if t.repo.IsNotFound(err) { + return utils.BadRequest(errors.WithMessage(err, "revision")) + } + return err + } + + results, err := t.txCall(txCallMsg, summary.Header, st) + if err != nil { + return err + } + return utils.WriteJSON(w, results) +} func (t *Transactions) Mount(root *mux.Router, pathPrefix string) { sub := root.PathPrefix(pathPrefix).Subrouter() @@ -229,4 +314,8 @@ func (t *Transactions) Mount(root *mux.Router, pathPrefix string) { Methods(http.MethodGet). Name("transactions_get_receipt"). HandlerFunc(utils.WrapHandlerFunc(t.handleGetTransactionReceiptByID)) + sub.Path("/call"). + Methods(http.MethodPost). + Name("transactions_call_tx"). + HandlerFunc(utils.WrapHandlerFunc(t.handleCallTransaction)) } diff --git a/api/transactions/transactions_test.go b/api/transactions/transactions_test.go index 52b127a56..94a908bd7 100644 --- a/api/transactions/transactions_test.go +++ b/api/transactions/transactions_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/vechain/thor/v2/api/transactions" "github.com/vechain/thor/v2/chain" + "github.com/vechain/thor/v2/cmd/thor/solo" "github.com/vechain/thor/v2/genesis" "github.com/vechain/thor/v2/muxdb" "github.com/vechain/thor/v2/packer" @@ -75,6 +76,14 @@ func TestTransaction(t *testing.T) { } { t.Run(name, tt) } + + // Call transaction + for name, tt := range map[string]func(*testing.T){ + "callTx": callTx, + "invalidCallTx": invalidCallTx, + } { + t.Run(name, tt) + } } func getTx(t *testing.T) { @@ -260,6 +269,114 @@ func handleGetTransactionReceiptByIDWithNonExistingHead(t *testing.T) { assert.Equal(t, "head: leveldb: not found", strings.TrimSpace(string(res))) } +func callTx(t *testing.T) { + var blockRef = tx.NewBlockRef(0) + var chainTag = repo.ChainTag() + var expiration = uint32(10) + var gas = uint64(21000) + + for _, testTx := range []*tx.Transaction{ + new(tx.Builder). + BlockRef(blockRef). + ChainTag(chainTag). + Expiration(expiration). + Gas(gas). + Build(), + new(tx.Builder). + BlockRef(blockRef). + ChainTag(chainTag). + Expiration(expiration). + Clause(tx.NewClause(&genesis.DevAccounts()[0].Address).WithValue(big.NewInt(1234))). + Gas(gas). + Build(), + new(tx.Builder). + BlockRef(blockRef). + ChainTag(chainTag). + Expiration(expiration). + Clause( + tx.NewClause(&genesis.DevAccounts()[0].Address).WithValue(big.NewInt(1234)), + ). + Clause( + tx.NewClause(&genesis.DevAccounts()[0].Address).WithValue(big.NewInt(1234)), + ). + Gas(2 * gas). // 2 clauses of value transfer + Build(), + } { + txCall := transactions.ConvertCallTransaction(testTx, nil, &genesis.DevAccounts()[0].Address, nil) + + res := httpPostAndCheckResponseStatus(t, ts.URL+"/transactions/call", txCall, 200) + var callReceipt transactions.CallReceipt + if err := json.Unmarshal(res, &callReceipt); err != nil { + t.Fatal(err) + } + validateTxCall(t, testTx, &callReceipt, &genesis.DevAccounts()[0].Address, nil) + } +} + +func invalidCallTx(t *testing.T) { + var chainTag = repo.ChainTag() + //var expiration = uint32(10) + var gas = uint64(21000) + var sendAddr = &genesis.DevAccounts()[0].Address + + for _, tc := range []struct { + testTx *transactions.Transaction + errMsg string + }{ + { + testTx: transactions.ConvertCallTransaction(new(tx.Builder). + Gas(gas). + Build(), + nil, sendAddr, nil), + errMsg: "chain tag mismatch", + }, + //{ + // testTx: transactions.ConvertCallTransaction(new(tx.Builder). + // ChainTag(chainTag). + // Expiration(0). + // Gas(gas). + // Build(), + // nil, sendAddr, nil), + // errMsg: "chain tag mismatch", + //}, + { + testTx: transactions.ConvertCallTransaction(new(tx.Builder). + ChainTag(chainTag). + Gas(gas). + Build(), + nil, &thor.Address{}, nil), + errMsg: "no Origin address specified", + }, + { + testTx: transactions.ConvertCallTransaction(new(tx.Builder). + ChainTag(chainTag). + Gas(gas). + Clause(tx.NewClause(nil).WithData(make([]byte, 64*1024+1))). + Build(), + nil, sendAddr, nil), + errMsg: "size too large", + }, + } { + t.Run(tc.errMsg, func(t *testing.T) { + res := httpPostAndCheckResponseStatus(t, ts.URL+"/transactions/call", tc.testTx, 500) + assert.Equal(t, tc.errMsg, strings.TrimSpace(string(res))) + }) + } +} + +func validateTxCall(t *testing.T, callTx *tx.Transaction, callRcpt *transactions.CallReceipt, callAddr, delegator *thor.Address) { + assert.Equal(t, callTx.ID(), callRcpt.TxID) + assert.Equal(t, *callAddr, callRcpt.TxOrigin) + + if delegator != nil { + assert.Equal(t, delegator.String(), callRcpt.GasPayer.String()) + } else { + assert.Equal(t, callAddr.String(), callRcpt.GasPayer.String()) + } + + assert.Equal(t, len(callTx.Clauses()), len(callRcpt.Outputs)) +} + func httpPostAndCheckResponseStatus(t *testing.T, url string, obj interface{}, responseStatusCode int) []byte { data, err := json.Marshal(obj) if err != nil { @@ -349,7 +466,7 @@ func initTransactionServer(t *testing.T) { t.Fatal(e) } - transactions.New(repo, mempool).Mount(router, "/transactions") + transactions.New(repo, stater, mempool, solo.NewBFTEngine(repo)).Mount(router, "/transactions") ts = httptest.NewServer(router) } diff --git a/api/transactions/types.go b/api/transactions/types.go index 7aa3f822e..793d8783c 100644 --- a/api/transactions/types.go +++ b/api/transactions/types.go @@ -7,6 +7,7 @@ package transactions import ( "fmt" + "math/big" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" @@ -83,12 +84,9 @@ type rawTransaction struct { Meta *TxMeta `json:"meta"` } -// convertTransaction convert a raw transaction into a json format transaction -func convertTransaction(tx *tx.Transaction, header *block.Header) *Transaction { - //tx origin - origin, _ := tx.Origin() - delegator, _ := tx.Delegator() - +// ConvertCallTransaction convert a raw transaction into a json format transaction +// allows to specify the origin and delegator +func ConvertCallTransaction(tx *tx.Transaction, header *block.Header, origin *thor.Address, delegator *thor.Address) *Transaction { cls := make(Clauses, len(tx.Clauses())) for i, c := range tx.Clauses() { cls[i] = convertClause(c) @@ -97,7 +95,7 @@ func convertTransaction(tx *tx.Transaction, header *block.Header) *Transaction { t := &Transaction{ ChainTag: tx.ChainTag(), ID: tx.ID(), - Origin: origin, + Origin: *origin, BlockRef: hexutil.Encode(br[:]), Expiration: tx.Expiration(), Nonce: math.HexOrDecimal64(tx.Nonce()), @@ -119,6 +117,14 @@ func convertTransaction(tx *tx.Transaction, header *block.Header) *Transaction { return t } +// ConvertTransaction convert a raw transaction into a json format transaction +func ConvertTransaction(tx *tx.Transaction, header *block.Header) *Transaction { + //tx origin + origin, _ := tx.Origin() + delegator, _ := tx.Delegator() + return ConvertCallTransaction(tx, header, &origin, delegator) +} + type TxMeta struct { BlockID thor.Bytes32 `json:"blockID"` BlockNumber uint32 `json:"blockNumber"` @@ -144,6 +150,19 @@ type Receipt struct { Outputs []*Output `json:"outputs"` } +// CallReceipt for json marshal +type CallReceipt struct { + GasUsed uint64 `json:"gasUsed"` + GasPayer thor.Address `json:"gasPayer"` + Paid *math.HexOrDecimal256 `json:"paid"` + Reward *math.HexOrDecimal256 `json:"reward"` + Reverted bool `json:"reverted"` + TxID thor.Bytes32 `json:"txID"` + TxOrigin thor.Address `json:"txOrigin"` + Outputs []*Output `json:"outputs"` + VmError string `json:"vmError"` +} + // Output output of clause execution. type Output struct { ContractAddress *thor.Address `json:"contractAddress"` @@ -165,7 +184,7 @@ type Transfer struct { Amount *math.HexOrDecimal256 `json:"amount"` } -// ConvertReceipt convert a raw clause into a jason format clause +// ConvertReceipt convert a raw clause into a json format clause func convertReceipt(txReceipt *tx.Receipt, header *block.Header, tx *tx.Transaction) (*Receipt, error) { reward := math.HexOrDecimal256(*txReceipt.Reward) paid := math.HexOrDecimal256(*txReceipt.Paid) @@ -220,3 +239,112 @@ func convertReceipt(txReceipt *tx.Receipt, header *block.Header, tx *tx.Transact } return receipt, nil } + +// convertCallReceipt converts a tx.Receipt into a transaction.CallReceipt +func convertCallReceipt( + txReceipt *tx.Receipt, + tx *Transaction, + callAddr *thor.Address, +) (*CallReceipt, error) { + reward := math.HexOrDecimal256(*txReceipt.Reward) + paid := math.HexOrDecimal256(*txReceipt.Paid) + origin := callAddr + + receipt := &CallReceipt{ + GasUsed: txReceipt.GasUsed, + GasPayer: txReceipt.GasPayer, + Paid: &paid, + Reward: &reward, + Reverted: txReceipt.Reverted, + TxOrigin: *origin, + TxID: tx.ID, + } + receipt.Outputs = make([]*Output, len(txReceipt.Outputs)) + for i, output := range txReceipt.Outputs { + clause := tx.Clauses[i] + var contractAddr *thor.Address + if clause.To == nil { + cAddr := thor.CreateContractAddress(tx.ID, uint32(i), 0) + contractAddr = &cAddr + } + otp := &Output{contractAddr, + make([]*Event, len(output.Events)), + make([]*Transfer, len(output.Transfers)), + } + for j, txEvent := range output.Events { + event := &Event{ + Address: txEvent.Address, + Data: hexutil.Encode(txEvent.Data), + } + event.Topics = make([]thor.Bytes32, len(txEvent.Topics)) + copy(event.Topics, txEvent.Topics) + otp.Events[j] = event + } + for j, txTransfer := range output.Transfers { + transfer := &Transfer{ + Sender: txTransfer.Sender, + Recipient: txTransfer.Recipient, + Amount: (*math.HexOrDecimal256)(txTransfer.Amount), + } + otp.Transfers[j] = transfer + } + receipt.Outputs[i] = otp + } + return receipt, nil +} + +func convertErrorCallReceipt( + vmErr error, + tx *Transaction, + callAddr *thor.Address, +) (*CallReceipt, error) { + origin := callAddr + + receipt := &CallReceipt{ + Reverted: true, + TxOrigin: *origin, + TxID: tx.ID, + VmError: vmErr.Error(), + } + receipt.Outputs = make([]*Output, len(tx.Clauses)) + for i := range tx.Clauses { + clause := tx.Clauses[i] + var contractAddr *thor.Address + if clause.To == nil { + cAddr := thor.CreateContractAddress(tx.ID, uint32(i), 0) + contractAddr = &cAddr + } + + receipt.Outputs[i] = &Output{ContractAddress: contractAddr} + } + return receipt, nil +} + +// convertToTxTransaction converts a transaction.Transaction into a tx.Transaction +func convertToTxTransaction(incomingTx *Transaction) (*tx.Transaction, error) { + //blockRef, err := thor.ParseBytes32(incomingTx.BlockRef) + //if err != nil { + // return nil, fmt.Errorf("unable to parse block ref: %w", err) + //} + + convertedTxBuilder := new(tx.Builder). + ChainTag(incomingTx.ChainTag). + //Features(incomingTx). // TODO hook in the future + Nonce(uint64(incomingTx.Nonce)). + //BlockRef(tx.NewBlockRefFromID(blockRef)). // TODO hook in the future + Expiration(incomingTx.Expiration). + GasPriceCoef(incomingTx.GasPriceCoef). + Gas(incomingTx.Gas). + DependsOn(incomingTx.DependsOn) + + for _, c := range incomingTx.Clauses { + value := big.Int(c.Value) + dataVal, err := hexutil.Decode(c.Data) + if err != nil { + return nil, fmt.Errorf("unable to decode clause data: %w", err) + } + convertedTxBuilder.Clause(tx.NewClause(c.To).WithValue(&value).WithData(dataVal)) + } + + return convertedTxBuilder.Build(), nil +} diff --git a/runtime/resolved_tx.go b/runtime/resolved_tx.go index 3c3b797b0..ce1397c5e 100644 --- a/runtime/resolved_tx.go +++ b/runtime/resolved_tx.go @@ -67,6 +67,42 @@ func ResolveTransaction(tx *tx.Transaction) (*ResolvedTransaction, error) { }, nil } +// ResolveCallTransaction resolves the transaction and performs basic validation. +// Signed tx's will be ignored +func ResolveCallTransaction(tx *tx.Transaction, callAddr *thor.Address, delegator *thor.Address) (*ResolvedTransaction, error) { + var err error + + intrinsicGas, err := tx.IntrinsicGas() + if err != nil { + return nil, err + } + if tx.Gas() < intrinsicGas { + return nil, errors.New("intrinsic gas exceeds provided gas") + } + + clauses := tx.Clauses() + sumValue := new(big.Int) + for _, clause := range clauses { + value := clause.Value() + if value.Sign() < 0 { + return nil, errors.New("clause with negative value") + } + + sumValue.Add(sumValue, value) + if sumValue.Cmp(math.MaxBig256) > 0 { + return nil, errors.New("tx value too large") + } + } + + return &ResolvedTransaction{ + tx, + *callAddr, + delegator, + intrinsicGas, + clauses, + }, nil +} + // CommonTo returns common 'To' field of clauses if any. // Nil returned if no common 'To'. func (r *ResolvedTransaction) CommonTo() *thor.Address { diff --git a/runtime/runtime.go b/runtime/runtime.go index fac4d3096..efa6bd8f7 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -375,6 +375,134 @@ func (rt *Runtime) ExecuteTransaction(tx *tx.Transaction) (receipt *tx.Receipt, return executor.Finalize() } +func (rt *Runtime) CallTransaction(tx *tx.Transaction, callAddr *thor.Address, delegator *thor.Address) (receipt *tx.Receipt, err error) { + executor, err := rt.PrepareCallTransaction(tx, callAddr, delegator) + if err != nil { + return nil, err + } + for executor.HasNextClause() { + exec, _ := executor.PrepareNext() + if _, _, err := exec(); err != nil { + return nil, err + } + } + return executor.Finalize() +} + +// PrepareTransaction prepare to execute tx. +func (rt *Runtime) PrepareCallTransaction(tx *tx.Transaction, address *thor.Address, delegator *thor.Address) (*TransactionExecutor, error) { + resolvedTx, err := ResolveCallTransaction(tx, address, delegator) + if err != nil { + return nil, err + } + + baseGasPrice, gasPrice, payer, returnGas, err := resolvedTx.BuyGas(rt.state, rt.ctx.Time) + if err != nil { + return nil, err + } + + txCtx, err := resolvedTx.ToContext(gasPrice, payer, rt.ctx.Number, rt.chain.GetBlockID) + if err != nil { + return nil, err + } + + // ResolveTransaction has checked that tx.Gas() >= IntrinsicGas + leftOverGas := tx.Gas() - resolvedTx.IntrinsicGas + // checkpoint to be reverted when clause failure. + checkpoint := rt.state.NewCheckpoint() + + txOutputs := make([]*Tx.Output, 0, len(resolvedTx.Clauses)) + reverted := false + finalized := false + + hasNext := func() bool { + return !reverted && len(txOutputs) < len(resolvedTx.Clauses) + } + + return &TransactionExecutor{ + HasNextClause: hasNext, + PrepareNext: func() (exec func() (uint64, *Output, error), interrupt func()) { + nextClauseIndex := uint32(len(txOutputs)) + execFunc, interrupt := rt.PrepareClause(resolvedTx.Clauses[nextClauseIndex], nextClauseIndex, leftOverGas, txCtx) + + exec = func() (gasUsed uint64, output *Output, err error) { + output, _, err = execFunc() + if err != nil { + return 0, nil, err + } + gasUsed = leftOverGas - output.LeftOverGas + leftOverGas = output.LeftOverGas + + // Apply refund counter, capped to half of the used gas. + refund := gasUsed / 2 + if refund > output.RefundGas { + refund = output.RefundGas + } + + // won't overflow + leftOverGas += refund + + if output.VMErr != nil { + // vm exception here + // revert all executed clauses + rt.state.RevertTo(checkpoint) + reverted = true + txOutputs = nil + return + } + txOutputs = append(txOutputs, &Tx.Output{Events: output.Events, Transfers: output.Transfers}) + return + } + + return + }, + Finalize: func() (*Tx.Receipt, error) { + if hasNext() { + return nil, errors.New("not all clauses processed") + } + if finalized { + return nil, errors.New("already finalized") + } + finalized = true + + receipt := &Tx.Receipt{ + Reverted: reverted, + Outputs: txOutputs, + GasUsed: tx.Gas() - leftOverGas, + GasPayer: payer, + } + + receipt.Paid = new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), gasPrice) + + if err := returnGas(leftOverGas); err != nil { + return nil, err + } + + // reward + rewardRatio, err := builtin.Params.Native(rt.state).Get(thor.KeyRewardRatio) + if err != nil { + return nil, err + } + provedWork, err := tx.ProvedWork(rt.ctx.Number-1, rt.chain.GetBlockID) + if err != nil { + return nil, err + } + overallGasPrice := tx.OverallGasPrice(baseGasPrice, provedWork) + + reward := new(big.Int).SetUint64(receipt.GasUsed) + reward.Mul(reward, overallGasPrice) + reward.Mul(reward, rewardRatio) + reward.Div(reward, big.NewInt(1e18)) + if err := builtin.Energy.Native(rt.state, rt.ctx.Time).Add(rt.ctx.Beneficiary, reward); err != nil { + return nil, err + } + + receipt.Reward = reward + return receipt, nil + }, + }, nil +} + // PrepareTransaction prepare to execute tx. func (rt *Runtime) PrepareTransaction(tx *tx.Transaction) (*TransactionExecutor, error) { resolvedTx, err := ResolveTransaction(tx) From 8ff45c0ea9be16ec77c9863793ad16f272c901f4 Mon Sep 17 00:00:00 2001 From: otherview Date: Wed, 28 Aug 2024 16:48:30 +0100 Subject: [PATCH 02/13] passing forkconfig --- api/api.go | 2 +- api/transactions/transactions.go | 24 +++++++++++++----------- api/transactions/transactions_test.go | 4 ++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/api/api.go b/api/api.go index b666e89ad..0c94f736c 100644 --- a/api/api.go +++ b/api/api.go @@ -81,7 +81,7 @@ func New( } blocks.New(repo, bft). Mount(router, "/blocks") - transactions.New(repo, stater, txPool, bft). + transactions.New(repo, stater, txPool, bft, forkConfig). Mount(router, "/transactions") debug.New(repo, stater, forkConfig, callGasLimit, allowCustomTracer, bft, allowedTracers). Mount(router, "/debug") diff --git a/api/transactions/transactions.go b/api/transactions/transactions.go index 322641094..ef56c415e 100644 --- a/api/transactions/transactions.go +++ b/api/transactions/transactions.go @@ -27,18 +27,20 @@ import ( const maxTxSize = 64 * 1024 type Transactions struct { - repo *chain.Repository - pool *txpool.TxPool - stater *state.Stater - bft bft.Finalizer + repo *chain.Repository + pool *txpool.TxPool + stater *state.Stater + bft bft.Finalizer + forkConfig thor.ForkConfig } -func New(repo *chain.Repository, stater *state.Stater, pool *txpool.TxPool, bft bft.Finalizer) *Transactions { +func New(repo *chain.Repository, stater *state.Stater, pool *txpool.TxPool, bft bft.Finalizer, forkConfig thor.ForkConfig) *Transactions { return &Transactions{ - repo: repo, - stater: stater, - pool: pool, - bft: bft, + repo: repo, + stater: stater, + pool: pool, + bft: bft, + forkConfig: forkConfig, } } @@ -234,7 +236,7 @@ func (t *Transactions) txCall( ) (*CallReceipt, error) { callAddr := txCallMsg.Origin if callAddr.String() == (thor.Address{}).String() { - return nil, fmt.Errorf("no Origin address specified") + return nil, fmt.Errorf("no origin address specified") } // todo handle the txCallMsg.Delegator @@ -265,7 +267,7 @@ func (t *Transactions) txCall( GasLimit: header.GasLimit(), TotalScore: header.TotalScore(), }, - thor.NoFork) + t.forkConfig) receipt, err := rt.CallTransaction(txCallData, &callAddr, nil) // TODO hook delegator if err != nil { diff --git a/api/transactions/transactions_test.go b/api/transactions/transactions_test.go index 94a908bd7..f0d11eb60 100644 --- a/api/transactions/transactions_test.go +++ b/api/transactions/transactions_test.go @@ -345,7 +345,7 @@ func invalidCallTx(t *testing.T) { Gas(gas). Build(), nil, &thor.Address{}, nil), - errMsg: "no Origin address specified", + errMsg: "no origin address specified", }, { testTx: transactions.ConvertCallTransaction(new(tx.Builder). @@ -466,7 +466,7 @@ func initTransactionServer(t *testing.T) { t.Fatal(e) } - transactions.New(repo, stater, mempool, solo.NewBFTEngine(repo)).Mount(router, "/transactions") + transactions.New(repo, stater, mempool, solo.NewBFTEngine(repo), thor.NoFork).Mount(router, "/transactions") ts = httptest.NewServer(router) } From e3d3a0852db342a118d3844b61392086d7c06e9c Mon Sep 17 00:00:00 2001 From: otherview Date: Thu, 31 Oct 2024 18:01:39 +0000 Subject: [PATCH 03/13] Added docs + test + fixed thorclient Transaction method --- api/doc/thor.yaml | 171 ++++++++++++++++++++++++++ api/transactions/transactions.go | 6 +- api/transactions/transactions_test.go | 77 +++++++----- api/transactions/types.go | 16 +-- thorclient/httpclient/client.go | 20 +++ thorclient/httpclient/client_test.go | 20 +++ thorclient/thorclient.go | 15 ++- 7 files changed, 284 insertions(+), 41 deletions(-) diff --git a/api/doc/thor.yaml b/api/doc/thor.yaml index 732a5f1a3..93eda73d3 100644 --- a/api/doc/thor.yaml +++ b/api/doc/thor.yaml @@ -191,6 +191,177 @@ paths: type: string example: 'Invalid transaction ID' + /transactions/call: + post: + parameters: + - $ref: '#/components/parameters/HeadInQuery' + tags: + - Transactions + summary: Execute a transaction locally + description: | + This endpoint allows you to execute a transaction locally. It simulates the transaction execution without submitting it to the blockchain. + + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: Transaction ID placeholder. + example: '0x0000000000000000000000000000000000000000000000000000000000000000' + chainTag: + type: integer + description: Chain tag identifier. + example: 246 + blockRef: + type: string + description: Block reference. + example: '0x0000000000000000' + expiration: + type: integer + description: Expiration time for the transaction. + example: 10 + clauses: + type: array + description: An array of actions the transaction will perform. + items: + type: object + properties: + to: + type: string + description: Recipient address. + example: '0xf077b491b355e64048ce21e3a6fc4751eeea77fa' + value: + type: string + description: Amount to transfer, in hexadecimal. + example: '0x4d2' + data: + type: string + description: Input data for the clause. + example: '0x' + gasPriceCoef: + type: integer + description: Coefficient for gas price. + example: 0 + gas: + type: integer + description: Gas limit. + example: 21000 + origin: + type: string + description: Origin address. + example: '0xf077b491b355e64048ce21e3a6fc4751eeea77fa' + delegator: + type: string + description: Sponsor or delegator address, if any. + nullable: true + example: null + nonce: + type: string + description: Transaction nonce. + example: '0x0' + dependsOn: + type: string + description: Dependent transaction ID, if any. + nullable: true + example: null + size: + type: integer + description: Size of the transaction. + example: 40 + meta: + type: string + description: Metadata for the transaction, if any. + nullable: true + example: null + example: + id: '0x0000000000000000000000000000000000000000000000000000000000000000' + chainTag: 246 + blockRef: '0x0000000000000000' + expiration: 10 + clauses: + - to: '0xf077b491b355e64048ce21e3a6fc4751eeea77fa' + value: '0x4d2' + data: '0x' + gasPriceCoef: 0 + gas: 21000 + origin: '0xf077b491b355e64048ce21e3a6fc4751eeea77fa' + delegator: null + nonce: '0x0' + dependsOn: null + size: 40 + meta: null + + responses: + '200': + description: Execution result + content: + application/json: + schema: + type: object + properties: + gasUsed: + type: integer + description: Total gas used. + example: 21000 + gasPayer: + type: string + description: Address that paid the gas fee. + example: '0xf077b491b355e64048ce21e3a6fc4751eeea77fa' + paid: + type: string + description: Total amount paid for gas, in hexadecimal. + example: '0x2ea11e32ad50000' + reward: + type: string + description: Gas reward, in hexadecimal. + example: '0xdfd22a8cd98000' + reverted: + type: boolean + description: Indicates if the transaction was reverted. + example: false + txID: + type: string + description: Transaction ID. + example: '0x0000000000000000000000000000000000000000000000000000000000000000' + txOrigin: + type: string + description: Origin address. + example: '0xf077b491b355e64048ce21e3a6fc4751eeea77fa' + outputs: + type: array + description: Details of outputs produced by each clause. + items: + type: object + properties: + contractAddress: + type: string + description: Contract address, if applicable. + nullable: true + example: null + events: + type: array + items: + $ref: '#/components/schemas/Event' + transfers: + type: array + items: + $ref: '#/components/schemas/Transfer' + vmError: + type: string + description: Virtual machine error message, if any. + example: '' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: 'Invalid transaction request' + /transactions/{id}/receipt: get: parameters: diff --git a/api/transactions/transactions.go b/api/transactions/transactions.go index 0b063bddb..f75c0f920 100644 --- a/api/transactions/transactions.go +++ b/api/transactions/transactions.go @@ -90,7 +90,7 @@ func (t *Transactions) getTransactionByID(txID thor.Bytes32, head thor.Bytes32, if t.repo.IsNotFound(err) { if allowPending { if pending := t.pool.Get(txID); pending != nil { - return ConvertTransaction(pending, nil), nil + return convertSignedCoreTransaction(pending, nil), nil } } return nil, nil @@ -102,7 +102,7 @@ func (t *Transactions) getTransactionByID(txID thor.Bytes32, head thor.Bytes32, if err != nil { return nil, err } - return ConvertTransaction(tx, summary.Header), nil + return convertSignedCoreTransaction(tx, summary.Header), nil } // GetTransactionReceiptByID get tx's receipt @@ -239,7 +239,7 @@ func (t *Transactions) txCall( } // todo handle the txCallMsg.Delegator - txCallData, err := convertToTxTransaction(txCallMsg) + txCallData, err := ConvertCallTransaction(txCallMsg) if err != nil { return nil, fmt.Errorf("unable to convert transaction: %w", err) } diff --git a/api/transactions/transactions_test.go b/api/transactions/transactions_test.go index f24f976a1..fd5ead4b0 100644 --- a/api/transactions/transactions_test.go +++ b/api/transactions/transactions_test.go @@ -81,8 +81,9 @@ func TestTransaction(t *testing.T) { // Call transaction for name, tt := range map[string]func(*testing.T){ - "callTx": callTx, - "invalidCallTx": invalidCallTx, + "callTx": callTx, + "invalidCallTx": invalidCallTx, + "callExistingTx": callExistingTx, } { t.Run(name, tt) } @@ -138,12 +139,9 @@ func sendTx(t *testing.T) { t.Fatal(err) } - res := httpPostAndCheckResponseStatus(t, "/transactions", transactions.RawTx{Raw: hexutil.Encode(rlpTx)}, 200) - var txObj map[string]string - if err = json.Unmarshal(res, &txObj); err != nil { - t.Fatal(err) - } - assert.Equal(t, trx.ID().String(), txObj["id"], "should be the same transaction id") + res, err := tclient.SendRawTransaction(rlpTx) + require.NoError(t, err) + assert.Equal(t, res.ID.String(), trx.ID().String(), "should be the same transaction id") } func getTxWithBadID(t *testing.T) { @@ -303,17 +301,32 @@ func callTx(t *testing.T) { Gas(2 * gas). // 2 clauses of value transfer Build(), } { - txCall := transactions.ConvertCallTransaction(testTx, nil, &genesis.DevAccounts()[0].Address, nil) + txCall := transactions.ConvertCoreTransaction(testTx, nil, &genesis.DevAccounts()[0].Address, nil) + callReceipt, err := tclient.CallTransaction(txCall, nil) + require.NoError(t, err) - res := httpPostAndCheckResponseStatus(t, "/transactions/call", txCall, 200) - var callReceipt transactions.CallReceipt - if err := json.Unmarshal(res, &callReceipt); err != nil { - t.Fatal(err) - } - validateTxCall(t, testTx, &callReceipt, &genesis.DevAccounts()[0].Address, nil) + validateCoreTxCall(t, testTx, callReceipt, &genesis.DevAccounts()[0].Address, nil) } } +func callExistingTx(t *testing.T) { + // fetch an existing transaction + existingTxID := transaction.ID() + testTx, err := tclient.Transaction(&existingTxID) + require.NoError(t, err) + + // todo hook the block reference + //blk, err := tclient.Block(testTx.BlockRef) + //require.NoError(t, err) + + // locally execute the transaction + callReceipt, err := tclient.CallTransaction(testTx) + require.NoError(t, err) + + // evaluate call receipt response fields + validateTxCall(t, testTx, callReceipt, &genesis.DevAccounts()[0].Address, nil) +} + func invalidCallTx(t *testing.T) { var chainTag = repo.ChainTag() //var expiration = uint32(10) @@ -325,23 +338,14 @@ func invalidCallTx(t *testing.T) { errMsg string }{ { - testTx: transactions.ConvertCallTransaction(new(tx.Builder). + testTx: transactions.ConvertCoreTransaction(new(tx.Builder). Gas(gas). Build(), nil, sendAddr, nil), errMsg: "chain tag mismatch", }, - //{ - // testTx: transactions.ConvertCallTransaction(new(tx.Builder). - // ChainTag(chainTag). - // Expiration(0). - // Gas(gas). - // Build(), - // nil, sendAddr, nil), - // errMsg: "chain tag mismatch", - //}, { - testTx: transactions.ConvertCallTransaction(new(tx.Builder). + testTx: transactions.ConvertCoreTransaction(new(tx.Builder). ChainTag(chainTag). Gas(gas). Build(), @@ -349,7 +353,7 @@ func invalidCallTx(t *testing.T) { errMsg: "no origin address specified", }, { - testTx: transactions.ConvertCallTransaction(new(tx.Builder). + testTx: transactions.ConvertCoreTransaction(new(tx.Builder). ChainTag(chainTag). Gas(gas). Clause(tx.NewClause(nil).WithData(make([]byte, 64*1024+1))). @@ -359,13 +363,26 @@ func invalidCallTx(t *testing.T) { }, } { t.Run(tc.errMsg, func(t *testing.T) { - res := httpPostAndCheckResponseStatus(t, "/transactions/call", tc.testTx, 500) - assert.Equal(t, tc.errMsg, strings.TrimSpace(string(res))) + _, err := tclient.CallTransaction(tc.testTx, nil) + require.Error(t, err) + require.ErrorContains(t, err, tc.errMsg) }) } } -func validateTxCall(t *testing.T, callTx *tx.Transaction, callRcpt *transactions.CallReceipt, callAddr, delegator *thor.Address) { +func validateTxCall(t *testing.T, callTx *transactions.Transaction, callRcpt *transactions.CallReceipt, callAddr, delegator *thor.Address) { + assert.Equal(t, *callAddr, callRcpt.TxOrigin) + + if delegator != nil { + assert.Equal(t, delegator.String(), callRcpt.GasPayer.String()) + } else { + assert.Equal(t, callAddr.String(), callRcpt.GasPayer.String()) + } + + assert.Equal(t, len(callTx.Clauses), len(callRcpt.Outputs)) +} + +func validateCoreTxCall(t *testing.T, callTx *tx.Transaction, callRcpt *transactions.CallReceipt, callAddr, delegator *thor.Address) { assert.Equal(t, callTx.ID(), callRcpt.TxID) assert.Equal(t, *callAddr, callRcpt.TxOrigin) diff --git a/api/transactions/types.go b/api/transactions/types.go index ff9625353..a9f080f6a 100644 --- a/api/transactions/types.go +++ b/api/transactions/types.go @@ -84,9 +84,9 @@ type RawTransaction struct { Meta *TxMeta `json:"meta"` } -// ConvertCallTransaction convert a raw transaction into a json format transaction +// ConvertCoreTransaction converts a core type transaction into an api tx (json format transaction) // allows to specify the origin and delegator -func ConvertCallTransaction(tx *tx.Transaction, header *block.Header, origin *thor.Address, delegator *thor.Address) *Transaction { +func ConvertCoreTransaction(tx *tx.Transaction, header *block.Header, origin *thor.Address, delegator *thor.Address) *Transaction { cls := make(Clauses, len(tx.Clauses())) for i, c := range tx.Clauses() { cls[i] = convertClause(c) @@ -117,12 +117,13 @@ func ConvertCallTransaction(tx *tx.Transaction, header *block.Header, origin *th return t } -// ConvertTransaction convert a raw transaction into a json format transaction -func ConvertTransaction(tx *tx.Transaction, header *block.Header) *Transaction { +// convertSignedCoreTransaction converts a core type transaction into an api tx (json format transaction) +// retrieves the origin and delegator from signature +func convertSignedCoreTransaction(tx *tx.Transaction, header *block.Header) *Transaction { //tx origin origin, _ := tx.Origin() delegator, _ := tx.Delegator() - return ConvertCallTransaction(tx, header, &origin, delegator) + return ConvertCoreTransaction(tx, header, &origin, delegator) } type TxMeta struct { @@ -320,8 +321,9 @@ func convertErrorCallReceipt( return receipt, nil } -// convertToTxTransaction converts a transaction.Transaction into a tx.Transaction -func convertToTxTransaction(incomingTx *Transaction) (*tx.Transaction, error) { +// ConvertCallTransaction converts a transaction.Transaction into a tx.Transaction +// note: tx.Transaction will not be signed +func ConvertCallTransaction(incomingTx *Transaction) (*tx.Transaction, error) { //blockRef, err := thor.ParseBytes32(incomingTx.BlockRef) //if err != nil { // return nil, fmt.Errorf("unable to parse block ref: %w", err) diff --git a/thorclient/httpclient/client.go b/thorclient/httpclient/client.go index 8f88783f5..8b9a33f46 100644 --- a/thorclient/httpclient/client.go +++ b/thorclient/httpclient/client.go @@ -141,6 +141,26 @@ func (c *Client) GetTransaction(txID *thor.Bytes32, head string, isPending bool) return &tx, nil } +// CallTransaction locally executes a given transaction, along with options for head. +func (c *Client) CallTransaction(transaction *transactions.Transaction, head string) (*transactions.CallReceipt, error) { + url := c.url + "/transactions/call?" + if head != "" { + url += "head=" + head + } + + body, err := c.httpPOST(url, transaction) + if err != nil { + return nil, fmt.Errorf("unable to call transaction - %w", err) + } + + var callReceipt transactions.CallReceipt + if err = json.Unmarshal(body, &callReceipt); err != nil { + return nil, fmt.Errorf("unable to unmarshal transaction call receipt - %w", err) + } + + return &callReceipt, nil +} + // GetRawTransaction retrieves the raw transaction data by the transaction ID, along with options for head and pending status. func (c *Client) GetRawTransaction(txID *thor.Bytes32, head string, isPending bool) (*transactions.RawTransaction, error) { url := c.url + "/transactions/" + txID.String() + "?raw=true&" diff --git a/thorclient/httpclient/client_test.go b/thorclient/httpclient/client_test.go index 7e807ba55..4339ee00f 100644 --- a/thorclient/httpclient/client_test.go +++ b/thorclient/httpclient/client_test.go @@ -22,6 +22,7 @@ import ( "github.com/vechain/thor/v2/api/node" "github.com/vechain/thor/v2/api/transactions" "github.com/vechain/thor/v2/api/transfers" + "github.com/vechain/thor/v2/test/datagen" "github.com/vechain/thor/v2/thor" tccommon "github.com/vechain/thor/v2/thorclient/common" @@ -300,6 +301,25 @@ func TestClient_GetTransaction(t *testing.T) { assert.Equal(t, expectedTx, tx) } +func TestClient_CallTransaction(t *testing.T) { + calledTx := &transactions.Transaction{ID: datagen.RandomHash()} + expectedCallReceipt := &transactions.CallReceipt{TxID: calledTx.ID} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/transactions/call", r.URL.Path) + + txBytes, _ := json.Marshal(expectedCallReceipt) + w.Write(txBytes) + })) + defer ts.Close() + + client := New(ts.URL) + callReceipt, err := client.CallTransaction(calledTx, tccommon.BestRevision) + + assert.NoError(t, err) + assert.Equal(t, expectedCallReceipt, callReceipt) +} + func TestClient_GetRawTransaction(t *testing.T) { txID := thor.Bytes32{0x01} expectedTx := &transactions.RawTransaction{ diff --git a/thorclient/thorclient.go b/thorclient/thorclient.go index 8458a0ae4..81b3605ae 100644 --- a/thorclient/thorclient.go +++ b/thorclient/thorclient.go @@ -74,7 +74,9 @@ func applyOptions(opts []Option) *getOptions { pending: false, } for _, o := range opts { - o(options) + if o != nil { + o(options) + } } return options } @@ -137,9 +139,20 @@ func (c *Client) AccountStorage(addr *thor.Address, key *thor.Bytes32, opts ...O // Transaction retrieves a transaction by its ID. func (c *Client) Transaction(id *thor.Bytes32, opts ...Option) (*transactions.Transaction, error) { options := applyOptions(opts) + // TODO review the revision vs head difference + // plus fix the finalizedRevision use, throw an error ? + if options.revision == tccommon.BestRevision { + options.revision = "" + } return c.httpConn.GetTransaction(id, options.revision, options.pending) } +// CallTransaction locally executes a given transaction +func (c *Client) CallTransaction(transaction *transactions.Transaction, opts ...Option) (*transactions.CallReceipt, error) { + options := applyOptions(opts) + return c.httpConn.CallTransaction(transaction, options.revision) +} + // RawTransaction retrieves the raw transaction data by its ID. func (c *Client) RawTransaction(id *thor.Bytes32, opts ...Option) (*transactions.RawTransaction, error) { options := applyOptions(opts) From 9c85e652905f20c711ef64b0d1c7b2d6d7e3c3dd Mon Sep 17 00:00:00 2001 From: Makis Christou Date: Tue, 3 Dec 2024 09:33:33 +0200 Subject: [PATCH 04/13] Add txpool checks --- api/transactions/transactions.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/api/transactions/transactions.go b/api/transactions/transactions.go index f75c0f920..42a1328bb 100644 --- a/api/transactions/transactions.go +++ b/api/transactions/transactions.go @@ -244,8 +244,17 @@ func (t *Transactions) txCall( return nil, fmt.Errorf("unable to convert transaction: %w", err) } - // validation from the mempool - // TODO add more validations that are mandatory + // Txpool Checks + origin, _ := txCallData.Origin() + if thor.IsOriginBlocked(origin) { + // tx origin blocked + return nil, fmt.Errorf("origin blocked") + } + + if err := txCallData.TestFeatures(header.TxsFeatures()); err != nil { + return nil, fmt.Errorf("invalid features") + } + switch { case txCallMsg.ChainTag != t.repo.ChainTag(): return nil, fmt.Errorf("chain tag mismatch") From c790152a6b08c1468859dc3d0cac4114faa62de9 Mon Sep 17 00:00:00 2001 From: Makis Christou Date: Tue, 3 Dec 2024 15:39:50 +0200 Subject: [PATCH 05/13] Add features and blockref to converted tx --- api/transactions/transactions.go | 2 +- api/transactions/types.go | 15 ++++++++------- tx/block_ref.go | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/api/transactions/transactions.go b/api/transactions/transactions.go index 42a1328bb..b0685ec9a 100644 --- a/api/transactions/transactions.go +++ b/api/transactions/transactions.go @@ -239,7 +239,7 @@ func (t *Transactions) txCall( } // todo handle the txCallMsg.Delegator - txCallData, err := ConvertCallTransaction(txCallMsg) + txCallData, err := ConvertCallTransaction(txCallMsg, header) if err != nil { return nil, fmt.Errorf("unable to convert transaction: %w", err) } diff --git a/api/transactions/types.go b/api/transactions/types.go index a9f080f6a..60e6dae6e 100644 --- a/api/transactions/types.go +++ b/api/transactions/types.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/rlp" + "github.com/pkg/errors" "github.com/vechain/thor/v2/block" "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/tx" @@ -323,17 +324,17 @@ func convertErrorCallReceipt( // ConvertCallTransaction converts a transaction.Transaction into a tx.Transaction // note: tx.Transaction will not be signed -func ConvertCallTransaction(incomingTx *Transaction) (*tx.Transaction, error) { - //blockRef, err := thor.ParseBytes32(incomingTx.BlockRef) - //if err != nil { - // return nil, fmt.Errorf("unable to parse block ref: %w", err) - //} +func ConvertCallTransaction(incomingTx *Transaction, header *block.Header) (*tx.Transaction, error) { + blockRef, err := tx.NewBlockRefFromHex(incomingTx.BlockRef) + if err != nil { + return nil, errors.WithMessage(err, "blockRef") + } convertedTxBuilder := new(tx.Builder). ChainTag(incomingTx.ChainTag). - //Features(incomingTx). // TODO hook in the future + Features(header.TxsFeatures()). Nonce(uint64(incomingTx.Nonce)). - //BlockRef(tx.NewBlockRefFromID(blockRef)). // TODO hook in the future + BlockRef(blockRef). Expiration(incomingTx.Expiration). GasPriceCoef(incomingTx.GasPriceCoef). Gas(incomingTx.Gas). diff --git a/tx/block_ref.go b/tx/block_ref.go index 8a0c11e9b..361114ec6 100644 --- a/tx/block_ref.go +++ b/tx/block_ref.go @@ -7,7 +7,9 @@ package tx import ( "encoding/binary" + "fmt" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/vechain/thor/v2/thor" ) @@ -30,3 +32,23 @@ func NewBlockRefFromID(blockID thor.Bytes32) (br BlockRef) { copy(br[:], blockID[:]) return } + +// NewBlockRefFromHex creates a BlockRef from a hex string. +func NewBlockRefFromHex(hexStr string) (BlockRef, error) { + var br BlockRef + + // Decode hex string + bytes, err := hexutil.Decode(hexStr) + if err != nil { + return br, fmt.Errorf("invalid hex: %v", err) + } + + // Check length + if len(bytes) != 8 { + return br, fmt.Errorf("invalid length: expected 8 bytes, got %d", len(bytes)) + } + + // Copy bytes to BlockRef + copy(br[:], bytes) + return br, nil +} From f435ab8eaa9183de2375ef93cd056e88aa412bd7 Mon Sep 17 00:00:00 2001 From: Makis Christou Date: Wed, 4 Dec 2024 09:35:33 +0200 Subject: [PATCH 06/13] Test for best and older blocks --- api/transactions/transactions_test.go | 46 +++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/api/transactions/transactions_test.go b/api/transactions/transactions_test.go index fd5ead4b0..e249e6971 100644 --- a/api/transactions/transactions_test.go +++ b/api/transactions/transactions_test.go @@ -10,6 +10,7 @@ import ( "fmt" "math/big" "net/http/httptest" + "strconv" "strings" "testing" "time" @@ -81,9 +82,10 @@ func TestTransaction(t *testing.T) { // Call transaction for name, tt := range map[string]func(*testing.T){ - "callTx": callTx, - "invalidCallTx": invalidCallTx, - "callExistingTx": callExistingTx, + "callTx": callTx, + "invalidCallTx": invalidCallTx, + "callExistingTx": callExistingTx, + "callExistingTxOldBlock": callExistingTxOldBlock, } { t.Run(name, tt) } @@ -309,15 +311,45 @@ func callTx(t *testing.T) { } } -func callExistingTx(t *testing.T) { +func calcNewBlockRef(currentBlockRef string) (*string, error) { + num, err := strconv.ParseInt(strings.TrimPrefix(currentBlockRef, "0x"), 16, 64) + if err != nil { + return nil, err + } + + // Subtract 10 blocks if we can + if num >= 10 { + num = num - 10 + } + + // Format with padding to 16 zeros (8 bytes) + newBlockRef := fmt.Sprintf("0x%016x", num) + return &newBlockRef, nil +} + +func callExistingTxOldBlock(t *testing.T) { // fetch an existing transaction existingTxID := transaction.ID() testTx, err := tclient.Transaction(&existingTxID) require.NoError(t, err) - // todo hook the block reference - //blk, err := tclient.Block(testTx.BlockRef) - //require.NoError(t, err) + newBlockRef, err := calcNewBlockRef(testTx.BlockRef) + require.NoError(t, err) + testTx.BlockRef = *newBlockRef + + // locally execute the transaction + callReceipt, err := tclient.CallTransaction(testTx) + require.NoError(t, err) + + // evaluate call receipt response fields + validateTxCall(t, testTx, callReceipt, &genesis.DevAccounts()[0].Address, nil) +} + +func callExistingTx(t *testing.T) { + // fetch an existing transaction + existingTxID := transaction.ID() + testTx, err := tclient.Transaction(&existingTxID) + require.NoError(t, err) // locally execute the transaction callReceipt, err := tclient.CallTransaction(testTx) From da734f175df298cca7aa9f696110f913367337d2 Mon Sep 17 00:00:00 2001 From: Makis Christou Date: Wed, 4 Dec 2024 09:50:52 +0200 Subject: [PATCH 07/13] Hook delegator --- api/transactions/transactions.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/transactions/transactions.go b/api/transactions/transactions.go index b0685ec9a..4b3b15b3a 100644 --- a/api/transactions/transactions.go +++ b/api/transactions/transactions.go @@ -238,7 +238,6 @@ func (t *Transactions) txCall( return nil, fmt.Errorf("no origin address specified") } - // todo handle the txCallMsg.Delegator txCallData, err := ConvertCallTransaction(txCallMsg, header) if err != nil { return nil, fmt.Errorf("unable to convert transaction: %w", err) @@ -277,7 +276,7 @@ func (t *Transactions) txCall( }, t.forkConfig) - receipt, err := rt.CallTransaction(txCallData, &callAddr, nil) // TODO hook delegator + receipt, err := rt.CallTransaction(txCallData, &callAddr, txCallMsg.Delegator) if err != nil { // TODO add some metric here return convertErrorCallReceipt(err, txCallMsg, &callAddr) From 0aa4d387584fe92696b45599fa8373911e8621f4 Mon Sep 17 00:00:00 2001 From: Makis Christou Date: Thu, 5 Dec 2024 15:22:30 +0200 Subject: [PATCH 08/13] Count call evm errors --- api/metrics.go | 40 +++++++++++++++++++++++++++++++- api/transactions/transactions.go | 1 - 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/api/metrics.go b/api/metrics.go index 9fd5c3d94..e8899f1f7 100644 --- a/api/metrics.go +++ b/api/metrics.go @@ -7,6 +7,7 @@ package api import ( "bufio" + "encoding/json" "errors" "net" "net/http" @@ -22,6 +23,7 @@ var ( metricHTTPReqCounter = metrics.LazyLoadCounterVec("api_request_count", []string{"name", "code", "method"}) metricHTTPReqDuration = metrics.LazyLoadHistogramVec("api_duration_ms", []string{"name", "code", "method"}, metrics.BucketHTTPReqs) metricActiveWebsocketCount = metrics.LazyLoadGaugeVec("api_active_websocket_count", []string{"subject"}) + metricTxCallVMErrors = metrics.LazyLoadCounter("api_tx_call_vm_errors") ) // metricsResponseWriter is a wrapper around http.ResponseWriter that captures the status code. @@ -34,11 +36,35 @@ func newMetricsResponseWriter(w http.ResponseWriter) *metricsResponseWriter { return &metricsResponseWriter{w, http.StatusOK} } +type callTxResponseWriter struct { + http.ResponseWriter + statusCode int + vmError bool +} + +func newCallTxResponseWriter(w http.ResponseWriter) *callTxResponseWriter { + return &callTxResponseWriter{w, http.StatusOK, false} +} + func (m *metricsResponseWriter) WriteHeader(code int) { m.statusCode = code m.ResponseWriter.WriteHeader(code) } +func (c *callTxResponseWriter) Write(b []byte) (int, error) { + var resp struct { + VmError string `json:"vmError"` + } + + if err := json.Unmarshal(b, &resp); err == nil { + if resp.VmError != "" { + c.vmError = true + } + } + + return c.ResponseWriter.Write(b) +} + // Hijack complies the writer with WS subscriptions interface // Hijack lets the caller take over the connection. // After a call to Hijack the HTTP server library @@ -65,10 +91,22 @@ func metricsMiddleware(next http.Handler) http.Handler { subscription = "" ) - // all named route will be recorded if rt != nil && rt.GetName() != "" { enabled = true name = rt.GetName() + + if name == "transactions_call_tx" { + ctxWriter := newCallTxResponseWriter(w) + next.ServeHTTP(ctxWriter, r) + + // Record VM error if present + if ctxWriter.vmError { + metricTxCallVMErrors().Add(1) + } + return + } + + // Handle subscriptions if strings.HasPrefix(name, "subscriptions") { // example path: /subscriptions/txpool -> subject = txpool paths := strings.Split(r.URL.Path, "/") diff --git a/api/transactions/transactions.go b/api/transactions/transactions.go index 4b3b15b3a..e3e23aa46 100644 --- a/api/transactions/transactions.go +++ b/api/transactions/transactions.go @@ -278,7 +278,6 @@ func (t *Transactions) txCall( receipt, err := rt.CallTransaction(txCallData, &callAddr, txCallMsg.Delegator) if err != nil { - // TODO add some metric here return convertErrorCallReceipt(err, txCallMsg, &callAddr) } From 099f6b94e912f3e35504212ca69394c01efa438b Mon Sep 17 00:00:00 2001 From: Makis Christou Date: Thu, 5 Dec 2024 15:29:03 +0200 Subject: [PATCH 09/13] Count different types of vmErrors --- api/metrics.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/api/metrics.go b/api/metrics.go index e8899f1f7..d6e614eb2 100644 --- a/api/metrics.go +++ b/api/metrics.go @@ -23,7 +23,7 @@ var ( metricHTTPReqCounter = metrics.LazyLoadCounterVec("api_request_count", []string{"name", "code", "method"}) metricHTTPReqDuration = metrics.LazyLoadHistogramVec("api_duration_ms", []string{"name", "code", "method"}, metrics.BucketHTTPReqs) metricActiveWebsocketCount = metrics.LazyLoadGaugeVec("api_active_websocket_count", []string{"subject"}) - metricTxCallVMErrors = metrics.LazyLoadCounter("api_tx_call_vm_errors") + metricTxCallVMErrors = metrics.LazyLoadCounterVec("api_tx_call_vm_errors", []string{"error"}) ) // metricsResponseWriter is a wrapper around http.ResponseWriter that captures the status code. @@ -39,11 +39,11 @@ func newMetricsResponseWriter(w http.ResponseWriter) *metricsResponseWriter { type callTxResponseWriter struct { http.ResponseWriter statusCode int - vmError bool + vmError string } func newCallTxResponseWriter(w http.ResponseWriter) *callTxResponseWriter { - return &callTxResponseWriter{w, http.StatusOK, false} + return &callTxResponseWriter{w, http.StatusOK, ""} } func (m *metricsResponseWriter) WriteHeader(code int) { @@ -58,7 +58,7 @@ func (c *callTxResponseWriter) Write(b []byte) (int, error) { if err := json.Unmarshal(b, &resp); err == nil { if resp.VmError != "" { - c.vmError = true + c.vmError = resp.VmError } } @@ -100,8 +100,10 @@ func metricsMiddleware(next http.Handler) http.Handler { next.ServeHTTP(ctxWriter, r) // Record VM error if present - if ctxWriter.vmError { - metricTxCallVMErrors().Add(1) + if ctxWriter.vmError != "" { + metricTxCallVMErrors().AddWithLabel(1, map[string]string{ + "error": ctxWriter.vmError, + }) } return } From eb47f49543c79ccebb92464ecedea38bf527dc36 Mon Sep 17 00:00:00 2001 From: Makis Christou Date: Tue, 17 Dec 2024 15:53:46 +0200 Subject: [PATCH 10/13] Fix failing tests --- .../transactions_benchmark_test.go | 4 +- api/transactions/transactions_test.go | 53 ++++++++++++++----- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/api/transactions/transactions_benchmark_test.go b/api/transactions/transactions_benchmark_test.go index f6f1fccd6..4429fa270 100644 --- a/api/transactions/transactions_benchmark_test.go +++ b/api/transactions/transactions_benchmark_test.go @@ -161,7 +161,7 @@ func BenchmarkFetchTx_RandomSigners_OneClausePerTx(b *testing.B) { func benchmarkGetTransaction(b *testing.B, thorChain *testchain.Chain, randTxs tx.Transactions) { mempool := txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{Limit: 10, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute}) - transactionAPI := New(thorChain.Repo(), mempool) + transactionAPI := New(thorChain.Repo(), thorChain.Stater(), mempool, thorChain.Engine(), thor.GetForkConfig(thorChain.GenesisBlock().Header().ID())) head := thorChain.Repo().BestBlockSummary().Header.ID() var err error @@ -181,7 +181,7 @@ func benchmarkGetTransaction(b *testing.B, thorChain *testchain.Chain, randTxs t func benchmarkGetReceipt(b *testing.B, thorChain *testchain.Chain, randTxs tx.Transactions) { mempool := txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{Limit: 10, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute}) - transactionAPI := New(thorChain.Repo(), mempool) + transactionAPI := New(thorChain.Repo(), thorChain.Stater(), mempool, thorChain.Engine(), thor.GetForkConfig(thorChain.GenesisBlock().Header().ID())) head := thorChain.Repo().BestBlockSummary().Header.ID() var err error diff --git a/api/transactions/transactions_test.go b/api/transactions/transactions_test.go index c90539807..e249e6971 100644 --- a/api/transactions/transactions_test.go +++ b/api/transactions/transactions_test.go @@ -24,7 +24,9 @@ import ( "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/cmd/thor/solo" "github.com/vechain/thor/v2/genesis" - "github.com/vechain/thor/v2/test/testchain" + "github.com/vechain/thor/v2/muxdb" + "github.com/vechain/thor/v2/packer" + "github.com/vechain/thor/v2/state" "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/thorclient" "github.com/vechain/thor/v2/tx" @@ -32,11 +34,11 @@ import ( ) var ( + repo *chain.Repository ts *httptest.Server transaction *tx.Transaction mempoolTx *tx.Transaction tclient *thorclient.Client - chainTag byte ) func TestTransaction(t *testing.T) { @@ -120,6 +122,7 @@ func getTxReceipt(t *testing.T) { func sendTx(t *testing.T) { var blockRef = tx.NewBlockRef(0) + var chainTag = repo.ChainTag() var expiration = uint32(10) var gas = uint64(21000) @@ -433,15 +436,19 @@ func httpPostAndCheckResponseStatus(t *testing.T, url string, obj interface{}, r } func initTransactionServer(t *testing.T) { - thorChain, err := testchain.NewIntegrationTestChain() - require.NoError(t, err) - - chainTag = thorChain.Repo().ChainTag() + db := muxdb.NewMem() + stater := state.NewStater(db) + gene := genesis.NewDevnet() + b, _, _, err := gene.Build(stater) + if err != nil { + t.Fatal(err) + } + repo, _ = chain.NewRepository(db, b) addr := thor.BytesToAddress([]byte("to")) cla := tx.NewClause(&addr).WithValue(big.NewInt(10000)) transaction = new(tx.Builder). - ChainTag(chainTag). + ChainTag(repo.ChainTag()). GasPriceCoef(1). Expiration(10). Gas(21000). @@ -451,19 +458,41 @@ func initTransactionServer(t *testing.T) { Build() transaction = tx.MustSign(transaction, genesis.DevAccounts()[0].PrivateKey) - require.NoError(t, thorChain.MintTransactions(genesis.DevAccounts()[0], transaction)) - - mempool := txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{Limit: 10000, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute}) - mempoolTx = new(tx.Builder). - ChainTag(chainTag). + ChainTag(repo.ChainTag()). Expiration(10). Gas(21000). Nonce(1). Build() mempoolTx = tx.MustSign(mempoolTx, genesis.DevAccounts()[0].PrivateKey) + packer := packer.New(repo, stater, genesis.DevAccounts()[0].Address, &genesis.DevAccounts()[0].Address, thor.NoFork) + sum, _ := repo.GetBlockSummary(b.Header().ID()) + flow, err := packer.Schedule(sum, uint64(time.Now().Unix())) + if err != nil { + t.Fatal(err) + } + err = flow.Adopt(transaction) + if err != nil { + t.Fatal(err) + } + b, stage, receipts, err := flow.Pack(genesis.DevAccounts()[0].PrivateKey, 0, false) + if err != nil { + t.Fatal(err) + } + if _, err := stage.Commit(); err != nil { + t.Fatal(err) + } + if err := repo.AddBlock(b, receipts, 0); err != nil { + t.Fatal(err) + } + if err := repo.SetBestBlockID(b.Header().ID()); err != nil { + t.Fatal(err) + } + router := mux.NewRouter() + // Add a tx to the mempool to have both pending and non-pending transactions + mempool := txpool.New(repo, stater, txpool.Options{Limit: 10000, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute}) e := mempool.Add(mempoolTx) if e != nil { t.Fatal(e) From 7ceaf293548fb3533cfa824dd023720d2bcc15a2 Mon Sep 17 00:00:00 2001 From: Makis Christou Date: Wed, 18 Dec 2024 11:26:42 +0200 Subject: [PATCH 11/13] Fix failed test && Remove unrelated comments --- thorclient/api_test.go | 2 +- thorclient/thorclient.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/thorclient/api_test.go b/thorclient/api_test.go index df72b1296..b8845863e 100644 --- a/thorclient/api_test.go +++ b/thorclient/api_test.go @@ -60,7 +60,7 @@ func initAPIServer(t *testing.T) (*testchain.Chain, *httptest.Server) { Mount(router, "/accounts") mempool := txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{Limit: 10000, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute}) - transactions.New(thorChain.Repo(), mempool).Mount(router, "/transactions") + transactions.New(thorChain.Repo(), thorChain.Stater(), mempool, thorChain.Engine(), thor.GetForkConfig(thorChain.GenesisBlock().Header().ID())).Mount(router, "/transactions") blocks.New(thorChain.Repo(), thorChain.Engine()).Mount(router, "/blocks") diff --git a/thorclient/thorclient.go b/thorclient/thorclient.go index d94177810..c26660e32 100644 --- a/thorclient/thorclient.go +++ b/thorclient/thorclient.go @@ -159,8 +159,6 @@ func (c *Client) AccountStorage(addr *thor.Address, key *thor.Bytes32, opts ...O // Transaction retrieves a transaction by its ID. func (c *Client) Transaction(id *thor.Bytes32, opts ...Option) (*transactions.Transaction, error) { options := applyOptions(opts) - // TODO review the revision vs head difference - // plus fix the finalizedRevision use, throw an error ? if options.revision == tccommon.BestRevision { options.revision = "" } From 5c167fbfc8af58b79cf1edb5eb15812c9231d213 Mon Sep 17 00:00:00 2001 From: Makis Christou Date: Wed, 18 Dec 2024 11:37:21 +0200 Subject: [PATCH 12/13] Fix golangci-lint --- api/metrics.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/api/metrics.go b/api/metrics.go index 3805c7f16..bad5bd76c 100644 --- a/api/metrics.go +++ b/api/metrics.go @@ -26,7 +26,6 @@ var ( } metricHTTPReqCounter = metrics.LazyLoadCounterVec("api_request_count", []string{"name", "code", "method"}) metricHTTPReqDuration = metrics.LazyLoadHistogramVec("api_duration_ms", []string{"name", "code", "method"}, metrics.BucketHTTPReqs) - metricActiveWebsocketCount = metrics.LazyLoadGaugeVec("api_active_websocket_count", []string{"subject"}) metricTxCallVMErrors = metrics.LazyLoadCounterVec("api_tx_call_vm_errors", []string{"error"}) metricWebsocketDuration = metrics.LazyLoadHistogramVec("api_websocket_duration", []string{"name", "code"}, websocketDurations) metricActiveWebsocketGauge = metrics.LazyLoadGaugeVec("api_active_websocket_gauge", []string{"name"}) @@ -46,7 +45,7 @@ func newMetricsResponseWriter(w http.ResponseWriter) *metricsResponseWriter { type callTxResponseWriter struct { http.ResponseWriter statusCode int - vmError string + VMError string } func newCallTxResponseWriter(w http.ResponseWriter) *callTxResponseWriter { @@ -60,12 +59,12 @@ func (m *metricsResponseWriter) WriteHeader(code int) { func (c *callTxResponseWriter) Write(b []byte) (int, error) { var resp struct { - VmError string `json:"vmError"` + VMError string `json:"VMError"` } if err := json.Unmarshal(b, &resp); err == nil { - if resp.VmError != "" { - c.vmError = resp.VmError + if resp.VMError != "" { + c.VMError = resp.VMError } } @@ -107,9 +106,9 @@ func metricsMiddleware(next http.Handler) http.Handler { next.ServeHTTP(ctxWriter, r) // Record VM error if present - if ctxWriter.vmError != "" { + if ctxWriter.VMError != "" { metricTxCallVMErrors().AddWithLabel(1, map[string]string{ - "error": ctxWriter.vmError, + "error": ctxWriter.VMError, }) } return From 1971e107f6bf2ffda19fdbdfaa5f1e4183f5b4dc Mon Sep 17 00:00:00 2001 From: yeahnotsewerside Date: Thu, 23 Jan 2025 13:15:34 +0200 Subject: [PATCH 13/13] remove redundant check --- api/transactions/transactions.go | 4 ---- api/transactions/transactions_test.go | 11 ++++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/api/transactions/transactions.go b/api/transactions/transactions.go index c1e4c7565..0a017151d 100644 --- a/api/transactions/transactions.go +++ b/api/transactions/transactions.go @@ -250,10 +250,6 @@ func (t *Transactions) txCall( return nil, fmt.Errorf("origin blocked") } - if err := txCallData.TestFeatures(header.TxsFeatures()); err != nil { - return nil, fmt.Errorf("invalid features") - } - switch { case txCallMsg.ChainTag != t.repo.ChainTag(): return nil, fmt.Errorf("chain tag mismatch") diff --git a/api/transactions/transactions_test.go b/api/transactions/transactions_test.go index 4694ff017..9bd0db978 100644 --- a/api/transactions/transactions_test.go +++ b/api/transactions/transactions_test.go @@ -8,6 +8,7 @@ package transactions_test import ( "encoding/json" "fmt" + "math" "math/big" "net/http/httptest" "strconv" @@ -466,7 +467,15 @@ func initTransactionServer(t *testing.T) { Build() mempoolTx = tx.MustSign(mempoolTx, genesis.DevAccounts()[0].PrivateKey) - packer := packer.New(repo, stater, genesis.DevAccounts()[0].Address, &genesis.DevAccounts()[0].Address, thor.NoFork) + packer := packer.New(repo, stater, genesis.DevAccounts()[0].Address, &genesis.DevAccounts()[0].Address, thor.ForkConfig{ + VIP191: 0, + ETH_CONST: math.MaxUint32, + BLOCKLIST: math.MaxUint32, + ETH_IST: math.MaxUint32, + VIP214: math.MaxUint32, + FINALITY: math.MaxUint32, + }, + ) sum, _ := repo.GetBlockSummary(b.Header().ID()) flow, err := packer.Schedule(sum, uint64(time.Now().Unix())) if err != nil {