lbryio/chainquery

View on GitHub
lbrycrd/script.go

Summary

Maintainability
C
1 day
Test Coverage
package lbrycrd

import (
    "bytes"
    "encoding/binary"
    "encoding/hex"

    "github.com/golang/protobuf/proto"

    "github.com/lbryio/chainquery/global"
    "github.com/lbryio/chainquery/util"

    "github.com/lbryio/lbry.go/v2/extras/errors"

    "github.com/btcsuite/btcd/chaincfg"
    "github.com/btcsuite/btcd/txscript"
    "github.com/btcsuite/btcutil"
    pb "github.com/lbryio/types/v2/go"
    log "github.com/sirupsen/logrus"
)

const (
    //lbrycrd                //btcd
    opClaimName    = 0xb5 //OP_NOP6             = 181
    opSupportClaim = 0xb6 //OP_NOP7             = 182
    opUpdateClaim  = 0xb7 //OP_NOP8             = 183
    opReturn       = 0x6a //OP_RETURN           = 106
    purchase       = 0x50 //PURCHASE = 80
    opDup          = 0x76 //opDup                 = 118
    opChecksig     = 0xac //opChecksig             = 172
    op2drop        = 0x6d //op2Drop
    opEqualverify  = 0x88 //opEqualverify         = 136
    opHash160      = 0xa9 //opHash160            = 169
    opPushdata1    = 0x4c //opPushdata1          = 76
    opPushdata2    = 0x4d //opPushdata2         = 77
    opPushdata4    = 0x4e //opPushdata4         = 78

    // Types of vOut scripts
    p2SH   = "scripthash"            // Pay to Script Hash
    p2PK   = "pubkey"                // Pay to Public Key
    p2PKH  = "pubkeyhash"            // Pay to Public Key Hash
    p2WPKH = "witness_v0_keyhash"    //Segwit Pub Key Hash
    p2WSH  = "witness_v0_scripthash" //Segwit Script Hash
    // NonStandard is a transaction type, usually used for a claim.
    NonStandard = "nonstandard"
    // NullData Transaction type related to segwit outputs
    NullData = "nulldata"

    lbrycrdMainPubkeyPrefix    = byte(85)
    lbrycrdMainScriptPrefix    = byte(122)
    lbrycrdTestnetPubkeyPrefix = byte(111)
    lbrycrdTestnetScriptPrefix = byte(196)
    lbrycrdRegtestPubkeyPrefix = byte(111)
    lbrycrdRegtestScriptPrefix = byte(196)

    lbrycrdMain    = "lbrycrd_main"
    lbrycrdTestnet = "lbrycrd_testnet"
    lbrycrdRegtest = "lbrycrd_regtest"
)

var mainNetParams = chaincfg.Params{
    PubKeyHashAddrID: lbrycrdMainPubkeyPrefix,
    ScriptHashAddrID: lbrycrdMainScriptPrefix,
    PrivateKeyID:     0x1c,
    Bech32HRPSegwit:  "lbc",
}

var testNetParams = chaincfg.Params{
    PubKeyHashAddrID: lbrycrdTestnetPubkeyPrefix,
    ScriptHashAddrID: lbrycrdTestnetScriptPrefix,
    PrivateKeyID:     0x1c,
    Bech32HRPSegwit:  "tlbc",
}

var regTestNetParams = chaincfg.Params{
    PubKeyHashAddrID: lbrycrdRegtestPubkeyPrefix,
    ScriptHashAddrID: lbrycrdRegtestScriptPrefix,
    PrivateKeyID:     0x1c,
    Bech32HRPSegwit:  "rlbc",
}

var paramsMap = map[string]chaincfg.Params{lbrycrdMain: mainNetParams, lbrycrdTestnet: testNetParams, lbrycrdRegtest: regTestNetParams}

//GetChainParams returns the currently set blockchain name as the chain parameters. Set in the config.
func GetChainParams() (*chaincfg.Params, error) {
    chainParams, ok := paramsMap[global.BlockChainName]
    if !ok {
        return nil, errors.Err("unknown chain name %s", global.BlockChainName)
    }

    return &chainParams, nil
}

// IsClaimScript return true if the script for the vout contains the right opt codes pertaining to a claim.
func IsClaimScript(script []byte) bool {
    return script[0] == opSupportClaim ||
        script[0] == opClaimName ||
        script[0] == opUpdateClaim
}

// IsClaimNameScript returns true if the script for the vout contains the OP_CLAIM_NAME code.
func IsClaimNameScript(script []byte) bool {
    if len(script) > 0 {
        return script[0] == opClaimName
    }
    log.Error("script is nil or length 0!")
    return false
}

