import (
"context"
"encoding/json"
- "fmt"
+ "math"
"strings"
"time"
log "github.com/sirupsen/logrus"
- "github.com/bytom/blockchain/pseudohsm"
+ "github.com/bytom/account"
"github.com/bytom/blockchain/txbuilder"
"github.com/bytom/consensus"
+ "github.com/bytom/consensus/segwit"
"github.com/bytom/errors"
"github.com/bytom/math/checked"
"github.com/bytom/net/http/reqid"
"github.com/bytom/protocol/bc/types"
)
-var defaultTxTTL = 5 * time.Minute
+var (
+ defaultTxTTL = 5 * time.Minute
+ defaultBaseRate = float64(100000)
+ flexibleGas = int64(1800)
+)
func (a *API) actionDecoder(action string) (func([]byte) (txbuilder.Action, error), bool) {
- var decoder func([]byte) (txbuilder.Action, error)
- switch action {
- case "control_address":
- decoder = txbuilder.DecodeControlAddressAction
- case "control_program":
- decoder = txbuilder.DecodeControlProgramAction
- case "control_receiver":
- decoder = txbuilder.DecodeControlReceiverAction
- case "issue":
- decoder = a.wallet.AssetReg.DecodeIssueAction
- case "retire":
- decoder = txbuilder.DecodeRetireAction
- case "spend_account":
- decoder = a.wallet.AccountMgr.DecodeSpendAction
- case "spend_account_unspent_output":
- decoder = a.wallet.AccountMgr.DecodeSpendUTXOAction
- default:
- return nil, false
- }
- return decoder, true
-}
-
-func mergeActions(req *BuildRequest) []map[string]interface{} {
- var actions []map[string]interface{}
- actionMap := make(map[string]map[string]interface{})
-
- for _, m := range req.Actions {
- if actionType := m["type"].(string); actionType != "spend_account" {
- actions = append(actions, m)
- continue
- }
-
- actionKey := m["asset_id"].(string) + m["account_id"].(string)
- amountNumber := m["amount"].(json.Number)
- amount, _ := amountNumber.Int64()
-
- if tmpM, ok := actionMap[actionKey]; ok {
- tmpNumber, _ := tmpM["amount"].(json.Number)
- tmpAmount, _ := tmpNumber.Int64()
- tmpM["amount"] = json.Number(fmt.Sprintf("%v", tmpAmount+amount))
- } else {
- actionMap[actionKey] = m
- actions = append(actions, m)
- }
+ decoders := map[string]func([]byte) (txbuilder.Action, error){
+ "control_address": txbuilder.DecodeControlAddressAction,
+ "control_program": txbuilder.DecodeControlProgramAction,
+ "issue": a.wallet.AssetReg.DecodeIssueAction,
+ "retire": txbuilder.DecodeRetireAction,
+ "spend_account": a.wallet.AccountMgr.DecodeSpendAction,
+ "spend_account_unspent_output": a.wallet.AccountMgr.DecodeSpendUTXOAction,
}
-
- return actions
+ decoder, ok := decoders[action]
+ return decoder, ok
}
-func onlyHaveSpendActions(req *BuildRequest) bool {
+func onlyHaveInputActions(req *BuildRequest) (bool, error) {
count := 0
- for _, m := range req.Actions {
- if actionType := m["type"].(string); strings.HasPrefix(actionType, "spend") {
+ for i, act := range req.Actions {
+ actionType, ok := act["type"].(string)
+ if !ok {
+ return false, errors.WithDetailf(ErrBadActionType, "no action type provided on action %d", i)
+ }
+
+ if strings.HasPrefix(actionType, "spend") || actionType == "issue" {
count++
}
}
- return count == len(req.Actions)
+ return count == len(req.Actions), nil
}
func (a *API) buildSingle(ctx context.Context, req *BuildRequest) (*txbuilder.Template, error) {
- err := a.filterAliases(ctx, req)
+ if err := a.checkRequestValidity(ctx, req); err != nil {
+ return nil, err
+ }
+ actions, err := a.mergeSpendActions(req)
+ if err != nil {
+ return nil, err
+ }
+
+ maxTime := time.Now().Add(req.TTL.Duration)
+ tpl, err := txbuilder.Build(ctx, req.Tx, actions, maxTime, req.TimeRange)
+ if errors.Root(err) == txbuilder.ErrAction {
+ // append each of the inner errors contained in the data.
+ var Errs string
+ var rootErr error
+ for i, innerErr := range errors.Data(err)["actions"].([]error) {
+ if i == 0 {
+ rootErr = errors.Root(innerErr)
+ }
+ Errs = Errs + innerErr.Error()
+ }
+ err = errors.WithDetail(rootErr, Errs)
+ }
if err != nil {
return nil, err
}
- if onlyHaveSpendActions(req) {
- return nil, errors.New("transaction only contain spend actions, didn't have output actions")
+ // ensure null is never returned for signing instructions
+ if tpl.SigningInstructions == nil {
+ tpl.SigningInstructions = []*txbuilder.SigningInstruction{}
}
+ return tpl, nil
+}
- reqActions := mergeActions(req)
- actions := make([]txbuilder.Action, 0, len(reqActions))
- for i, act := range reqActions {
+// POST /build-transaction
+func (a *API) build(ctx context.Context, buildReqs *BuildRequest) Response {
+ subctx := reqid.NewSubContext(ctx, reqid.New())
+ tmpl, err := a.buildSingle(subctx, buildReqs)
+ if err != nil {
+ return NewErrorResponse(err)
+ }
+
+ return NewSuccessResponse(tmpl)
+}
+func (a *API) checkRequestValidity(ctx context.Context, req *BuildRequest) error {
+ if err := a.completeMissingIDs(ctx, req); err != nil {
+ return err
+ }
+
+ if req.TTL.Duration == 0 {
+ req.TTL.Duration = defaultTxTTL
+ }
+
+ if ok, err := onlyHaveInputActions(req); err != nil {
+ return err
+ } else if ok {
+ return errors.WithDetail(ErrBadActionConstruction, "transaction contains only input actions and no output actions")
+ }
+ return nil
+}
+
+func (a *API) mergeSpendActions(req *BuildRequest) ([]txbuilder.Action, error) {
+ actions := make([]txbuilder.Action, 0, len(req.Actions))
+ for i, act := range req.Actions {
typ, ok := act["type"].(string)
if !ok {
- return nil, errors.WithDetailf(errBadActionType, "no action type provided on action %d", i)
+ return nil, errors.WithDetailf(ErrBadActionType, "no action type provided on action %d", i)
}
decoder, ok := a.actionDecoder(typ)
if !ok {
- return nil, errors.WithDetailf(errBadActionType, "unknown action type %q on action %d", typ, i)
+ return nil, errors.WithDetailf(ErrBadActionType, "unknown action type %q on action %d", typ, i)
}
// Remarshal to JSON, the action may have been modified when we
}
action, err := decoder(b)
if err != nil {
- return nil, errors.WithDetailf(errBadAction, "%s on action %d", err.Error(), i)
+ return nil, errors.WithDetailf(ErrBadAction, "%s on action %d", err.Error(), i)
}
actions = append(actions, action)
}
+ actions = account.MergeSpendAction(actions)
+ return actions, nil
+}
- ttl := req.TTL.Duration
- if ttl == 0 {
- ttl = defaultTxTTL
+func (a *API) buildTxs(ctx context.Context, req *BuildRequest) ([]*txbuilder.Template, error) {
+ if err := a.checkRequestValidity(ctx, req); err != nil {
+ return nil, err
+ }
+ actions, err := a.mergeSpendActions(req)
+ if err != nil {
+ return nil, err
}
- maxTime := time.Now().Add(ttl)
- tpl, err := txbuilder.Build(ctx, req.Tx, actions, maxTime, req.TimeRange)
- if errors.Root(err) == txbuilder.ErrAction {
- // append each of the inner errors contained in the data.
- var Errs string
- for _, innerErr := range errors.Data(err)["actions"].([]error) {
- Errs = Errs + "<" + innerErr.Error() + ">"
+ builder := txbuilder.NewBuilder(time.Now().Add(req.TTL.Duration))
+ tpls := []*txbuilder.Template{}
+ for _, action := range actions {
+ if action.ActionType() == "spend_account" {
+ tpls, err = account.SpendAccountChain(ctx, builder, action)
+ } else {
+ err = action.Build(ctx, builder)
+ }
+
+ if err != nil {
+ builder.Rollback()
+ return nil, err
}
- err = errors.New(err.Error() + "-" + Errs)
}
+
+ tpl, _, err := builder.Build()
if err != nil {
+ builder.Rollback()
return nil, err
}
- // ensure null is never returned for signing instructions
- if tpl.SigningInstructions == nil {
- tpl.SigningInstructions = []*txbuilder.SigningInstruction{}
- }
- return tpl, nil
+ tpls = append(tpls, tpl)
+ return tpls, nil
}
-// POST /build-transaction
-func (a *API) build(ctx context.Context, buildReqs *BuildRequest) Response {
+// POST /build-chain-transactions
+func (a *API) buildChainTxs(ctx context.Context, buildReqs *BuildRequest) Response {
subctx := reqid.NewSubContext(ctx, reqid.New())
-
- tmpl, err := a.buildSingle(subctx, buildReqs)
+ tmpls, err := a.buildTxs(subctx, buildReqs)
if err != nil {
return NewErrorResponse(err)
}
-
- return NewSuccessResponse(tmpl)
-}
-
-func (a *API) submitSingle(ctx context.Context, tpl *txbuilder.Template) (map[string]string, error) {
- if tpl.Transaction == nil {
- return nil, errors.Wrap(txbuilder.ErrMissingRawTx)
- }
-
- if err := txbuilder.FinalizeTx(ctx, a.chain, tpl.Transaction); err != nil {
- return nil, errors.Wrapf(err, "tx %s", tpl.Transaction.ID.String())
- }
-
- return map[string]string{"tx_id": tpl.Transaction.ID.String()}, nil
+ return NewSuccessResponse(tmpls)
}
type submitTxResp struct {
return NewErrorResponse(err)
}
- log.WithField("tx_id", ins.Tx.ID).Info("submit single tx")
+ log.WithField("tx_id", ins.Tx.ID.String()).Info("submit single tx")
return NewSuccessResponse(&submitTxResp{TxID: &ins.Tx.ID})
}
-// POST /sign-submit-transaction
-func (a *API) signSubmit(ctx context.Context, x struct {
- Password string `json:"password"`
- Txs txbuilder.Template `json:"transaction"`
+type submitTxsResp struct {
+ TxID []*bc.Hash `json:"tx_id"`
+}
+
+// POST /submit-transactions
+func (a *API) submitTxs(ctx context.Context, ins struct {
+ Tx []types.Tx `json:"raw_transactions"`
}) Response {
- if err := txbuilder.Sign(ctx, &x.Txs, nil, x.Password, a.pseudohsmSignTemplate); err != nil {
- log.WithField("build err", err).Error("fail on sign transaction.")
- return NewErrorResponse(err)
+ txHashs := []*bc.Hash{}
+ for i := range ins.Tx {
+ if err := txbuilder.FinalizeTx(ctx, a.chain, &ins.Tx[i]); err != nil {
+ return NewErrorResponse(err)
+ }
+ log.WithField("tx_id", ins.Tx[i].ID.String()).Info("submit single tx")
+ txHashs = append(txHashs, &ins.Tx[i].ID)
}
+ return NewSuccessResponse(&submitTxsResp{TxID: txHashs})
+}
- if signCount, complete := txbuilder.SignInfo(&x.Txs); !complete && signCount == 0 {
- return NewErrorResponse(pseudohsm.ErrLoadKey)
- }
- log.Info("Sign Transaction complete.")
+// EstimateTxGasResp estimate transaction consumed gas
+type EstimateTxGasResp struct {
+ TotalNeu int64 `json:"total_neu"`
+ StorageNeu int64 `json:"storage_neu"`
+ VMNeu int64 `json:"vm_neu"`
+}
- txID, err := a.submitSingle(nil, &x.Txs)
+// EstimateTxGas estimate consumed neu for transaction
+func EstimateTxGas(template txbuilder.Template) (*EstimateTxGasResp, error) {
+ // base tx size and not include sign
+ data, err := template.Transaction.TxData.MarshalText()
if err != nil {
- log.WithField("err", err).Error("submit single tx")
- return NewErrorResponse(err)
+ return nil, err
}
+ baseTxSize := int64(len(data))
- log.WithField("tx_id", txID["tx_id"]).Info("submit single tx")
- return NewSuccessResponse(txID)
-}
+ // extra tx size for sign witness parts
+ signSize := estimateSignSize(template.SigningInstructions)
-type calculateTxGasResp struct {
- LeftBTM int64 `json:"left_btm"`
- ConsumedBTM int64 `json:"consumed_btm"`
- LeftGas int64 `json:"left_gas"`
- ConsumedGas int64 `json:"consumed_gas"`
- StorageGas int64 `json:"storage_gas"`
- VMGas int64 `json:"vm_gas"`
-}
+ // total gas for tx storage
+ totalTxSizeGas, ok := checked.MulInt64(baseTxSize+signSize, consensus.StorageGasRate)
+ if !ok {
+ return nil, errors.New("calculate txsize gas got a math error")
+ }
-// POST /calculate-transaction-gas
-func (a *API) calculateGas(ctx context.Context, ins struct {
- Tx types.Tx `json:"raw_transaction"`
-}) Response {
- gasState, err := txbuilder.CalculateTxGas(a.chain, &ins.Tx)
- if err != nil {
- return NewErrorResponse(err)
+ // consume gas for run VM
+ totalP2WPKHGas := int64(0)
+ totalP2WSHGas := int64(0)
+ baseP2WPKHGas := int64(1419)
+ // flexible Gas is used for handle need extra utxo situation
+
+ for pos, inpID := range template.Transaction.Tx.InputIDs {
+ sp, err := template.Transaction.Spend(inpID)
+ if err != nil {
+ continue
+ }
+
+ resOut, err := template.Transaction.Output(*sp.SpentOutputId)
+ if err != nil {
+ continue
+ }
+
+ if segwit.IsP2WPKHScript(resOut.ControlProgram.Code) {
+ totalP2WPKHGas += baseP2WPKHGas
+ } else if segwit.IsP2WSHScript(resOut.ControlProgram.Code) {
+ sigInst := template.SigningInstructions[pos]
+ totalP2WSHGas += estimateP2WSHGas(sigInst)
+ }
}
- btmLeft, ok := checked.MulInt64(gasState.GasLeft, consensus.VMGasRate)
- if !ok {
- return NewErrorResponse(errors.New("calculate btmleft got a math error"))
+ // total estimate gas
+ totalGas := totalTxSizeGas + totalP2WPKHGas + totalP2WSHGas + flexibleGas
+
+ // rounding totalNeu with base rate 100000
+ totalNeu := float64(totalGas*consensus.VMGasRate) / defaultBaseRate
+ roundingNeu := math.Ceil(totalNeu)
+ estimateNeu := int64(roundingNeu) * int64(defaultBaseRate)
+
+ // TODO add priority
+
+ return &EstimateTxGasResp{
+ TotalNeu: estimateNeu,
+ StorageNeu: totalTxSizeGas * consensus.VMGasRate,
+ VMNeu: (totalP2WPKHGas + totalP2WSHGas) * consensus.VMGasRate,
+ }, nil
+}
+
+// estimate p2wsh gas.
+// OP_CHECKMULTISIG consume (984 * a - 72 * b - 63) gas,
+// where a represent the num of public keys, and b represent the num of quorum.
+func estimateP2WSHGas(sigInst *txbuilder.SigningInstruction) int64 {
+ P2WSHGas := int64(0)
+ baseP2WSHGas := int64(738)
+
+ for _, witness := range sigInst.WitnessComponents {
+ switch t := witness.(type) {
+ case *txbuilder.SignatureWitness:
+ P2WSHGas += baseP2WSHGas + (984*int64(len(t.Keys)) - 72*int64(t.Quorum) - 63)
+ case *txbuilder.RawTxSigWitness:
+ P2WSHGas += baseP2WSHGas + (984*int64(len(t.Keys)) - 72*int64(t.Quorum) - 63)
+ }
}
+ return P2WSHGas
+}
- txGasResp := &calculateTxGasResp{
- LeftBTM: btmLeft,
- ConsumedBTM: int64(gasState.BTMValue) - btmLeft,
- LeftGas: gasState.GasLeft,
- ConsumedGas: gasState.GasUsed,
- StorageGas: gasState.StorageGas,
- VMGas: gasState.GasUsed - gasState.StorageGas,
+// estimate signature part size.
+// if need multi-sign, calculate the size according to the length of keys.
+func estimateSignSize(signingInstructions []*txbuilder.SigningInstruction) int64 {
+ signSize := int64(0)
+ baseWitnessSize := int64(300)
+
+ for _, sigInst := range signingInstructions {
+ for _, witness := range sigInst.WitnessComponents {
+ switch t := witness.(type) {
+ case *txbuilder.SignatureWitness:
+ signSize += int64(t.Quorum) * baseWitnessSize
+ case *txbuilder.RawTxSigWitness:
+ signSize += int64(t.Quorum) * baseWitnessSize
+ }
+ }
}
+ return signSize
+}
+// POST /estimate-transaction-gas
+func (a *API) estimateTxGas(ctx context.Context, in struct {
+ TxTemplate txbuilder.Template `json:"transaction_template"`
+}) Response {
+ txGasResp, err := EstimateTxGas(in.TxTemplate)
+ if err != nil {
+ return NewErrorResponse(err)
+ }
return NewSuccessResponse(txGasResp)
}