rosetta-lbry/services/construction_service.go
2020-09-17 14:40:16 -07:00

788 lines
21 KiB
Go

// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/big"
"strconv"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-bitcoin/configuration"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/coinbase/rosetta-sdk-go/parser"
"github.com/coinbase/rosetta-sdk-go/server"
"github.com/coinbase/rosetta-sdk-go/types"
)
const (
// bytesInKB is the number of bytes in a KB. In Bitcoin, this is
// considered to be 1000.
bytesInKb = float64(1000) // nolint:gomnd
// defaultConfirmationTarget is the number of blocks we would
// like our transaction to be included by.
defaultConfirmationTarget = int64(2) // nolint:gomnd
)
// ConstructionAPIService implements the server.ConstructionAPIServicer interface.
type ConstructionAPIService struct {
config *configuration.Configuration
client Client
i Indexer
}
// NewConstructionAPIService creates a new instance of a ConstructionAPIService.
func NewConstructionAPIService(
config *configuration.Configuration,
client Client,
i Indexer,
) server.ConstructionAPIServicer {
return &ConstructionAPIService{
config: config,
client: client,
i: i,
}
}
// ConstructionDerive implements the /construction/derive endpoint.
func (s *ConstructionAPIService) ConstructionDerive(
ctx context.Context,
request *types.ConstructionDeriveRequest,
) (*types.ConstructionDeriveResponse, *types.Error) {
addr, err := btcutil.NewAddressWitnessPubKeyHash(
btcutil.Hash160(request.PublicKey.Bytes),
s.config.Params,
)
if err != nil {
return nil, wrapErr(ErrUnableToDerive, err)
}
return &types.ConstructionDeriveResponse{
AccountIdentifier: &types.AccountIdentifier{
Address: addr.EncodeAddress(),
},
}, nil
}
// estimateSize returns the estimated size of a transaction in vBytes.
func (s *ConstructionAPIService) estimateSize(operations []*types.Operation) float64 {
size := bitcoin.TransactionOverhead
for _, operation := range operations {
switch operation.Type {
case bitcoin.InputOpType:
size += bitcoin.InputSize
case bitcoin.OutputOpType:
size += bitcoin.OutputOverhead
addr, err := btcutil.DecodeAddress(operation.Account.Address, s.config.Params)
if err != nil {
size += bitcoin.P2PKHScriptPubkeySize
continue
}
script, err := txscript.PayToAddrScript(addr)
if err != nil {
size += bitcoin.P2PKHScriptPubkeySize
continue
}
size += len(script)
}
}
return float64(size)
}
// ConstructionPreprocess implements the /construction/preprocess
// endpoint.
func (s *ConstructionAPIService) ConstructionPreprocess(
ctx context.Context,
request *types.ConstructionPreprocessRequest,
) (*types.ConstructionPreprocessResponse, *types.Error) {
descriptions := &parser.Descriptions{
OperationDescriptions: []*parser.OperationDescription{
{
Type: bitcoin.InputOpType,
Account: &parser.AccountDescription{
Exists: true,
},
Amount: &parser.AmountDescription{
Exists: true,
Sign: parser.NegativeAmountSign,
Currency: s.config.Currency,
},
CoinAction: types.CoinSpent,
AllowRepeats: true,
},
},
}
matches, err := parser.MatchOperations(descriptions, request.Operations)
if err != nil {
return nil, wrapErr(ErrUnclearIntent, err)
}
coins := make([]*types.Coin, len(matches[0].Operations))
for i, input := range matches[0].Operations {
if input.CoinChange == nil {
return nil, wrapErr(ErrUnclearIntent, errors.New("CoinChange cannot be nil"))
}
coins[i] = &types.Coin{
CoinIdentifier: input.CoinChange.CoinIdentifier,
Amount: input.Amount,
}
}
options, err := types.MarshalMap(&preprocessOptions{
Coins: coins,
EstimatedSize: s.estimateSize(request.Operations),
FeeMultiplier: request.SuggestedFeeMultiplier,
})
if err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
return &types.ConstructionPreprocessResponse{
Options: options,
}, nil
}
// ConstructionMetadata implements the /construction/metadata endpoint.
func (s *ConstructionAPIService) ConstructionMetadata(
ctx context.Context,
request *types.ConstructionMetadataRequest,
) (*types.ConstructionMetadataResponse, *types.Error) {
if s.config.Mode != configuration.Online {
return nil, wrapErr(ErrUnavailableOffline, nil)
}
var options preprocessOptions
if err := types.UnmarshalMap(request.Options, &options); err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
// Determine feePerKB and ensure it is not below the minimum fee
// relay rate.
feePerKB, err := s.client.SuggestedFeeRate(ctx, defaultConfirmationTarget)
if err != nil {
return nil, wrapErr(ErrCouldNotGetFeeRate, err)
}
if options.FeeMultiplier != nil {
feePerKB *= *options.FeeMultiplier
}
if feePerKB < bitcoin.MinFeeRate {
feePerKB = bitcoin.MinFeeRate
}
// Calculated the estimated fee in Satoshis
satoshisPerB := (feePerKB * float64(bitcoin.SatoshisInBitcoin)) / bytesInKb
estimatedFee := satoshisPerB * options.EstimatedSize
suggestedFee := &types.Amount{
Value: fmt.Sprintf("%d", int64(estimatedFee)),
Currency: s.config.Currency,
}
scripts, err := s.i.GetScriptPubKeys(ctx, options.Coins)
if err != nil {
return nil, wrapErr(ErrScriptPubKeysMissing, err)
}
metadata, err := types.MarshalMap(&constructionMetadata{ScriptPubKeys: scripts})
if err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
return &types.ConstructionMetadataResponse{
Metadata: metadata,
SuggestedFee: []*types.Amount{suggestedFee},
}, nil
}
// ConstructionPayloads implements the /construction/payloads endpoint.
func (s *ConstructionAPIService) ConstructionPayloads(
ctx context.Context,
request *types.ConstructionPayloadsRequest,
) (*types.ConstructionPayloadsResponse, *types.Error) {
descriptions := &parser.Descriptions{
OperationDescriptions: []*parser.OperationDescription{
{
Type: bitcoin.InputOpType,
Account: &parser.AccountDescription{
Exists: true,
},
Amount: &parser.AmountDescription{
Exists: true,
Sign: parser.NegativeAmountSign,
Currency: s.config.Currency,
},
AllowRepeats: true,
CoinAction: types.CoinSpent,
},
{
Type: bitcoin.OutputOpType,
Account: &parser.AccountDescription{
Exists: true,
},
Amount: &parser.AmountDescription{
Exists: true,
Sign: parser.PositiveAmountSign,
Currency: s.config.Currency,
},
AllowRepeats: true,
},
},
ErrUnmatched: true,
}
matches, err := parser.MatchOperations(descriptions, request.Operations)
if err != nil {
return nil, wrapErr(ErrUnclearIntent, err)
}
tx := wire.NewMsgTx(wire.TxVersion)
for _, input := range matches[0].Operations {
if input.CoinChange == nil {
return nil, wrapErr(ErrUnclearIntent, errors.New("CoinChange cannot be nil"))
}
transactionHash, index, err := bitcoin.ParseCoinIdentifier(input.CoinChange.CoinIdentifier)
if err != nil {
return nil, wrapErr(ErrInvalidCoin, err)
}
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *transactionHash,
Index: index,
},
SignatureScript: nil,
Sequence: wire.MaxTxInSequenceNum,
})
}
for i, output := range matches[1].Operations {
addr, err := btcutil.DecodeAddress(output.Account.Address, s.config.Params)
if err != nil {
return nil, wrapErr(ErrUnableToDecodeAddress, fmt.Errorf(
"%w unable to decode address %s",
err,
output.Account.Address,
),
)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to construct payToAddrScript", err),
)
}
tx.AddTxOut(&wire.TxOut{
Value: matches[1].Amounts[i].Int64(),
PkScript: pkScript,
})
}
// Create Signing Payloads (must be done after entire tx is constructed
// or hash will not be correct).
inputAmounts := make([]string, len(tx.TxIn))
inputAddresses := make([]string, len(tx.TxIn))
payloads := make([]*types.SigningPayload, len(tx.TxIn))
var metadata constructionMetadata
if err := types.UnmarshalMap(request.Metadata, &metadata); err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
for i := range tx.TxIn {
address := matches[0].Operations[i].Account.Address
script, err := hex.DecodeString(metadata.ScriptPubKeys[i].Hex)
if err != nil {
return nil, wrapErr(ErrUnableToDecodeScriptPubKey, err)
}
class, _, err := bitcoin.ParseSingleAddress(s.config.Params, script)
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to parse address for utxo %d", err, i),
)
}
inputAddresses[i] = address
inputAmounts[i] = matches[0].Amounts[i].String()
absAmount := new(big.Int).Abs(matches[0].Amounts[i]).Int64()
switch class {
case txscript.WitnessV0PubKeyHashTy:
hash, err := txscript.CalcWitnessSigHash(
script,
txscript.NewTxSigHashes(tx),
txscript.SigHashAll,
tx,
i,
absAmount,
)
if err != nil {
return nil, wrapErr(ErrUnableToCalculateSignatureHash, err)
}
payloads[i] = &types.SigningPayload{
AccountIdentifier: &types.AccountIdentifier{
Address: address,
},
Bytes: hash,
SignatureType: types.Ecdsa,
}
default:
return nil, wrapErr(
ErrUnsupportedScriptType,
fmt.Errorf("unupported script type: %s", class),
)
}
}
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
if err := tx.Serialize(buf); err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
rawTx, err := json.Marshal(&unsignedTransaction{
Transaction: hex.EncodeToString(buf.Bytes()),
ScriptPubKeys: metadata.ScriptPubKeys,
InputAmounts: inputAmounts,
InputAddresses: inputAddresses,
})
if err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
return &types.ConstructionPayloadsResponse{
UnsignedTransaction: hex.EncodeToString(rawTx),
Payloads: payloads,
}, nil
}
func normalizeSignature(signature []byte) []byte {
sig := btcec.Signature{ // signature is in form of R || S
R: new(big.Int).SetBytes(signature[:32]),
S: new(big.Int).SetBytes(signature[32:64]),
}
return append(sig.Serialize(), byte(txscript.SigHashAll))
}
// ConstructionCombine implements the /construction/combine
// endpoint.
func (s *ConstructionAPIService) ConstructionCombine(
ctx context.Context,
request *types.ConstructionCombineRequest,
) (*types.ConstructionCombineResponse, *types.Error) {
decodedTx, err := hex.DecodeString(request.UnsignedTransaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w transaction cannot be decoded", err),
)
}
var unsigned unsignedTransaction
if err := json.Unmarshal(decodedTx, &unsigned); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to unmarshal bitcoin transaction", err),
)
}
decodedCoreTx, err := hex.DecodeString(unsigned.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w transaction cannot be decoded", err),
)
}
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(decodedCoreTx)); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to deserialize tx", err),
)
}
for i := range tx.TxIn {
decodedScript, err := hex.DecodeString(unsigned.ScriptPubKeys[i].Hex)
if err != nil {
return nil, wrapErr(ErrUnableToDecodeScriptPubKey, err)
}
class, _, err := bitcoin.ParseSingleAddress(s.config.Params, decodedScript)
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to parse address for script", err),
)
}
pkData := request.Signatures[i].PublicKey.Bytes
fullsig := normalizeSignature(request.Signatures[i].Bytes)
switch class {
case txscript.WitnessV0PubKeyHashTy:
tx.TxIn[i].Witness = wire.TxWitness{fullsig, pkData}
default:
return nil, wrapErr(
ErrUnsupportedScriptType,
fmt.Errorf("unupported script type: %s", class),
)
}
}
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
if err := tx.Serialize(buf); err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, fmt.Errorf("%w serialize tx", err))
}
rawTx, err := json.Marshal(&signedTransaction{
Transaction: hex.EncodeToString(buf.Bytes()),
InputAmounts: unsigned.InputAmounts,
})
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to serialize signed tx", err),
)
}
return &types.ConstructionCombineResponse{
SignedTransaction: hex.EncodeToString(rawTx),
}, nil
}
// ConstructionHash implements the /construction/hash endpoint.
func (s *ConstructionAPIService) ConstructionHash(
ctx context.Context,
request *types.ConstructionHashRequest,
) (*types.TransactionIdentifierResponse, *types.Error) {
decodedTx, err := hex.DecodeString(request.SignedTransaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w signed transaction cannot be decoded", err),
)
}
var signed signedTransaction
if err := json.Unmarshal(decodedTx, &signed); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to unmarshal signed bitcoin transaction", err),
)
}
bytesTx, err := hex.DecodeString(signed.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to decode hex transaction", err),
)
}
tx, err := btcutil.NewTxFromBytes(bytesTx)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to parse transaction", err),
)
}
return &types.TransactionIdentifierResponse{
TransactionIdentifier: &types.TransactionIdentifier{
Hash: tx.Hash().String(),
},
}, nil
}
func (s *ConstructionAPIService) parseUnsignedTransaction(
request *types.ConstructionParseRequest,
) (*types.ConstructionParseResponse, *types.Error) {
decodedTx, err := hex.DecodeString(request.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w transaction cannot be decoded", err),
)
}
var unsigned unsignedTransaction
if err := json.Unmarshal(decodedTx, &unsigned); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to unmarshal bitcoin transaction", err),
)
}
decodedCoreTx, err := hex.DecodeString(unsigned.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w transaction cannot be decoded", err),
)
}
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(decodedCoreTx)); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to deserialize tx", err),
)
}
ops := []*types.Operation{}
for i, input := range tx.TxIn {
networkIndex := int64(i)
ops = append(ops, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: int64(len(ops)),
NetworkIndex: &networkIndex,
},
Type: bitcoin.InputOpType,
Account: &types.AccountIdentifier{
Address: unsigned.InputAddresses[i],
},
Amount: &types.Amount{
Value: unsigned.InputAmounts[i],
Currency: s.config.Currency,
},
CoinChange: &types.CoinChange{
CoinAction: types.CoinSpent,
CoinIdentifier: &types.CoinIdentifier{
Identifier: fmt.Sprintf(
"%s:%d",
input.PreviousOutPoint.Hash.String(),
input.PreviousOutPoint.Index,
),
},
},
})
}
for i, output := range tx.TxOut {
networkIndex := int64(i)
_, addr, err := bitcoin.ParseSingleAddress(s.config.Params, output.PkScript)
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to parse output address", err),
)
}
ops = append(ops, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: int64(len(ops)),
NetworkIndex: &networkIndex,
},
Type: bitcoin.OutputOpType,
Account: &types.AccountIdentifier{
Address: addr.String(),
},
Amount: &types.Amount{
Value: strconv.FormatInt(output.Value, 10),
Currency: s.config.Currency,
},
})
}
return &types.ConstructionParseResponse{
Operations: ops,
AccountIdentifierSigners: []*types.AccountIdentifier{},
}, nil
}
func (s *ConstructionAPIService) parseSignedTransaction(
request *types.ConstructionParseRequest,
) (*types.ConstructionParseResponse, *types.Error) {
decodedTx, err := hex.DecodeString(request.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w signed transaction cannot be decoded", err),
)
}
var signed signedTransaction
if err := json.Unmarshal(decodedTx, &signed); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to unmarshal signed bitcoin transaction", err),
)
}
serializedTx, err := hex.DecodeString(signed.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to decode hex transaction", err),
)
}
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(serializedTx)); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to decode msgTx", err),
)
}
ops := []*types.Operation{}
signers := []*types.AccountIdentifier{}
for i, input := range tx.TxIn {
pkScript, err := txscript.ComputePkScript(input.SignatureScript, input.Witness)
if err != nil {
return nil, wrapErr(
ErrUnableToComputePkScript,
fmt.Errorf("%w: unable to compute pk script", err),
)
}
_, addr, err := bitcoin.ParseSingleAddress(s.config.Params, pkScript.Script())
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to decode address", err),
)
}
networkIndex := int64(i)
signers = append(signers, &types.AccountIdentifier{
Address: addr.EncodeAddress(),
})
ops = append(ops, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: int64(len(ops)),
NetworkIndex: &networkIndex,
},
Type: bitcoin.InputOpType,
Account: &types.AccountIdentifier{
Address: addr.EncodeAddress(),
},
Amount: &types.Amount{
Value: signed.InputAmounts[i],
Currency: s.config.Currency,
},
CoinChange: &types.CoinChange{
CoinAction: types.CoinSpent,
CoinIdentifier: &types.CoinIdentifier{
Identifier: fmt.Sprintf(
"%s:%d",
input.PreviousOutPoint.Hash.String(),
input.PreviousOutPoint.Index,
),
},
},
})
}
for i, output := range tx.TxOut {
networkIndex := int64(i)
_, addr, err := bitcoin.ParseSingleAddress(s.config.Params, output.PkScript)
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to parse output address", err),
)
}
ops = append(ops, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: int64(len(ops)),
NetworkIndex: &networkIndex,
},
Type: bitcoin.OutputOpType,
Account: &types.AccountIdentifier{
Address: addr.String(),
},
Amount: &types.Amount{
Value: strconv.FormatInt(output.Value, 10),
Currency: s.config.Currency,
},
})
}
return &types.ConstructionParseResponse{
Operations: ops,
AccountIdentifierSigners: signers,
}, nil
}
// ConstructionParse implements the /construction/parse endpoint.
func (s *ConstructionAPIService) ConstructionParse(
ctx context.Context,
request *types.ConstructionParseRequest,
) (*types.ConstructionParseResponse, *types.Error) {
if request.Signed {
return s.parseSignedTransaction(request)
}
return s.parseUnsignedTransaction(request)
}
// ConstructionSubmit implements the /construction/submit endpoint.
func (s *ConstructionAPIService) ConstructionSubmit(
ctx context.Context,
request *types.ConstructionSubmitRequest,
) (*types.TransactionIdentifierResponse, *types.Error) {
if s.config.Mode != configuration.Online {
return nil, wrapErr(ErrUnavailableOffline, nil)
}
decodedTx, err := hex.DecodeString(request.SignedTransaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w signed transaction cannot be decoded", err),
)
}
var signed signedTransaction
if err := json.Unmarshal(decodedTx, &signed); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to unmarshal signed bitcoin transaction", err),
)
}
txHash, err := s.client.SendRawTransaction(ctx, signed.Transaction)
if err != nil {
return nil, wrapErr(ErrBitcoind, fmt.Errorf("%w unable to submit transaction", err))
}
return &types.TransactionIdentifierResponse{
TransactionIdentifier: &types.TransactionIdentifier{
Hash: txHash,
},
}, nil
}