From 4d17553a238ac442d9882719a83d43f7c37c17b6 Mon Sep 17 00:00:00 2001 From: Mark Beamer Jr Date: Wed, 14 Aug 2019 00:44:27 -0400 Subject: [PATCH] Add claim management for lbrycrd client --- go.mod | 2 +- go.sum | 2 + lbrycrd/channel.go | 39 ++++++++++++ lbrycrd/claim.go | 86 +++++++++++++++++++++++++ lbrycrd/client.go | 156 ++++++++++++++++++++++++++++++++++++++------- lbrycrd/finder.go | 38 +++++++++++ lbrycrd/script.go | 76 ++++++++++++++++++++++ 7 files changed, 374 insertions(+), 25 deletions(-) create mode 100644 lbrycrd/channel.go create mode 100644 lbrycrd/claim.go create mode 100644 lbrycrd/finder.go create mode 100644 lbrycrd/script.go diff --git a/go.mod b/go.mod index 20f9c2a..32563bf 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/gorilla/websocket v1.2.0 // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c - github.com/lbryio/lbryschema.go v0.0.0-20190428231007-c54836bca002 + github.com/lbryio/lbryschema.go v0.0.0-20190602173230-6d2f69a36f46 github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04 github.com/lbryio/types v0.0.0-20190422033210-321fb2abda9c github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect diff --git a/go.sum b/go.sum index 1a7c58c..f790c78 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c h1:BhdcWGsuKif/Xo github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c/go.mod h1:muH7wpUqE8hRA3OrYYosw9+Sl681BF9cwcjzE+OCNK8= github.com/lbryio/lbryschema.go v0.0.0-20190428231007-c54836bca002 h1:urfYK5ElpUrAv90auPLldoVC60LwiGAcY0OE6HJB9KI= github.com/lbryio/lbryschema.go v0.0.0-20190428231007-c54836bca002/go.mod h1:dAzPCBj3CKKWBGYBZxK6tKBP5SCgY2tqd9SnQd/OyKo= +github.com/lbryio/lbryschema.go v0.0.0-20190602173230-6d2f69a36f46 h1:LemfR+rMxhf7nnOrzy2HqS7Me7SZ5gEwOcNFzKC8ySQ= +github.com/lbryio/lbryschema.go v0.0.0-20190602173230-6d2f69a36f46/go.mod h1:dAzPCBj3CKKWBGYBZxK6tKBP5SCgY2tqd9SnQd/OyKo= github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04 h1:Nze+C2HbeKvhjI/kVn+9Poj/UuEW5sOQxcsxqO7L3GI= github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04/go.mod h1:fbG/dzobG8r95KzMwckXiLMHfFjZaBRQqC9hPs2XAQ4= github.com/lbryio/types v0.0.0-20190422033210-321fb2abda9c h1:m3O7561xBQ00lfUVayW4c6SnpVbUDQtPUwGcGYSUYQA= diff --git a/lbrycrd/channel.go b/lbrycrd/channel.go new file mode 100644 index 0000000..f8282a9 --- /dev/null +++ b/lbrycrd/channel.go @@ -0,0 +1,39 @@ +package lbrycrd + +import ( + "github.com/btcsuite/btcd/btcec" + "github.com/lbryio/lbry.go/extras/errors" + c "github.com/lbryio/lbryschema.go/claim" + pb "github.com/lbryio/types/v2/go" +) + +func NewChannel() (*c.ClaimHelper, *btcec.PrivateKey, error) { + claimChannel := new(pb.Claim_Channel) + channel := new(pb.Channel) + claimChannel.Channel = channel + + pbClaim := new(pb.Claim) + pbClaim.Type = claimChannel + + privateKey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + return nil, nil, errors.Err(err) + } + pubkeyBytes, err := c.PublicKeyToDER(privateKey.PubKey()) + if err != nil { + return nil, nil, errors.Err(err) + } + + helper := c.ClaimHelper{Claim: pbClaim} + helper.Version = c.NoSig + helper.GetChannel().PublicKey = pubkeyBytes + helper.Tags = []string{} + coverSrc := new(pb.Source) + helper.GetChannel().Cover = coverSrc + helper.Languages = []*pb.Language{} + thumbnailSrc := new(pb.Source) + helper.Thumbnail = thumbnailSrc + helper.Locations = []*pb.Location{} + + return &helper, privateKey, nil +} diff --git a/lbrycrd/claim.go b/lbrycrd/claim.go new file mode 100644 index 0000000..b63c19c --- /dev/null +++ b/lbrycrd/claim.go @@ -0,0 +1,86 @@ +package lbrycrd + +import ( + "encoding/hex" + + "github.com/lbryio/lbry.go/extras/errors" + c "github.com/lbryio/lbryschema.go/claim" + pb "github.com/lbryio/types/v2/go" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" +) + +func NewImageStreamClaim() (*c.ClaimHelper, error) { + streamClaim := new(pb.Claim_Stream) + stream := new(pb.Stream) + image := new(pb.Stream_Image) + image.Image = new(pb.Image) + stream.Type = image + + streamClaim.Stream = stream + + pbClaim := new(pb.Claim) + pbClaim.Type = streamClaim + + helper := c.ClaimHelper{Claim: pbClaim} + + return &helper, nil +} + +func NewVideoStreamClaim() (*c.ClaimHelper, error) { + streamClaim := new(pb.Claim_Stream) + stream := new(pb.Stream) + video := new(pb.Stream_Video) + video.Video = new(pb.Video) + stream.Type = video + streamClaim.Stream = stream + + pbClaim := new(pb.Claim) + pbClaim.Type = streamClaim + + helper := c.ClaimHelper{Claim: pbClaim} + + return &helper, nil +} + +func NewStreamClaim(title, description string) (*c.ClaimHelper, error) { + streamClaim := new(pb.Claim_Stream) + stream := new(pb.Stream) + streamClaim.Stream = stream + + pbClaim := new(pb.Claim) + pbClaim.Type = streamClaim + + helper := c.ClaimHelper{Claim: pbClaim} + helper.Title = title + helper.Description = description + + return &helper, nil +} + +func SignClaim(rawTx *wire.MsgTx, privKey btcec.PrivateKey, claim, channel *c.ClaimHelper, channelClaimID string) error { + claimIDHexBytes, err := hex.DecodeString(channelClaimID) + if err != nil { + return errors.Err(err) + } + claim.Version = c.WithSig + claim.ClaimID = rev(claimIDHexBytes) + hash, err := c.GetOutpointHash(rawTx.TxIn[0].PreviousOutPoint.Hash.String(), rawTx.TxIn[0].PreviousOutPoint.Index) + if err != nil { + return err + } + sig, err := c.Sign(privKey, *channel, *claim, hash) + if err != nil { + return err + } + + lbrySig, err := sig.LBRYSDKEncode() + if err != nil { + return err + } + claim.Signature = lbrySig + + return nil + +} diff --git a/lbrycrd/client.go b/lbrycrd/client.go index 1d77c21..c709440 100644 --- a/lbrycrd/client.go +++ b/lbrycrd/client.go @@ -1,15 +1,20 @@ package lbrycrd import ( + "encoding/hex" "net/url" "os" "strconv" "github.com/lbryio/lbry.go/extras/errors" + c "github.com/lbryio/lbryschema.go/claim" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/go-ini/ini" ) @@ -114,30 +119,6 @@ func (c *Client) SimpleSend(toAddress string, amount float64) (*chainhash.Hash, return hash, nil } -//func (c *Client) SendWithSplit(toAddress string, amount float64, numUTXOs int) (*chainhash.Hash, error) { -// decodedAddress, err := DecodeAddress(toAddress, &MainNetParams) -// if err != nil { -// return nil, errors.Wrap(err, 0) -// } -// -// amountPerAddress, err := btcutil.NewAmount(amount / float64(numUTXOs)) -// if err != nil { -// return nil, errors.Wrap(err, 0) -// } -// -// amounts := map[btcutil.Address]btcutil.Amount{} -// for i := 0; i < numUTXOs; i++ { -// addr := decodedAddress // to give it a new address, so -// amounts[addr] = amountPerAddress -// } -// -// hash, err := c.Client.SendManyMinConf("", amounts, 0) -// if err != nil && err.Error() == "-6: Insufficient funds" { -// err = errors.Wrap(errInsufficientFunds, 0) -// } -// return hash, errors.Wrap(err, 0) -//} - func getLbrycrdURLFromConfFile() (string, error) { if os.Getenv("HOME") == "" { return "", errors.Err("no $HOME var found") @@ -179,3 +160,130 @@ func getLbrycrdURLFromConfFile() (string, error) { return "rpc://" + userpass + host + ":" + port, nil } + +func (c *Client) CreateBaseRawTx(inputs []btcjson.TransactionInput, change float64) (*wire.MsgTx, error) { + addresses := make(map[btcutil.Address]btcutil.Amount) + changeAddress, err := c.GetNewAddress("") + if err != nil { + return nil, errors.Err(err) + } + changeAmount, err := btcutil.NewAmount(change) + if err != nil { + return nil, errors.Err(err) + } + addresses[changeAddress] = changeAmount + lockTime := int64(0) + return c.CreateRawTransaction(inputs, addresses, &lockTime) +} + +func (c *Client) GetEmptyTx(totalOutputSpend float64) (*wire.MsgTx, error) { + totalFees := 0.1 + unspentResults, err := c.ListUnspentMin(1) + if err != nil { + return nil, errors.Err(err) + } + finder := newOutputFinder(unspentResults) + + outputs, err := finder.nextBatch(totalOutputSpend + totalFees) + if err != nil { + return nil, err + } + if len(outputs) == 0 { + return nil, errors.Err("Not enough spendable outputs to create transaction") + } + inputs := make([]btcjson.TransactionInput, len(outputs)) + var totalInputSpend float64 + for i, output := range outputs { + inputs[i] = btcjson.TransactionInput{Txid: output.TxID, Vout: output.Vout} + totalInputSpend = totalInputSpend + output.Amount + } + + change := totalInputSpend - totalOutputSpend - totalFees + return c.CreateBaseRawTx(inputs, change) +} + +func (c *Client) SignTxAndSend(rawTx *wire.MsgTx) (*chainhash.Hash, error) { + signedTx, allInputsSigned, err := c.SignRawTransaction(rawTx) + if err != nil { + return nil, errors.Err(err) + } + if !allInputsSigned { + return nil, errors.Err("Not all inputs for the tx could be signed!") + } + + return c.SendRawTransaction(signedTx, false) +} + +type ScriptType int + +const ( + ClaimName ScriptType = iota + ClaimUpdate + ClaimSupport +) + +func (c *Client) AddStakeToTx(rawTx *wire.MsgTx, claim *c.ClaimHelper, name string, claimAmount float64, scriptType ScriptType) error { + + address, err := c.GetNewAddress("") + if err != nil { + return errors.Err(err) + } + amount, err := btcutil.NewAmount(claimAmount) + if err != nil { + return errors.Err(err) + } + + value, err := claim.CompileValue() + if err != nil { + return errors.Err(err) + } + var claimID string + if len(claim.ClaimID) > 0 { + claimID = hex.EncodeToString(rev(claim.ClaimID)) + } + var script []byte + switch scriptType { + case ClaimName: + script, err = getClaimNamePayoutScript(name, value, address) + if err != nil { + return errors.Err(err) + } + case ClaimUpdate: + script, err = getUpdateClaimPayoutScript(name, claimID, value, address) + if err != nil { + return errors.Err(err) + } + case ClaimSupport: + script, err = getUpdateClaimPayoutScript(name, claimID, value, address) + if err != nil { + return errors.Err(err) + } + } + + rawTx.AddTxOut(wire.NewTxOut(int64(amount), script)) + + return nil +} + +func (c *Client) CreateChannel(name string, amount float64) (*c.ClaimHelper, *btcec.PrivateKey, error) { + channel, key, err := NewChannel() + if err != nil { + return nil, nil, err + } + + rawTx, err := c.GetEmptyTx(amount) + if err != nil { + return nil, nil, err + } + err = c.AddStakeToTx(rawTx, channel, name, amount, ClaimName) + if err != nil { + return nil, nil, err + } + + _, err = c.SignTxAndSend(rawTx) + if err != nil { + return nil, nil, err + } + + return channel, key, nil +} diff --git a/lbrycrd/finder.go b/lbrycrd/finder.go new file mode 100644 index 0000000..771a70c --- /dev/null +++ b/lbrycrd/finder.go @@ -0,0 +1,38 @@ +package lbrycrd + +import ( + "github.com/lbryio/lbry.go/extras/errors" + + "github.com/btcsuite/btcd/btcjson" +) + +type outputFinder struct { + unspent []btcjson.ListUnspentResult + lastChecked int +} + +func newOutputFinder(unspentResults []btcjson.ListUnspentResult) *outputFinder { + return &outputFinder{unspent: unspentResults, lastChecked: -1} +} + +func (f *outputFinder) nextBatch(minAmount float64) ([]btcjson.ListUnspentResult, error) { + var batch []btcjson.ListUnspentResult + var lbcBatched float64 + for i, unspent := range f.unspent { + if i > f.lastChecked { + if unspent.Spendable { + batch = append(batch, unspent) + lbcBatched = lbcBatched + unspent.Amount + } + } + if lbcBatched >= minAmount { + f.lastChecked = i + break + } + if i == len(f.unspent)-1 { + return nil, errors.Err("Not enough unspent outputs to spend %d on supports.", minAmount) + } + } + + return batch, nil +} diff --git a/lbrycrd/script.go b/lbrycrd/script.go new file mode 100644 index 0000000..9d92872 --- /dev/null +++ b/lbrycrd/script.go @@ -0,0 +1,76 @@ +package lbrycrd + +import ( + "encoding/hex" + + "github.com/lbryio/lbry.go/extras/errors" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" +) + +func getClaimSupportPayoutScript(name, claimid string, address btcutil.Address) ([]byte, error) { + //OP_SUPPORT_CLAIM OP_2DROP OP_DROP OP_DUP OP_HASH160
OP_EQUALVERIFY OP_CHECKSIG + + pkscript, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, errors.Err(err) + } + + bytes, err := hex.DecodeString(claimid) + if err != nil { + return nil, errors.Err(err) + } + + return txscript.NewScriptBuilder(). + AddOp(txscript.OP_NOP7). //OP_SUPPORT_CLAIM + AddData([]byte(name)). // + AddData(rev(bytes)). // + AddOp(txscript.OP_2DROP). //OP_2DROP + AddOp(txscript.OP_DROP). //OP_DROP + AddOps(pkscript). //OP_DUP OP_HASH160
OP_EQUALVERIFY OP_CHECKSIG + Script() + +} + +func getClaimNamePayoutScript(name string, value []byte, address btcutil.Address) ([]byte, error) { + //OP_CLAIM_NAME OP_2DROP OP_DROP OP_DUP OP_HASH160
OP_EQUALVERIFY OP_CHECKSIG + + pkscript, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, errors.Err(err) + } + + return txscript.NewScriptBuilder(). + AddOp(txscript.OP_NOP6). //OP_CLAIMNAME + AddData([]byte(name)). // + AddData(value). // + AddOp(txscript.OP_2DROP). //OP_2DROP + AddOp(txscript.OP_DROP). //OP_DROP + AddOps(pkscript). //OP_DUP OP_HASH160
OP_EQUALVERIFY OP_CHECKSIG + Script() +} + +func getUpdateClaimPayoutScript(name, claimid string, value []byte, address btcutil.Address) ([]byte, error) { + //OP_UPDATE_CLAIM OP_2DROP OP_DROP OP_DUP OP_HASH160
OP_EQUALVERIFY OP_CHECKSIG + + pkscript, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, errors.Err(err) + } + + bytes, err := hex.DecodeString(claimid) + if err != nil { + return nil, errors.Err(err) + } + + return txscript.NewScriptBuilder(). + AddOp(txscript.OP_NOP8). //OP_UPDATE_CLAIM + AddData([]byte(name)). // + AddData(rev(bytes)). // + AddData(value). // + AddOp(txscript.OP_2DROP). //OP_2DROP + AddOp(txscript.OP_DROP). //OP_DROP + AddOps(pkscript). //OP_DUP OP_HASH160
OP_EQUALVERIFY OP_CHECKSIG + Script() +}