package rpc import ( "bytes" "encoding/hex" "fmt" "github.com/kaspanet/kaspad/dagconfig" "github.com/kaspanet/kaspad/domainmessage" "github.com/kaspanet/kaspad/rpc/model" "github.com/kaspanet/kaspad/txscript" "github.com/kaspanet/kaspad/util" "github.com/kaspanet/kaspad/util/daghash" "github.com/kaspanet/kaspad/util/pointers" "math/big" "strconv" ) var ( // ErrRPCUnimplemented is an error returned to RPC clients when the // provided command is recognized, but not implemented. ErrRPCUnimplemented = &model.RPCError{ Code: model.ErrRPCUnimplemented, Message: "Command unimplemented", } ) // internalRPCError is a convenience function to convert an internal error to // an RPC error with the appropriate code set. It also logs the error to the // RPC server subsystem since internal errors really should not occur. The // context parameter is only used in the log message and may be empty if it's // not needed. func internalRPCError(errStr, context string) *model.RPCError { logStr := errStr if context != "" { logStr = context + ": " + errStr } log.Error(logStr) return model.NewRPCError(model.ErrRPCInternal.Code, errStr) } // rpcDecodeHexError is a convenience function for returning a nicely formatted // RPC error which indicates the provided hex string failed to decode. func rpcDecodeHexError(gotHex string) *model.RPCError { return model.NewRPCError(model.ErrRPCDecodeHexString, fmt.Sprintf("Argument must be hexadecimal string (not %q)", gotHex)) } // rpcNoTxInfoError is a convenience function for returning a nicely formatted // RPC error which indicates there is no information available for the provided // transaction hash. func rpcNoTxInfoError(txID *daghash.TxID) *model.RPCError { return model.NewRPCError(model.ErrRPCNoTxInfo, fmt.Sprintf("No information available about transaction %s", txID)) } // msgTxToHex serializes a transaction using the latest protocol version and // returns a hex-encoded string of the result. func msgTxToHex(msgTx *domainmessage.MsgTx) (string, error) { var buf bytes.Buffer if err := msgTx.KaspaEncode(&buf, maxProtocolVersion); err != nil { context := fmt.Sprintf("Failed to encode msg of type %T", msgTx) return "", internalRPCError(err.Error(), context) } return hex.EncodeToString(buf.Bytes()), nil } // createVinList returns a slice of JSON objects for the inputs of the passed // transaction. func createVinList(mtx *domainmessage.MsgTx) []model.Vin { vinList := make([]model.Vin, len(mtx.TxIn)) for i, txIn := range mtx.TxIn { // The disassembled string will contain [error] inline // if the script doesn't fully parse, so ignore the // error here. disbuf, _ := txscript.DisasmString(txIn.SignatureScript) vinEntry := &vinList[i] vinEntry.TxID = txIn.PreviousOutpoint.TxID.String() vinEntry.Vout = txIn.PreviousOutpoint.Index vinEntry.Sequence = txIn.Sequence vinEntry.ScriptSig = &model.ScriptSig{ Asm: disbuf, Hex: hex.EncodeToString(txIn.SignatureScript), } } return vinList } // createVoutList returns a slice of JSON objects for the outputs of the passed // transaction. func createVoutList(mtx *domainmessage.MsgTx, dagParams *dagconfig.Params, filterAddrMap map[string]struct{}) []model.Vout { voutList := make([]model.Vout, 0, len(mtx.TxOut)) for i, v := range mtx.TxOut { // The disassembled string will contain [error] inline if the // script doesn't fully parse, so ignore the error here. disbuf, _ := txscript.DisasmString(v.ScriptPubKey) // Ignore the error here since an error means the script // couldn't parse and there is no additional information about // it anyways. scriptClass, addr, _ := txscript.ExtractScriptPubKeyAddress( v.ScriptPubKey, dagParams) // Encode the addresses while checking if the address passes the // filter when needed. passesFilter := len(filterAddrMap) == 0 var encodedAddr *string if addr != nil { encodedAddr = pointers.String(addr.EncodeAddress()) // If the filter doesn't already pass, make it pass if // the address exists in the filter. if _, exists := filterAddrMap[*encodedAddr]; exists { passesFilter = true } } if !passesFilter { continue } var vout model.Vout vout.N = uint32(i) vout.Value = v.Value vout.ScriptPubKey.Address = encodedAddr vout.ScriptPubKey.Asm = disbuf vout.ScriptPubKey.Hex = hex.EncodeToString(v.ScriptPubKey) vout.ScriptPubKey.Type = scriptClass.String() voutList = append(voutList, vout) } return voutList } // createTxRawResult converts the passed transaction and associated parameters // to a raw transaction JSON object. func createTxRawResult(dagParams *dagconfig.Params, mtx *domainmessage.MsgTx, txID string, blkHeader *domainmessage.BlockHeader, blkHash string, acceptingBlock *daghash.Hash, isInMempool bool) (*model.TxRawResult, error) { mtxHex, err := msgTxToHex(mtx) if err != nil { return nil, err } var payloadHash string if mtx.PayloadHash != nil { payloadHash = mtx.PayloadHash.String() } txReply := &model.TxRawResult{ Hex: mtxHex, TxID: txID, Hash: mtx.TxHash().String(), Size: int32(mtx.SerializeSize()), Vin: createVinList(mtx), Vout: createVoutList(mtx, dagParams, nil), Version: mtx.Version, LockTime: mtx.LockTime, Subnetwork: mtx.SubnetworkID.String(), Gas: mtx.Gas, PayloadHash: payloadHash, Payload: hex.EncodeToString(mtx.Payload), } if blkHeader != nil { txReply.Time = uint64(blkHeader.Timestamp.UnixMilliseconds()) txReply.BlockTime = uint64(blkHeader.Timestamp.UnixMilliseconds()) txReply.BlockHash = blkHash } txReply.IsInMempool = isInMempool if acceptingBlock != nil { txReply.AcceptedBy = pointers.String(acceptingBlock.String()) } return txReply, nil } // getDifficultyRatio returns the proof-of-work difficulty as a multiple of the // minimum difficulty using the passed bits field from the header of a block. func getDifficultyRatio(bits uint32, params *dagconfig.Params) float64 { // The minimum difficulty is the max possible proof-of-work limit bits // converted back to a number. Note this is not the same as the proof of // work limit directly because the block difficulty is encoded in a block // with the compact form which loses precision. target := util.CompactToBig(bits) difficulty := new(big.Rat).SetFrac(params.PowMax, target) outString := difficulty.FloatString(8) diff, err := strconv.ParseFloat(outString, 64) if err != nil { log.Errorf("Cannot get difficulty: %s", err) return 0 } return diff } // buildGetBlockVerboseResult takes a block and convert it to model.GetBlockVerboseResult // // This function MUST be called with the DAG state lock held (for reads). func buildGetBlockVerboseResult(s *Server, block *util.Block, isVerboseTx bool) (*model.GetBlockVerboseResult, error) { hash := block.Hash() params := s.dag.Params blockHeader := block.MsgBlock().Header blockBlueScore, err := s.dag.BlueScoreByBlockHash(hash) if err != nil { context := "Could not get block blue score" return nil, internalRPCError(err.Error(), context) } // Get the hashes for the next blocks unless there are none. childHashes, err := s.dag.ChildHashesByHash(hash) if err != nil { context := "No next block" return nil, internalRPCError(err.Error(), context) } blockConfirmations, err := s.dag.BlockConfirmationsByHashNoLock(hash) if err != nil { context := "Could not get block confirmations" return nil, internalRPCError(err.Error(), context) } selectedParentHash, err := s.dag.SelectedParentHash(hash) if err != nil { context := "Could not get block selected parent" return nil, internalRPCError(err.Error(), context) } selectedParentHashStr := "" if selectedParentHash != nil { selectedParentHashStr = selectedParentHash.String() } isChainBlock, err := s.dag.IsInSelectedParentChain(hash) if err != nil { context := "Could not get whether block is in the selected parent chain" return nil, internalRPCError(err.Error(), context) } acceptedBlockHashes, err := s.dag.BluesByBlockHash(hash) if err != nil { context := fmt.Sprintf("Could not get block accepted blocks for block %s", hash) return nil, internalRPCError(err.Error(), context) } result := &model.GetBlockVerboseResult{ Hash: hash.String(), Version: blockHeader.Version, VersionHex: fmt.Sprintf("%08x", blockHeader.Version), HashMerkleRoot: blockHeader.HashMerkleRoot.String(), AcceptedIDMerkleRoot: blockHeader.AcceptedIDMerkleRoot.String(), UTXOCommitment: blockHeader.UTXOCommitment.String(), ParentHashes: daghash.Strings(blockHeader.ParentHashes), SelectedParentHash: selectedParentHashStr, Nonce: blockHeader.Nonce, Time: blockHeader.Timestamp.UnixMilliseconds(), Confirmations: blockConfirmations, BlueScore: blockBlueScore, IsChainBlock: isChainBlock, Size: int32(block.MsgBlock().SerializeSize()), Bits: strconv.FormatInt(int64(blockHeader.Bits), 16), Difficulty: getDifficultyRatio(blockHeader.Bits, params), ChildHashes: daghash.Strings(childHashes), AcceptedBlockHashes: daghash.Strings(acceptedBlockHashes), } if isVerboseTx { transactions := block.Transactions() txNames := make([]string, len(transactions)) for i, tx := range transactions { txNames[i] = tx.ID().String() } result.Tx = txNames } else { txns := block.Transactions() rawTxns := make([]model.TxRawResult, len(txns)) for i, tx := range txns { rawTxn, err := createTxRawResult(params, tx.MsgTx(), tx.ID().String(), &blockHeader, hash.String(), nil, false) if err != nil { return nil, err } rawTxns[i] = *rawTxn } result.RawTx = rawTxns } return result, nil } func collectChainBlocks(s *Server, hashes []*daghash.Hash) ([]model.ChainBlock, error) { chainBlocks := make([]model.ChainBlock, 0, len(hashes)) for _, hash := range hashes { acceptanceData, err := s.acceptanceIndex.TxsAcceptanceData(hash) if err != nil { return nil, &model.RPCError{ Code: model.ErrRPCInternal.Code, Message: fmt.Sprintf("could not retrieve acceptance data for block %s", hash), } } acceptedBlocks := make([]model.AcceptedBlock, 0, len(acceptanceData)) for _, blockAcceptanceData := range acceptanceData { acceptedTxIds := make([]string, 0, len(blockAcceptanceData.TxAcceptanceData)) for _, txAcceptanceData := range blockAcceptanceData.TxAcceptanceData { if txAcceptanceData.IsAccepted { acceptedTxIds = append(acceptedTxIds, txAcceptanceData.Tx.ID().String()) } } acceptedBlock := model.AcceptedBlock{ Hash: blockAcceptanceData.BlockHash.String(), AcceptedTxIDs: acceptedTxIds, } acceptedBlocks = append(acceptedBlocks, acceptedBlock) } chainBlock := model.ChainBlock{ Hash: hash.String(), AcceptedBlocks: acceptedBlocks, } chainBlocks = append(chainBlocks, chainBlock) } return chainBlocks, nil } // hashesToGetBlockVerboseResults takes block hashes and returns their // correspondent block verbose. // // This function MUST be called with the DAG state lock held (for reads). func hashesToGetBlockVerboseResults(s *Server, hashes []*daghash.Hash) ([]model.GetBlockVerboseResult, error) { getBlockVerboseResults := make([]model.GetBlockVerboseResult, 0, len(hashes)) for _, blockHash := range hashes { block, err := s.dag.BlockByHash(blockHash) if err != nil { return nil, &model.RPCError{ Code: model.ErrRPCInternal.Code, Message: fmt.Sprintf("could not retrieve block %s.", blockHash), } } getBlockVerboseResult, err := buildGetBlockVerboseResult(s, block, false) if err != nil { return nil, &model.RPCError{ Code: model.ErrRPCInternal.Code, Message: fmt.Sprintf("could not build getBlockVerboseResult for block %s: %s", blockHash, err), } } getBlockVerboseResults = append(getBlockVerboseResults, *getBlockVerboseResult) } return getBlockVerboseResults, nil }