// IsClaimSupportScript returns true if the script for the vout contains the OP_CLAIM_SUPPORT code.
func IsClaimSupportScript(script []byte) bool {
    if len(script) > 0 {
        return script[0] == opSupportClaim
    }
    return false
}

// IsClaimUpdateScript returns true if the script for the vout contains the OP_CLAIM_UPDATE code.
func IsClaimUpdateScript(script []byte) bool {
    if len(script) > 0 {
        return script[0] == opUpdateClaim
    }
    return false
}

// IsPurchaseScript returns true if the script for the vout contains the OP_RETURN + 'P' byte identifier for a purchase
func IsPurchaseScript(script []byte) bool {
    if len(script) > 2 {
        if script[0] == opReturn && script[2] == purchase {
            _, err := ParsePurchaseScript(script)
            return err == nil
        }
    }
    return false
}

// ParseClaimNameScript parses a script for the claim of a name.
func ParseClaimNameScript(script []byte) (name string, value []byte, pubkeyscript []byte, err error) {
    // Already validated by blockchain so can be assumed
    // opClaimName Name Value OP_2DROP OP_DROP pubkeyscript
    nameBytesToRead := int(script[1])
    nameStart := 2
    if nameBytesToRead == opPushdata1 {
        nameBytesToRead = int(script[2])
        nameStart = 3
    } else if nameBytesToRead > opPushdata1 {
        panic(errors.Base("Bytes to read is more than next byte! "))
    }
    nameEnd := nameStart + nameBytesToRead
    name = string(script[nameStart:nameEnd])
    dataPushType := int(script[nameEnd])
    valueBytesToRead := int(script[nameEnd])
    valueStart := nameEnd + 1
    if dataPushType == opPushdata1 {
        valueBytesToRead = int(script[nameEnd+1])
        valueStart = nameEnd + 2
    } else if dataPushType == opPushdata2 {
        valueStart = nameEnd + 3
        valueBytesToRead = int(binary.LittleEndian.Uint16(script[nameEnd+1 : valueStart]))
    } else if dataPushType == opPushdata4 {
        valueStart = nameEnd + 5
        valueBytesToRead = int(binary.LittleEndian.Uint32(script[nameEnd+2 : valueStart]))
    }
    valueEnd := valueStart + valueBytesToRead
    value = script[valueStart:valueEnd]
    pksStart := valueEnd + 2         // +2 to ignore OP_2DROP and OP_DROP
    pubkeyscript = script[pksStart:] //Remainder is always pubkeyscript

    return name, value, pubkeyscript, err
}

// ParseClaimSupportScript parses a script for a support of a claim.
func ParseClaimSupportScript(script []byte) (name string, claimid string, value []byte, pubkeyscript []byte, err error) {
    // Already validated by blockchain so can be assumed
    // opSupportClaim vchName vchClaimId OP_2DROP OP_DROP pubkeyscript

    //Name
    nameBytesToRead := int(script[1])
    nameStart := 2
    if nameBytesToRead == opPushdata1 {
        nameBytesToRead = int(script[2])
        nameStart = 3
    } else if nameBytesToRead > opPushdata1 {
        err = errors.Err("Bytes to read is more than next byte! ")
        return
    }
    nameEnd := nameStart + nameBytesToRead
    name = string(script[nameStart:nameEnd])

    //ClaimID
    claimidBytesToRead := int(script[nameEnd])
    claimidStart := nameEnd + 1
    claimidEnd := claimidStart + claimidBytesToRead
    claimIDBytes := util.ReverseBytes(script[claimidStart:claimidEnd])
    claimid = hex.EncodeToString(claimIDBytes)
    //OP_SUPPORT_CLAIM vchName vchClaimId OP_2DROP OP_DROP pubkeyscript
    pksStart := claimidEnd + 2 // +2 to ignore OP_2DROP and OP_DROP

    if script[claimidEnd+1] != op2drop {
        var vSize uint64
        vSize, _, err = readCompactSize(bytes.NewBuffer(script[claimidEnd+1:]))
        if err != nil {
            return
        }
        if len(script) < claimidEnd+1+int(vSize) {
            //log.Error("intended support for claim ", claimid, " is invalid")
        } else {
            value = script[claimidEnd+1 : claimidEnd+1+int(vSize)]
            //OP_SUPPORT_CLAIM vchName vchClaimId vchValue OP_2DROP OP_2DROP pubkeyscript
            pksStart = claimidEnd + 1 + int(vSize)
        }
    }

    //PubKeyScript
    pubkeyscript = script[pksStart:] //Remainder is always pubkeyscript
    return
}

