// 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 }