diff --git a/chain/lbry/address.go b/chain/lbry/address.go new file mode 100644 index 00000000..403f6208 --- /dev/null +++ b/chain/lbry/address.go @@ -0,0 +1,14 @@ +package lbry + +import "github.com/renproject/multichain/chain/bitcoin" + +type ( + // AddressEncoder re-exports bitcoin.AddressEncoder. + AddressEncoder = bitcoin.AddressEncoder + + // AddressDecoder re-exports bitcoin.AddressDecoder. + AddressDecoder = bitcoin.AddressDecoder + + // AddressEncodeDecoder re-exports bitcoin.AddressEncodeDecoder. + AddressEncodeDecoder = bitcoin.AddressEncodeDecoder +) diff --git a/chain/lbry/address_test.go b/chain/lbry/address_test.go new file mode 100644 index 00000000..bb67ec10 --- /dev/null +++ b/chain/lbry/address_test.go @@ -0,0 +1,84 @@ +package lbry_test + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/renproject/multichain" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/chain/bitcoin" + "github.com/renproject/multichain/chain/lbry" +) + +// These Base58Check fixtures encode the public hash 000102...1213. Testnet and +// regtest intentionally share address versions; they are not distinguishable by +// their legacy addresses. No key, node or funded account is used. +func TestLBRYAddressRoundTrip(t *testing.T) { + cases := []struct { + name string + params *chaincfg.Params + encoded string + version byte + }{ + {"mainnet-p2pkh", &lbry.MainNetParams, "bCjHuFv7ggRwqouv9M2Wp5nHbZdhKdPGhd", 85}, + {"mainnet-p2sh", &lbry.MainNetParams, "r6EcLGwmxMaN6r583sMJkirPtEAbfrdFcU", 122}, + {"testnet-p2pkh", &lbry.TestNetParams, "mfWyW5fc9NUj75YAnFgoRLrjxgLDn2MMth", 111}, + {"testnet-p2sh", &lbry.TestNetParams, "2MsFFCK16VhsCcvPXruztdzzcTZEQCbNKjJ", 196}, + {"regtest-p2pkh", &lbry.RegressionNetParams, "mfWyW5fc9NUj75YAnFgoRLrjxgLDn2MMth", 111}, + {"regtest-p2sh", &lbry.RegressionNetParams, "2MsFFCK16VhsCcvPXruztdzzcTZEQCbNKjJ", 196}, + } + payload := make([]byte, 20) + for i := range payload { + payload[i] = byte(i) + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var codec lbry.AddressEncodeDecoder = bitcoin.NewAddressEncodeDecoder(tc.params) + raw, err := codec.DecodeAddress(address.Address(tc.encoded)) + if err != nil { + t.Fatal(err) + } + if len(raw) != 25 || raw[0] != tc.version || !bytes.Equal(raw[1:21], payload) { + t.Fatalf("incorrect network or payload: %x", raw) + } + encoded, err := codec.EncodeAddress(raw) + if err != nil || encoded != address.Address(tc.encoded) { + t.Fatalf("round trip: got %q, %v", encoded, err) + } + }) + } +} + +func TestLBRYAddressRejectsMalformedAndWrongNetwork(t *testing.T) { + decoder := bitcoin.NewAddressDecoder(&lbry.MainNetParams) + for _, invalid := range []string{ + "", "bCjHuFv7ggRwqouv9M2Wp5nHbZdhKdPGh1", "not-an-address", "\u0100", + "mfWyW5fc9NUj75YAnFgoRLrjxgLDn2MMth", // LBRY testnet/regtest + "1BoatSLRHtKNngkdXEeobR76b53LETtpyT", // Bitcoin mainnet + } { + t.Run(invalid, func(t *testing.T) { + if _, err := decoder.DecodeAddress(address.Address(invalid)); err == nil { + t.Fatal("accepted invalid or wrong-network address") + } + }) + } +} + +func TestLBRYAddressChainMappings(t *testing.T) { + if multichain.LBC.OriginChain() != multichain.LBRY || multichain.LBRY.NativeAsset() != multichain.LBC { + t.Fatal("LBC and LBRY must map to each other") + } + if multichain.LBC.ChainType() != multichain.ChainTypeUTXOBased || !multichain.LBRY.IsUTXOBased() { + t.Fatal("LBRY must use UTXO semantics") + } + // The merge must preserve current upstream mappings alongside the addition. + for chain, asset := range map[multichain.Chain]multichain.Asset{ + multichain.Polygon: multichain.MATIC, multichain.Moonbeam: multichain.GLMR, + multichain.Kava: multichain.KAVA, multichain.Optimism: multichain.Asset("oETH"), + } { + if chain.NativeAsset() != asset || asset.OriginChain() != chain || !chain.IsAccountBased() { + t.Fatalf("lost upstream mapping for %s", chain) + } + } +} diff --git a/chain/lbry/gas.go b/chain/lbry/gas.go new file mode 100644 index 00000000..039b47b8 --- /dev/null +++ b/chain/lbry/gas.go @@ -0,0 +1,9 @@ +package lbry + +import "github.com/renproject/multichain/chain/bitcoin" + +// GasEstimator re-exports bitcoin.GasEstimator. +type GasEstimator = bitcoin.GasEstimator + +// NewGasEstimator re-exports bitcoin.NewGasEstimator. +var NewGasEstimator = bitcoin.NewGasEstimator diff --git a/chain/lbry/gas_test.go b/chain/lbry/gas_test.go new file mode 100644 index 00000000..afcdd2d2 --- /dev/null +++ b/chain/lbry/gas_test.go @@ -0,0 +1,52 @@ +package lbry_test + +import ( + "context" + + "github.com/renproject/multichain/chain/lbry" + "github.com/renproject/pack" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Gas", func() { + Context("when estimating LBRY network fee", func() { + It("should work", func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + client := lbry.NewClient(lbry.DefaultClientOptions()) + + // estimate fee to include tx within 1 block. + fallback1 := uint64(123) + gasEstimator1 := lbry.NewGasEstimator(client, 1, pack.NewU256FromUint64(fallback1)) + gasPrice1, _, err := gasEstimator1.EstimateGas(ctx) + if err != nil { + Expect(gasPrice1).To(Equal(pack.NewU256FromUint64(fallback1))) + } + + // estimate fee to include tx within 10 blocks. + fallback2 := uint64(234) + gasEstimator2 := lbry.NewGasEstimator(client, 10, pack.NewU256FromUint64(fallback2)) + gasPrice2, _, err := gasEstimator2.EstimateGas(ctx) + if err != nil { + Expect(gasPrice2).To(Equal(pack.NewU256FromUint64(fallback2))) + } + + // estimate fee to include tx within 100 blocks. + fallback3 := uint64(345) + gasEstimator3 := lbry.NewGasEstimator(client, 100, pack.NewU256FromUint64(fallback3)) + gasPrice3, _, err := gasEstimator3.EstimateGas(ctx) + if err != nil { + Expect(gasPrice3).To(Equal(pack.NewU256FromUint64(fallback3))) + } + + // expect fees in this order at the very least. + if err == nil { + Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue()) + Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue()) + } + }) + }) +}) diff --git a/chain/lbry/lbry.go b/chain/lbry/lbry.go new file mode 100644 index 00000000..ddc50dda --- /dev/null +++ b/chain/lbry/lbry.go @@ -0,0 +1,177 @@ +package lbry + +import ( + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +func init() { + if err := chaincfg.Register(&MainNetParams); err != nil { + panic(err) + } + if err := chaincfg.Register(&TestNetParams); err != nil { + panic(err) + } + if err := chaincfg.Register(&RegressionNetParams); err != nil { + panic(err) + } +} + +// genesisCoinbaseTx is the coinbase transaction for the genesis blocks for +// the main network, regression test network, and test network (version 3). +var genesisCoinbaseTx = wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0xffffffff, + }, + SignatureScript: []byte{ + 0x04, 0xff, 0xff, 0x00, 0x1d, 0x01, 0x04, 0x17, + 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x20, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, + }, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 0x12a05f200, + PkScript: []byte{ + 0x76, 0xa9, 0x14, 0x34, 0x59, 0x91, 0xdb, 0xf5, + 0x7b, 0xfb, 0x01, 0x4b, 0x87, 0x00, 0x6a, 0xcd, + 0xfa, 0xfb, 0xfc, 0x5f, 0xe8, 0x29, 0x2f, 0x88, + 0xac, + }, + }, + }, + LockTime: 0, +} + +// https://github.com/lbryio/lbrycrd/blob/master/src/chainparams.cpp#L19 +var genesisMerkleRoot = chainhash.Hash([chainhash.HashSize]byte{ // Make go vet happy. + 0xcc, 0x59, 0xe5, 0x9f, 0xf9, 0x7a, 0xc0, 0x92, + 0xb5, 0x5e, 0x42, 0x3a, 0xa5, 0x49, 0x51, 0x51, + 0xed, 0x6f, 0xb8, 0x05, 0x70, 0xa5, 0xbb, 0x78, + 0xcd, 0x5b, 0xd1, 0xc3, 0x82, 0x1c, 0x21, 0xb8, +}) + +var genesisBlock = wire.MsgBlock{ + Header: wire.BlockHeader{ + Version: 1, + PrevBlock: chainhash.Hash{}, // 0000000000000000000000000000000000000000000000000000000000000000 + MerkleRoot: genesisMerkleRoot, // b8211c82c3d15bcd78bba57005b86fed515149a53a425eb592c07af99fe559cc + Timestamp: time.Unix(1446058291, 0), // Wednesday, October 28, 2015 6:51:31 PM GMT + Bits: 0x1f00ffff, + Nonce: 1287, + }, + Transactions: []*wire.MsgTx{&genesisCoinbaseTx}, +} + +var genesisHash = chainhash.Hash([chainhash.HashSize]byte{ // Make go vet happy. + 0x63, 0xf4, 0x34, 0x6a, 0x4d, 0xb3, 0x4f, 0xdf, + 0xce, 0x29, 0xa7, 0x0f, 0x5e, 0x8d, 0x11, 0xf0, + 0x65, 0xf6, 0xb9, 0x16, 0x02, 0xb7, 0x03, 0x6c, + 0x7f, 0x22, 0xf3, 0xa0, 0x3b, 0x28, 0x89, 0x9c, +}) + +func newHashFromStr(hexStr string) *chainhash.Hash { + hash, err := chainhash.NewHashFromStr(hexStr) + if err != nil { + panic(err) + } + return hash +} + +// MainNetParams returns the chain configuration for mainnet. +var MainNetParams = chaincfg.Params{ + Name: "mainnet", + Net: 0xfae4aaf1, + DefaultPort: "9246", + + // Chain parameters + GenesisBlock: &genesisBlock, + GenesisHash: &genesisHash, + + // Address encoding magics + PubKeyHashAddrID: 85, + ScriptHashAddrID: 122, + PrivateKeyID: 28, + WitnessPubKeyHashAddrID: 0x06, // starts with p2 + WitnessScriptHashAddrID: 0x0A, // starts with 7Xh + BIP0034Height: 1, + BIP0065Height: 200000, + BIP0066Height: 200000, + + // BIP32 hierarchical deterministic extended key magics + HDPrivateKeyID: [4]byte{0x04, 0x88, 0xad, 0xe4}, // starts with xprv + HDPublicKeyID: [4]byte{0x04, 0x88, 0xb2, 0x1e}, // starts with xpub + + // Human-readable part for Bech32 encoded segwit addresses, as defined in + // BIP 173. + Bech32HRPSegwit: "lbc", + + // BIP44 coin type used in the hierarchical deterministic path for + // address generation. + HDCoinType: 0x8c, +} + +// TestNetParams returns the chain configuration for testnet. +var TestNetParams = chaincfg.Params{ + Name: "testnet", + Net: 0xfae4aae1, + DefaultPort: "19246", + + // Chain parameters + GenesisBlock: &genesisBlock, + GenesisHash: &genesisHash, + + // Address encoding magics + PubKeyHashAddrID: 111, + ScriptHashAddrID: 196, + PrivateKeyID: 239, + + // BIP32 hierarchical deterministic extended key magics + HDPrivateKeyID: [4]byte{0x04, 0x35, 0x83, 0x94}, // starts with xprv + HDPublicKeyID: [4]byte{0x04, 0x35, 0x87, 0xcf}, // starts with xpub + + // Human-readable part for Bech32 encoded segwit addresses, as defined in + // BIP 173. + Bech32HRPSegwit: "tlbc", + + // BIP44 coin type used in the hierarchical deterministic path for + // address generation. + HDCoinType: 0x8c, +} + +// RegressionNetParams returns the chain configuration for regression net. +var RegressionNetParams = chaincfg.Params{ + Name: "regtest", + + // LBRY has 0xfae4aad1 as RegTest (same as Bitcoin's RegTest). + // Setting it to an arbitrary value (leet_hex(LBRY)), so that we can + // register the regtest network. + Net: 0xfae4aad1, + + // Address encoding magics + PubKeyHashAddrID: 111, + ScriptHashAddrID: 196, + PrivateKeyID: 239, + + // BIP32 hierarchical deterministic extended key magics + HDPrivateKeyID: [4]byte{0x04, 0x35, 0x83, 0x94}, // starts with xprv + HDPublicKeyID: [4]byte{0x04, 0x35, 0x87, 0xcf}, // starts with xpub + + // Human-readable part for Bech32 encoded segwit addresses, as defined in + // BIP 173. + Bech32HRPSegwit: "rlbc", + + // BIP44 coin type used in the hierarchical deterministic path for + // address generation. + HDCoinType: 0x8c, +} diff --git a/chain/lbry/lbry_suite_test.go b/chain/lbry/lbry_suite_test.go new file mode 100644 index 00000000..82c8fddd --- /dev/null +++ b/chain/lbry/lbry_suite_test.go @@ -0,0 +1,13 @@ +package lbry_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestLBRY(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "LBRY Suite") +} diff --git a/chain/lbry/lbry_test.go b/chain/lbry/lbry_test.go new file mode 100644 index 00000000..0da67760 --- /dev/null +++ b/chain/lbry/lbry_test.go @@ -0,0 +1,146 @@ +package lbry_test + +import ( + "context" + "log" + "os" + "reflect" + "time" + + "github.com/btcsuite/btcutil" + "github.com/renproject/id" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/utxo" + "github.com/renproject/multichain/chain/lbry" + "github.com/renproject/pack" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("LBRY", func() { + Context("when submitting transactions", func() { + Context("when sending LBC to multiple addresses", func() { + It("should work", func() { + // Load private key, and assume that the associated address has + // funds to spend. You can do this by setting LBRY_PK to the + // value specified in the `./multichaindeploy/.env` file. + pkEnv := os.Getenv("LBRY_PK") + if pkEnv == "" { + panic("LBRY_PK is undefined") + } + wif, err := btcutil.DecodeWIF(pkEnv) + Expect(err).ToNot(HaveOccurred()) + + // PKH + pkhAddr, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(wif.PrivKey.PubKey().SerializeCompressed()), &lbry.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + pkhAddrUncompressed, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(wif.PrivKey.PubKey().SerializeUncompressed()), &lbry.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + log.Printf("PKH %v", pkhAddr.EncodeAddress()) + log.Printf("PKH (uncompressed) %v", pkhAddrUncompressed.EncodeAddress()) + + // WPKH + wpkAddr, err := btcutil.NewAddressWitnessPubKeyHash([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19}, &lbry.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + log.Printf("WPKH %v", wpkAddr.EncodeAddress()) + + // Setup the client and load the unspent transaction outputs. + client := lbry.NewClient(lbry.DefaultClientOptions()) + outputs, err := client.UnspentOutputs(context.Background(), 0, 999999999, address.Address(pkhAddr.EncodeAddress())) + Expect(err).ToNot(HaveOccurred()) + Expect(len(outputs)).To(BeNumerically(">", 0)) + output := outputs[0] + + // Check that we can load the output and that it is equal. + // Otherwise, something strange is happening with the RPC + // client. + output2, _, err := client.Output(context.Background(), output.Outpoint) + Expect(err).ToNot(HaveOccurred()) + Expect(reflect.DeepEqual(output, output2)).To(BeTrue()) + output2, _, err = client.UnspentOutput(context.Background(), output.Outpoint) + Expect(err).ToNot(HaveOccurred()) + Expect(reflect.DeepEqual(output, output2)).To(BeTrue()) + + // Build the transaction by consuming the outputs and spending + // them to a set of recipients. + inputs := []utxo.Input{ + {Output: utxo.Output{ + Outpoint: utxo.Outpoint{ + Hash: output.Outpoint.Hash[:], + Index: output.Outpoint.Index, + }, + PubKeyScript: output.PubKeyScript, + Value: output.Value, + }}, + } + recipients := []utxo.Recipient{ + { + To: address.Address(pkhAddr.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 1000) / 3)), + }, + { + To: address.Address(pkhAddrUncompressed.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 1000) / 3)), + }, + { + To: address.Address(wpkAddr.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 1000) / 3)), + }, + } + tx, err := lbry.NewTxBuilder(&lbry.RegressionNetParams).BuildTx(inputs, recipients) + Expect(err).ToNot(HaveOccurred()) + + // Get the digests that need signing from the transaction, and + // sign them. In production, this would be done using the RZL + // MPC algorithm, but for the purposes of this test, using an + // explicit privkey is ok. + sighashes, err := tx.Sighashes() + signatures := make([]pack.Bytes65, len(sighashes)) + Expect(err).ToNot(HaveOccurred()) + for i := range sighashes { + hash := id.Hash(sighashes[i]) + privKey := (*id.PrivKey)(wif.PrivKey) + signature, err := privKey.Sign(&hash) + Expect(err).ToNot(HaveOccurred()) + signatures[i] = pack.NewBytes65(signature) + } + Expect(tx.Sign(signatures, pack.NewBytes(wif.SerializePubKey()))).To(Succeed()) + + // Submit the transaction to the lbry node. Again, this + // should be running a la `./multichaindeploy`. + txHash, err := tx.Hash() + Expect(err).ToNot(HaveOccurred()) + err = client.SubmitTx(context.Background(), tx) + Expect(err).ToNot(HaveOccurred()) + log.Printf("TXID %v", txHash) + + for { + // Loop until the transaction has at least a few + // confirmations. This implies that the transaction is + // definitely valid, and the test has passed. We were + // successfully able to use the multichain to construct and + // submit a LBRY transaction! + confs, err := client.Confirmations(context.Background(), txHash) + Expect(err).ToNot(HaveOccurred()) + log.Printf(" %v/3 confirmations", confs) + if confs >= 1 { + break + } + time.Sleep(10 * time.Second) + } + ctxWithTimeout, cancelCtxWithTimeout := context.WithTimeout(context.Background(), time.Second) + defer cancelCtxWithTimeout() + _, _, err = client.UnspentOutput(ctxWithTimeout, output.Outpoint) + Expect(err).To(HaveOccurred()) + + // Check that we can load the output and that it is equal. + // Otherwise, something strange is happening with the RPC + // client. + output2, _, err = client.Output(context.Background(), output.Outpoint) + Expect(err).ToNot(HaveOccurred()) + Expect(reflect.DeepEqual(output, output2)).To(BeTrue()) + }) + }) + }) +}) diff --git a/chain/lbry/utxo.go b/chain/lbry/utxo.go new file mode 100644 index 00000000..9a8be83c --- /dev/null +++ b/chain/lbry/utxo.go @@ -0,0 +1,32 @@ +package lbry + +import "github.com/renproject/multichain/chain/bitcoin" + +type ( + // Tx re-exports bitcoin.Tx. + Tx = bitcoin.Tx + + // TxBuilder re-exports bitcoin.TxBuilder. + TxBuilder = bitcoin.TxBuilder + + // Client re-exports bitcoin.Client. + Client = bitcoin.Client + + // ClientOptions re-exports bitcoin.ClientOptions. + ClientOptions = bitcoin.ClientOptions +) + +var ( + // NewTxBuilder re-exports bitcoin.NewTxBuilder. + NewTxBuilder = bitcoin.NewTxBuilder + + // NewClient re-exports bitcoin.NewClient. + NewClient = bitcoin.NewClient +) + +// DefaultClientOptions returns ClientOptions with the default settings. These +// settings are valid for use with the default local deployment of the +// multichain. In production, the host, user, and password should be changed. +func DefaultClientOptions() ClientOptions { + return bitcoin.DefaultClientOptions().WithHost("http://0.0.0.0:29245") +} diff --git a/chain/lbry/utxo_test.go b/chain/lbry/utxo_test.go new file mode 100644 index 00000000..491a886e --- /dev/null +++ b/chain/lbry/utxo_test.go @@ -0,0 +1,148 @@ +package lbry_test + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/utxo" + "github.com/renproject/multichain/chain/lbry" + "github.com/renproject/pack" +) + +func publicLBRYFixture(t *testing.T) (*btcec.PrivateKey, []utxo.Input, []utxo.Recipient) { + t.Helper() + // Public test scalar 1; never a real wallet or a funded output. + key, _ := btcec.PrivKeyFromBytes(btcec.S256(), []byte{1}) + owner, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(key.PubKey().SerializeCompressed()), &lbry.MainNetParams) + if err != nil { + t.Fatal(err) + } + script, err := txscript.PayToAddrScript(owner) + if err != nil { + t.Fatal(err) + } + inputs := []utxo.Input{{Output: utxo.Output{ + Outpoint: utxo.Outpoint{Hash: pack.NewBytes(bytes.Repeat([]byte{0x42}, 32)), Index: pack.NewU32(3)}, + Value: pack.NewU256FromUint64(100000), PubKeyScript: pack.NewBytes(script), + }}} + recipients := []utxo.Recipient{ + {To: address.Address("bCjHuFv7ggRwqouv9M2Wp5nHbZdhKdPGhd"), Value: pack.NewU256FromUint64(70000)}, + {To: address.Address(owner.EncodeAddress()), Value: pack.NewU256FromUint64(29000)}, + } + return key, inputs, recipients +} + +func TestLBRYTxPreservesOutputsAndFee(t *testing.T) { + _, inputs, recipients := publicLBRYFixture(t) + tx, err := lbry.NewTxBuilder(&lbry.MainNetParams).BuildTx(inputs, recipients) + if err != nil { + t.Fatal(err) + } + serialized, err := tx.Serialize() + if err != nil { + t.Fatal(err) + } + var decoded wire.MsgTx + if err := decoded.Deserialize(bytes.NewReader(serialized)); err != nil { + t.Fatal(err) + } + if decoded.Version != 2 || len(decoded.TxIn) != 1 || len(decoded.TxOut) != 2 { + t.Fatalf("unexpected transaction structure: %+v", decoded) + } + if decoded.TxIn[0].PreviousOutPoint.Index != 3 || !bytes.Equal(decoded.TxIn[0].PreviousOutPoint.Hash[:], inputs[0].Hash) { + t.Fatal("outpoint changed") + } + var total int64 + for i, recipient := range recipients { + addr, err := btcutil.DecodeAddress(string(recipient.To), &lbry.MainNetParams) + if err != nil { + t.Fatal(err) + } + expectedScript, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatal(err) + } + if decoded.TxOut[i].Value != recipient.Value.Int().Int64() || !bytes.Equal(decoded.TxOut[i].PkScript, expectedScript) { + t.Fatalf("recipient %d amount or script changed", i) + } + total += decoded.TxOut[i].Value + } + if fee := inputs[0].Value.Int().Int64() - total; fee != 1000 { + t.Fatalf("fee: got %d, want 1000", fee) + } + outputs, err := tx.Outputs() + if err != nil || len(outputs) != 2 { + t.Fatalf("outputs: %v", err) + } + hash := decoded.TxHash() + for i, output := range outputs { + if !bytes.Equal(output.Hash, hash[:]) || output.Index.Uint32() != uint32(i) || output.Value.Int().Int64() != decoded.TxOut[i].Value { + t.Fatalf("output %d no longer matches serialized transaction", i) + } + } +} + +func TestLBRYTxSignaturesValidateAndBindOutputs(t *testing.T) { + key, inputs, recipients := publicLBRYFixture(t) + tx, err := lbry.NewTxBuilder(&lbry.MainNetParams).BuildTx(inputs, recipients) + if err != nil { + t.Fatal(err) + } + if err := tx.Sign(nil, pack.NewBytes(key.PubKey().SerializeCompressed())); err == nil { + t.Fatal("accepted missing input signature") + } + hashes, err := tx.Sighashes() + if err != nil || len(hashes) != 1 { + t.Fatalf("sighashes: %v", err) + } + signature, err := key.Sign(hashes[0][:]) + if err != nil { + t.Fatal(err) + } + var rsv pack.Bytes65 + r, s := signature.R.Bytes(), signature.S.Bytes() + copy(rsv[32-len(r):32], r) + copy(rsv[64-len(s):64], s) + pubkey := pack.NewBytes(key.PubKey().SerializeCompressed()) + if err := tx.Sign([]pack.Bytes65{rsv}, pubkey); err != nil { + t.Fatal(err) + } + if err := tx.Sign([]pack.Bytes65{rsv}, pubkey); err == nil { + t.Fatal("signed the same transaction twice") + } + serialized, err := tx.Serialize() + if err != nil { + t.Fatal(err) + } + var decoded wire.MsgTx + if err := decoded.Deserialize(bytes.NewReader(serialized)); err != nil { + t.Fatal(err) + } + check := func(tx *wire.MsgTx) error { + engine, err := txscript.NewEngine(inputs[0].PubKeyScript, tx, 0, txscript.StandardVerifyFlags, nil, nil, inputs[0].Value.Int().Int64()) + if err != nil { + return err + } + return engine.Execute() + } + if err := check(&decoded); err != nil { + t.Fatalf("serialized P2PKH signature failed script validation: %v", err) + } + decoded.TxOut[0].Value++ + if err := check(&decoded); err == nil { + t.Fatal("signature accepted an altered payment amount") + } +} + +func TestLBRYTxRejectsWrongNetworkRecipient(t *testing.T) { + _, inputs, recipients := publicLBRYFixture(t) + recipients[0].To = "mfWyW5fc9NUj75YAnFgoRLrjxgLDn2MMth" + if _, err := lbry.NewTxBuilder(&lbry.MainNetParams).BuildTx(inputs, recipients); err == nil { + t.Fatal("mainnet transaction accepted a testnet recipient") + } +} diff --git a/infra/.env b/infra/.env index 03a63a98..acb06fc1 100644 --- a/infra/.env +++ b/infra/.env @@ -64,6 +64,16 @@ export FANTOM_ADDRESS=0x239fA7623354eC26520dE878B52f13Fe84b06971 export FILECOIN_PK=7b2254797065223a22736563703235366b31222c22507269766174654b6579223a22756d6a634e436a487a5438455757485849754a4c4b58745035437153323435666238626c656c756e5448493d227d export FILECOIN_ADDRESS=f1ej2tountzqwnu6uswhqdzvw6yy5xvcig6rxl2qa +# +# LBRY +# + +# Address that will receive mining rewards. Generally, this is set to an address +# for which the private key is known by a test suite. This allows the test suite +# access to plenty of testing funds. +export LBRY_PK=cV9WB2d7VTCJkkJy5RrzkQvqtiFJvAXbL8XUte8YRBH9zG2u1LTu +export LBRY_ADDRESS=mufonb3XbgFWH6ayP84sz81x6rS9oCxvqR + # # Terra # diff --git a/infra/docker-compose.yaml b/infra/docker-compose.yaml index 7d72401b..7665270a 100644 --- a/infra/docker-compose.yaml +++ b/infra/docker-compose.yaml @@ -104,6 +104,18 @@ services: entrypoint: - "/root/run.sh" + # + # LBRY + # + lbry: + build: + context: ./lbry + ports: + - "0.0.0.0:29245:29245" + entrypoint: + - "./root/run.sh" + - "${LBRY_ADDRESS}" + # # Zcash # diff --git a/infra/lbry/Dockerfile b/infra/lbry/Dockerfile new file mode 100644 index 00000000..e3790fc7 --- /dev/null +++ b/infra/lbry/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:xenial + +RUN apt-get update --fix-missing && apt-get install --yes software-properties-common wget unzip + +RUN wget -c https://github.com/lbryio/lbrycrd/releases/download/v0.17.4.6/lbrycrd-linux-1746.zip -O tmp.zip && unzip tmp.zip -d lbrycrd && rm tmp.zip +RUN mv ./lbrycrd /app +RUN chmod +x /app/lbrycrdd +RUN chmod +x /app/lbrycrd-cli + +COPY lbrycrd.conf /root/.lbrycrd/lbrycrd.conf +COPY run.sh /root/run.sh +RUN chmod +x /root/run.sh + +EXPOSE 29245 + +ENTRYPOINT ["./root/run.sh"] diff --git a/infra/lbry/README.md b/infra/lbry/README.md new file mode 100644 index 00000000..bebce882 --- /dev/null +++ b/infra/lbry/README.md @@ -0,0 +1,22 @@ +# README + +## Generate a keypair (privatekey + address) + +```bash +$ go run keygen.go +LBRY_PK=cV9WB2d7VTCJkkJy5RrzkQvqtiFJvAXbL8XUte8YRBH9zG2u1LTu +LBRY_ADDRESS=n3qZBVhiMHW6vxizTdpnBRpvHXHqer3Q2x +``` + +## Build your docker container + +```bash +docker build . +``` + +## Run the container + +```bash +# Regtest +docker run -p 29245:29245 lbrycrd:latest "n3qZBVhiMHW6vxizTdpnBRpvHXHqer3Q2x" +``` diff --git a/infra/lbry/keygen.go b/infra/lbry/keygen.go new file mode 100644 index 00000000..a9e04dcf --- /dev/null +++ b/infra/lbry/keygen.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcutil" + "github.com/renproject/id" + "github.com/renproject/multichain/chain/lbry" +) + +func main() { + privKey := id.NewPrivKey() + wif, err := btcutil.NewWIF((*btcec.PrivateKey)(privKey), &lbry.RegressionNetParams, true) + if err != nil { + panic(err) + } + addrPubKeyHash, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(wif.SerializePubKey()), &lbry.RegressionNetParams) + if err != nil { + panic(err) + } + fmt.Printf("LBRY_PK=%v\n", wif) + fmt.Printf("LBRY_ADDRESS=%v\n", addrPubKeyHash) +} diff --git a/infra/lbry/lbrycrd.conf b/infra/lbry/lbrycrd.conf new file mode 100644 index 00000000..b04d1351 --- /dev/null +++ b/infra/lbry/lbrycrd.conf @@ -0,0 +1,10 @@ +daemon=1 +regtest=1 +rpcuser=user +rpcpassword=password +rpcallowip=0.0.0.0/0 +server=1 +txindex=1 + +[regtest] +rpcbind=0.0.0.0 diff --git a/infra/lbry/run.sh b/infra/lbry/run.sh new file mode 100644 index 00000000..6aa8f7b2 --- /dev/null +++ b/infra/lbry/run.sh @@ -0,0 +1,22 @@ +#!/bin/bash +ADDRESS=$1 + +# Start +/app/lbrycrdd -conf=/root/.lbrycrd/lbrycrd.conf # -server -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcuser=user -rpcpassword=password +sleep 10 + +# Print setup +echo "LBRY_ADDRESS=$ADDRESS" + +# Import the address +/app/lbrycrd-cli importaddress $ADDRESS + +# Generate enough block to pass the maturation time +/app/lbrycrd-cli generatetoaddress 101 $ADDRESS + +# Simulate mining +while : +do + /app/lbrycrd-cli generatetoaddress 1 $ADDRESS + sleep 10 +done diff --git a/multichain.go b/multichain.go index 95d1e1ee..d1e2b1a9 100644 --- a/multichain.go +++ b/multichain.go @@ -113,6 +113,7 @@ const ( GETH = Asset("gETH") // Goerli Ether GLMR = Asset("GLMR") // Glimmer KAVA = Asset("KAVA") // Kava + LBC = Asset("LBC") // LBRY LUNA = Asset("LUNA") // Luna MATIC = Asset("MATIC") // Matic PoS (Polygon) oETH = Asset("oETH") // Optimism Ether @@ -222,6 +223,8 @@ func (asset Asset) OriginChain() Chain { return Moonbeam case KAVA: return Kava + case LBC: + return LBRY case LUNA: return Terra case MATIC: @@ -261,7 +264,7 @@ func (asset Asset) OriginChain() Chain { // ChainType returns the chain-type (Account or UTXO) for the given asset func (asset Asset) ChainType() ChainType { switch asset { - case BCH, BTC, DGB, DOGE, ZEC: + case BCH, BTC, DGB, DOGE, LBC, ZEC: return ChainTypeUTXOBased case ArbETH, AVAX, BNB, CAT, ETH, FIL, FTM, GETH, GLMR, KAVA, LUNA, MATIC, oETH, SOL: @@ -354,6 +357,7 @@ const ( Fantom = Chain("Fantom") Filecoin = Chain("Filecoin") Kava = Chain("Kava") + LBRY = Chain("LBRY") Moonbeam = Chain("Moonbeam") Optimism = Chain("Optimism") Polygon = Chain("Polygon") @@ -394,7 +398,7 @@ func (chain *Chain) Unmarshal(buf []byte, rem int) ([]byte, int, error) { // for the chain. func (chain Chain) ChainType() ChainType { switch chain { - case Bitcoin, BitcoinCash, DigiByte, Dogecoin, Zcash: + case Bitcoin, BitcoinCash, DigiByte, Dogecoin, LBRY, Zcash: return ChainTypeUTXOBased case Avalanche, BinanceSmartChain, Catalog, Ethereum, Arbitrum, Fantom, Filecoin, Kava, Moonbeam, Optimism, Polygon, Solana, Terra: @@ -458,6 +462,8 @@ func (chain Chain) NativeAsset() Asset { return GETH case Kava: return KAVA + case LBRY: + return LBC case Moonbeam: return GLMR case Polygon: diff --git a/multichain_test.go b/multichain_test.go index f4730f31..7e17c846 100644 --- a/multichain_test.go +++ b/multichain_test.go @@ -41,6 +41,7 @@ import ( "github.com/renproject/multichain/chain/ethereum" "github.com/renproject/multichain/chain/fantom" "github.com/renproject/multichain/chain/filecoin" + "github.com/renproject/multichain/chain/lbry" "github.com/renproject/multichain/chain/polygon" "github.com/renproject/multichain/chain/terra" "github.com/renproject/multichain/chain/zcash" @@ -64,6 +65,7 @@ var ( testAVAX = flag.Bool("avax", false, "Pass this flag to test Avalanche") testBSC = flag.Bool("bsc", false, "Pass this flag to test Binance Smart Chain") testFTM = flag.Bool("ftm", false, "Pass this flag to test Fantom") + testLBC = flag.Bool("lbc", false, "Pass this flag to test LBRY") testLUNA = flag.Bool("luna", false, "Pass this flag to test Terra") testZEC = flag.Bool("zec", false, "Pass this flag to test Zcash") ) @@ -92,6 +94,7 @@ var _ = Describe("Multichain", func() { testFlags[multichain.Polygon] = *testMATIC testFlags[multichain.Avalanche] = *testAVAX testFlags[multichain.Fantom] = *testFTM + testFlags[multichain.LBRY] = *testLBC testFlags[multichain.Terra] = *testLUNA testFlags[multichain.Zcash] = *testZEC @@ -1062,6 +1065,26 @@ var _ = Describe("Multichain", func() { dogecoin.NewTxBuilder(&dogecoin.RegressionNetParams), multichain.Dogecoin, }, + { + "LBRY_PK", + func(pkh []byte) (btcutil.Address, error) { + addr, err := btcutil.NewAddressPubKeyHash(pkh, &lbry.RegressionNetParams) + return addr, err + }, + func(script []byte) (btcutil.Address, error) { + addr, err := btcutil.NewAddressScriptHash(script, &lbry.RegressionNetParams) + return addr, err + }, + pack.NewString("http://0.0.0.0:29245"), + func(rpcURL pack.String, pkhAddr btcutil.Address) (multichain.UTXOClient, []multichain.UTXOutput, func(context.Context, pack.Bytes) (int64, error)) { + client := lbry.NewClient(lbry.DefaultClientOptions()) + outputs, err := client.UnspentOutputs(ctx, 0, 999999999, multichain.Address(pkhAddr.EncodeAddress())) + Expect(err).NotTo(HaveOccurred()) + return client, outputs, client.Confirmations + }, + lbry.NewTxBuilder(&lbry.RegressionNetParams), + multichain.LBRY, + }, { "ZCASH_PK", func(pkh []byte) (btcutil.Address, error) {