// ParseClaimUpdateScript parses a script for an update of a claim.
func ParseClaimUpdateScript(script []byte) (name string, claimid string, value []byte, pubkeyscript []byte, err error) {
    // opUpdateClaim Name ClaimID Value OP_2DROP OP_2DROP pubkeyscript

    //Name
    nameBytesToRead := int(script[1])
    nameStart := 2
    if nameBytesToRead == opPushdata1 {
        nameBytesToRead = int(script[2])
        nameStart = 3
    } else if nameBytesToRead > opPushdata1 {
        err = errors.Err("ParseClaimUpdateScript: Bytes to read is more than next byte! ")
        return
    }
    nameEnd := nameStart + nameBytesToRead
    name = string(script[nameStart:nameEnd])

    //ClaimID
    claimidBytesToRead := int(script[nameEnd])
    claimidStart := nameEnd + 1
    claimidEnd := claimidStart + claimidBytesToRead
    bytes := util.ReverseBytes(script[claimidStart:claimidEnd])
    claimid = hex.EncodeToString(bytes)

    //Value
    dataPushType := int(script[claimidEnd])
    valueBytesToRead := int(script[claimidEnd])
    valueStart := claimidEnd + 1
    if dataPushType == opPushdata1 {
        valueBytesToRead = int(script[claimidEnd+1])
        valueStart = claimidEnd + 2
    } else if dataPushType == opPushdata2 {
        valueStart = claimidEnd + 3
        valueBytesToRead = int(binary.LittleEndian.Uint16(script[claimidEnd+1 : valueStart]))
    } else if dataPushType == opPushdata4 {
        valueStart = claimidEnd + 5
        valueBytesToRead = int(binary.LittleEndian.Uint32(script[claimidEnd+2 : valueStart]))
    }
    valueEnd := valueStart + valueBytesToRead
    value = script[valueStart:valueEnd]

    //PublicKeyScript
    pksStart := valueEnd + 2         // +2 to ignore OP_2DROP and OP_DROP
    pubkeyscript = script[pksStart:] //Remainder is always pubkeyscript

    return name, claimid, value, pubkeyscript, err
}

//ErrNotClaimScript is a base error for when a script cannot be parsed as a claim script.
var ErrNotClaimScript = errors.Base("Script is not a claim script!")

// GetPubKeyScriptFromClaimPKS gets the public key script at the end of a claim script.
func GetPubKeyScriptFromClaimPKS(script []byte) (pubkeyscript []byte, err error) {
    if IsClaimScript(script) {
        if IsClaimNameScript(script) {
            _, _, pubkeyscript, err = ParseClaimNameScript(script)
            if err != nil {
                return nil, errors.Err(err)
            }
            return pubkeyscript, nil
        } else if IsClaimUpdateScript(script) {
            _, _, _, pubkeyscript, err = ParseClaimUpdateScript(script)
            if err != nil {
                return
            }
            return
        } else if IsClaimSupportScript(script) {
            _, _, _, pubkeyscript, err = ParseClaimSupportScript(script)
            if err != nil {
                return
            }
            return
        }
    } else {
        err = ErrNotClaimScript
    }
    return
}

// GetAddressFromPublicKeyScript returns the address associated with a public key script.
func GetAddressFromPublicKeyScript(script []byte) (address string) {
    chainParams, err := GetChainParams()
    if err != nil {
        return ""
    }
    _, BTCAddress, _, err := txscript.ExtractPkScriptAddrs(script, chainParams)
    if len(BTCAddress) < 1 {
        return ""
    }
    address = BTCAddress[0].EncodeAddress()

    return address
}

func getPublicKeyScriptType(script []byte) string {
    if isPayToPublicKey(script) {
        return p2PK
    } else if isPayToPublicKeyHashScript(script) {
        return p2PKH
    } else if isPayToScriptHashScript(script) {
        return p2SH
    } else if txscript.IsPayToWitnessPubKeyHash(script) {
        return p2WPKH
    } else if txscript.IsPayToWitnessScriptHash(script) {
        return p2WSH
    }
    return NonStandard
}

func isPayToScriptHashScript(script []byte) bool {
    if len(script) > 0 {
        return script[0] == opUpdateClaim
    }
    return false
}

func isPayToPublicKey(script []byte) bool {
    return script[len(script)-1] == opChecksig &&
        script[len(script)-2] == opEqualverify &&
        script[0] != opDup

}

func isPayToPublicKeyHashScript(script []byte) bool {
    return len(script) > 0 &&
        script[0] == opDup &&
        script[1] == opHash160

}

func getAddressFromP2PK(hexstring string) (string, error) {
    hexstringBytes, err := hex.DecodeString(hexstring)
    if err != nil {
        return "", err
    }
    chainParams, err := GetChainParams()
    if err != nil {
        return "", errors.Err(err)
    }
    addr, err := btcutil.NewAddressPubKey(hexstringBytes, chainParams)
    if err != nil {
        return "", err
    }
    address := addr.EncodeAddress()

    return address, nil
}

