Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding transaction_call endpoint #832

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func New(
}
blocks.New(repo, bft).
Mount(router, "/blocks")
transactions.New(repo, txPool).
transactions.New(repo, stater, txPool, bft, forkConfig).
Mount(router, "/transactions")
debug.New(repo, stater, forkConfig, callGasLimit, allowCustomTracer, bft, allowedTracers).
Mount(router, "/debug")
Expand Down
105 changes: 98 additions & 7 deletions api/transactions/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,41 @@
package transactions

import (
"fmt"
"net/http"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rlp"
"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
forkConfig thor.ForkConfig
}

func New(repo *chain.Repository, pool *txpool.TxPool) *Transactions {
func New(repo *chain.Repository, stater *state.Stater, pool *txpool.TxPool, bft bft.Finalizer, forkConfig thor.ForkConfig) *Transactions {
return &Transactions{
repo,
pool,
repo: repo,
stater: stater,
pool: pool,
bft: bft,
forkConfig: forkConfig,
}
}

Expand Down Expand Up @@ -76,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 ConvertTransaction(pending, nil), nil
}
}
return nil, nil
Expand All @@ -88,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 ConvertTransaction(tx, summary.Header), nil
}

// GetTransactionReceiptByID get tx's receipt
Expand All @@ -114,6 +128,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 {
Expand Down Expand Up @@ -214,6 +229,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(),
},
t.forkConfig)

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

Expand All @@ -229,4 +316,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))
}
119 changes: 118 additions & 1 deletion api/transactions/transactions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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), thor.NoFork).Mount(router, "/transactions")

ts = httptest.NewServer(router)
}
Expand Down
Loading
Loading