func getAddressFromP2PKH(hexstring string) (string, error) {
    hexstringBytes, err := hex.DecodeString(hexstring)
    if err != nil {
        return "", err
    }
    chainParams, err := GetChainParams()
    if err != nil {
        return "", errors.Err(err)
    }
    addr, err := btcutil.NewAddressPubKeyHash(hexstringBytes, chainParams)
    if err != nil {
        return "", err
    }
    address := addr.EncodeAddress()
    return address, nil

}

func getAddressFromP2SH(hexstring string) (string, error) {
    hexstringBytes, err := hex.DecodeString(hexstring)
    if err != nil {
        return "", err
    }

    chainParams, err := GetChainParams()
    if err != nil {
        return "", errors.Err(err)
    }
    addr, err := btcutil.NewAddressScriptHashFromHash(hexstringBytes, chainParams)
    if err != nil {
        return "", err
    }
    address := addr.EncodeAddress()
    return address, nil
}

func getAddressFromP2WPKH(hexstring string) (string, error) {
    witnessProgram, err := hex.DecodeString(hexstring)
    if err != nil {
        return "", err
    }

    chainParams, err := GetChainParams()
    if err != nil {
        return "", errors.Err(err)
    }
    addr, err := btcutil.NewAddressWitnessPubKeyHash(witnessProgram, chainParams)
    if err != nil {
        return "", err
    }
    address := addr.EncodeAddress()
    return address, nil
}

func getAddressFromP2WSH(hexstring string) (string, error) {
    witnessProgram, err := hex.DecodeString(hexstring)
    if err != nil {
        return "", err
    }

    chainParams, err := GetChainParams()
    if err != nil {
        return "", errors.Err(err)
    }
    addr, err := btcutil.NewAddressWitnessScriptHash(witnessProgram, chainParams)
    if err != nil {
        return "", err
    }
    address := addr.EncodeAddress()
    return address, nil
}

func parseDataScript(script []byte) ([]byte, error) {
    // OP_RETURN (bytes) DATA
    if len(script) <= 1 {
        return nil, errors.Err("there is no script to parse")
    }
    if script[0] != opReturn {
        return nil, errors.Err("the first byte of script must be an OP_RETURN to quality as un-spendable data")
    }
    dataBytesToRead := int(script[1])
    if (len(script) - dataBytesToRead - 2) != 0 {
        return nil, errors.Err("supposed to have %d bytes to read but the script is %d bytes", dataBytesToRead, len(script))
    }
    return script[2:], nil
}

// ParsePurchaseScript returns the purchase from script bytes or errors if invalid
func ParsePurchaseScript(script []byte) (*pb.Purchase, error) {
    data, err := parseDataScript(script)
    if err != nil {
        return nil, err
    }
    if data[0] != purchase {
        return nil, errors.Err("the first byte must be 'P'(0x50) to be a purchase script")
    }
    purchase := &pb.Purchase{}
    err = proto.Unmarshal(data[1:], purchase)
    if err != nil {
        return nil, errors.Err(err)
    }
    return purchase, nil
}

func readCompactSize(bs *bytes.Buffer) (uint64, []byte, error) {
    var readBuf []byte
    bSize := make([]byte, 1)
    _, err := bs.Read(bSize)
    if err != nil {
        return 0, nil, errors.Err(err)
    }
    readBuf = append(readBuf, bSize...)

    size := uint64(bSize[0])
    if size < 253 {
        return size, readBuf, nil
    }

    if size == 253 {
        buf := make([]byte, 2)
        _, err := bs.Read(buf)
        if err != nil {
            return 0, nil, errors.Err(err)
        }
        readBuf = append(readBuf, buf...)

        return uint64(binary.LittleEndian.Uint16(buf)), readBuf, nil
    }
    if size == 254 {
        v, buf, err := readUint32(bs)
        if err != nil {
            return 0, nil, err
        }
        readBuf = append(readBuf, buf...)
        return uint64(v), readBuf, err
    }

    if size == 255 {
        buf := make([]byte, 8)
        readBuf = append(readBuf, buf...)
        return binary.LittleEndian.Uint64(buf), readBuf, nil
    }

    return 0, nil, errors.Err("size is greater than 255")
}

func readUint32(bs *bytes.Buffer) (uint32, []byte, error) {
    buf, err := readBytes(4, bs)
    if err != nil {
        return 0, nil, errors.Err(err)
    }
    return binary.LittleEndian.Uint32(buf), buf, nil
}

func readBytes(toRead int, bs *bytes.Buffer) ([]byte, error) {
    buf := make([]byte, toRead)
    _, err := bs.Read(buf)
    if err != nil {
        return nil, errors.Err(err)
    }
    return buf, nil
}