From d5c80db8f0b76d2615f97a5719b8e32026a7dc42 Mon Sep 17 00:00:00 2001 From: Martin Boehm Date: Wed, 5 Dec 2018 20:52:34 +0100 Subject: [PATCH] Add experimental Liquid support --- bchain/coins/blockchain.go | 1 + bchain/coins/liquid/liquidparser.go | 49 +++++ bchain/coins/liquid/liquidparser_test.go | 254 +++++++++++++++++++++++ bchain/coins/liquid/liquidrpc.go | 57 +++++ configs/coins/liquid.json | 65 ++++++ 5 files changed, 426 insertions(+) create mode 100644 bchain/coins/liquid/liquidparser.go create mode 100644 bchain/coins/liquid/liquidparser_test.go create mode 100644 bchain/coins/liquid/liquidrpc.go create mode 100644 configs/coins/liquid.json diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index 680ae27a..b3a0affe 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -57,6 +57,7 @@ func init() { BlockChainFactories["Monacoin Testnet"] = monacoin.NewMonacoinRPC BlockChainFactories["DigiByte"] = digibyte.NewDigiByteRPC BlockChainFactories["Myriad"] = myriad.NewMyriadRPC + BlockChainFactories["Liquid"] = liquid.NewLiquidRPC BlockChainFactories["Groestlcoin"] = grs.NewGroestlcoinRPC BlockChainFactories["Groestlcoin Testnet"] = grs.NewGroestlcoinRPC } diff --git a/bchain/coins/liquid/liquidparser.go b/bchain/coins/liquid/liquidparser.go new file mode 100644 index 00000000..80e111de --- /dev/null +++ b/bchain/coins/liquid/liquidparser.go @@ -0,0 +1,49 @@ +package liquid + +import ( + "blockbook/bchain/coins/btc" + + "github.com/btcsuite/btcd/wire" + "github.com/jakm/btcutil/chaincfg" +) + +const ( + MainnetMagic wire.BitcoinNet = 0xdab5bffa +) + +var ( + MainNetParams chaincfg.Params +) + +func init() { + MainNetParams = chaincfg.MainNetParams + MainNetParams.Net = MainnetMagic + MainNetParams.PubKeyHashAddrID = []byte{57} + MainNetParams.ScriptHashAddrID = []byte{39} + // BLINDED_ADDRESS 12 +} + +// LiquidParser handle +type LiquidParser struct { + *btc.BitcoinParser +} + +// NewLiquidParser returns new LiquidParser instance +func NewLiquidParser(params *chaincfg.Params, c *btc.Configuration) *LiquidParser { + return &LiquidParser{BitcoinParser: btc.NewBitcoinParser(params, c)} +} + +// GetChainParams contains network parameters for the main GameCredits network, +// and the test GameCredits network +func GetChainParams(chain string) *chaincfg.Params { + if !chaincfg.IsRegistered(&MainNetParams) { + err := chaincfg.Register(&MainNetParams) + if err != nil { + panic(err) + } + } + switch chain { + default: + return &MainNetParams + } +} diff --git a/bchain/coins/liquid/liquidparser_test.go b/bchain/coins/liquid/liquidparser_test.go new file mode 100644 index 00000000..602bcca6 --- /dev/null +++ b/bchain/coins/liquid/liquidparser_test.go @@ -0,0 +1,254 @@ +// build unittest + +package liquid + +import ( + "blockbook/bchain/coins/btc" + "encoding/hex" + "os" + "reflect" + "testing" + + "github.com/jakm/btcutil/chaincfg" +) + +func TestMain(m *testing.M) { + c := m.Run() + chaincfg.ResetParams() + os.Exit(c) +} + +func Test_GetAddrDescFromAddress(t *testing.T) { + type args struct { + address string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "P2PKH1", + args: args{address: "QHU1yszeZwVeuJosGJ4JDHuKaLRWmdEYDF"}, + want: "76a914dd95db91e8f914cbd63bae8e307d54399f060cd688ac", + wantErr: false, + }, + { + name: "P2PKH2", + args: args{address: "PwogNb9zqhrwPneDqVp18GqcVuygsCvXkU"}, + want: "76a91405eb4afe4615751cfb813a00846a8d9ef8a9a2e588ac", + wantErr: false, + }, + { + name: "P2SH1", + args: args{address: "GhWTZqLPHRK8KfuT6yo1wGisQzn4cXrbPP"}, + want: "a9140394b3cf9a44782c10105b93962daa8dba304d7f87", + wantErr: false, + }, + } + parser := NewLiquidParser(GetChainParams("main"), &btc.Configuration{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parser.GetAddrDescFromAddress(tt.args.address) + if (err != nil) != tt.wantErr { + t.Errorf("GetAddrDescFromAddress() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("GetAddrDescFromAddress() = %v, want %v", h, tt.want) + } + }) + } +} + +func Test_GetAddressesFromAddrDesc(t *testing.T) { + type args struct { + script string + } + tests := []struct { + name string + args args + want []string + want2 bool + wantErr bool + }{ + { + name: "P2PKH1", + args: args{script: "76a914dd95db91e8f914cbd63bae8e307d54399f060cd688ac"}, + want: []string{"QHU1yszeZwVeuJosGJ4JDHuKaLRWmdEYDF"}, + want2: true, + wantErr: false, + }, + { + name: "P2PKH2", + args: args{script: "76a91405eb4afe4615751cfb813a00846a8d9ef8a9a2e588ac"}, + want: []string{"PwogNb9zqhrwPneDqVp18GqcVuygsCvXkU"}, + want2: true, + wantErr: false, + }, + { + name: "P2SH", + args: args{script: "a9140394b3cf9a44782c10105b93962daa8dba304d7f87"}, + want: []string{"GhWTZqLPHRK8KfuT6yo1wGisQzn4cXrbPP"}, + want2: true, + wantErr: false, + }, + { + name: "P2PK compressed", + args: args{script: "21020e46e79a2a8d12b9b5d12c7a91adb4e454edfae43c0a0cb805427d2ac7613fd9ac"}, + want: []string{"QKKEbCNAV7BYdtfSb3cQcJ7QSmFMvtXETz"}, + want2: false, + wantErr: false, + }, + { + name: "P2PK uncompressed", + args: args{script: "41041057356b91bfd3efeff5fc0fa8b865faafafb67bd653c5da2cd16ce15c7b86db0e622c8e1e135f68918a23601eb49208c1ac72c7b64a4ee99c396cf788da16ccac"}, + want: []string{"QDoUiWY7iZXDrkBzXdk6dru8DbvGqExXuf"}, + want2: false, + wantErr: false, + }, + } + + parser := NewLiquidParser(GetChainParams("main"), &btc.Configuration{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, _ := hex.DecodeString(tt.args.script) + got, got2, err := parser.GetAddressesFromAddrDesc(b) + if (err != nil) != tt.wantErr { + t.Errorf("GetAddressesFromAddrDesc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAddressesFromAddrDesc() = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got2, tt.want2) { + t.Errorf("GetAddressesFromAddrDesc() = %v, want %v", got2, tt.want2) + } + }) + } +} + +/* +var ( + testTx1 bchain.Tx + + testTxPacked1 = "002151148bbcaa8406010000000123c41ad26dd5782635638effbc9e31c9b4a3b757591a52c83d2770ad82b33e93000000006b483045022100a20302bde6d2fb194bb9c0a8d7beb52ed0b5b72b912da75364efe169d5b74c67022065632d4032673a6093f513b93e380323487ad2708003e161a12e7b7362bf9f4a01210325c1b08d90a016cb73f4e8d37614cac7da00cb78121f21b7b6e0a7d4a03fbae4fdffffff0100f4aa01000000001976a914ca093a938a0e19e86b36859d9423a475d45eb3a288acc54f2100" +) + +func init() { + testTx1 = bchain.Tx{ + Hex: "010000000123c41ad26dd5782635638effbc9e31c9b4a3b757591a52c83d2770ad82b33e93000000006b483045022100a20302bde6d2fb194bb9c0a8d7beb52ed0b5b72b912da75364efe169d5b74c67022065632d4032673a6093f513b93e380323487ad2708003e161a12e7b7362bf9f4a01210325c1b08d90a016cb73f4e8d37614cac7da00cb78121f21b7b6e0a7d4a03fbae4fdffffff0100f4aa01000000001976a914ca093a938a0e19e86b36859d9423a475d45eb3a288acc54f2100", + Blocktime: 1539653891, + Txid: "983da8317fff45afb17290d4dd8da6ec1cd8ffbbfa98e53a0754e9b60f8cc0f9", + LockTime: 2183109, + Version: 1, + Vin: []bchain.Vin{ + { + ScriptSig: bchain.ScriptSig{ + Hex: "483045022100a20302bde6d2fb194bb9c0a8d7beb52ed0b5b72b912da75364efe169d5b74c67022065632d4032673a6093f513b93e380323487ad2708003e161a12e7b7362bf9f4a01210325c1b08d90a016cb73f4e8d37614cac7da00cb78121f21b7b6e0a7d4a03fbae4", + }, + Txid: "933eb382ad70273dc8521a5957b7a3b4c9319ebcff8e63352678d56dd21ac423", + Vout: 0, + Sequence: 4294967293, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(27980800), + N: 0, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "76a914ca093a938a0e19e86b36859d9423a475d45eb3a288ac", + Addresses: []string{ + "GcGBy77CCfZJJhGLALohdahf9eAc7jo7Yk", + }, + }, + }, + }, + } +} + +func Test_PackTx(t *testing.T) { + type args struct { + tx bchain.Tx + height uint32 + blockTime int64 + parser *LiquidParser + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "gamecredits-1", + args: args{ + tx: testTx1, + height: 2183444, + blockTime: 1539653891, + parser: NewLiquidParser(GetChainParams("main"), &btc.Configuration{}), + }, + want: testTxPacked1, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.args.parser.PackTx(&tt.args.tx, tt.args.height, tt.args.blockTime) + if (err != nil) != tt.wantErr { + t.Errorf("packTx() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("packTx() = %v, want %v", h, tt.want) + } + }) + } +} + +func Test_UnpackTx(t *testing.T) { + type args struct { + packedTx string + parser *LiquidParser + } + tests := []struct { + name string + args args + want *bchain.Tx + want1 uint32 + wantErr bool + }{ + { + name: "gamecredits-1", + args: args{ + packedTx: testTxPacked1, + parser: NewLiquidParser(GetChainParams("main"), &btc.Configuration{}), + }, + want: &testTx1, + want1: 2183444, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, _ := hex.DecodeString(tt.args.packedTx) + got, got1, err := tt.args.parser.UnpackTx(b) + if (err != nil) != tt.wantErr { + t.Errorf("unpackTx() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("unpackTx() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("unpackTx() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} +*/ diff --git a/bchain/coins/liquid/liquidrpc.go b/bchain/coins/liquid/liquidrpc.go new file mode 100644 index 00000000..28fbf114 --- /dev/null +++ b/bchain/coins/liquid/liquidrpc.go @@ -0,0 +1,57 @@ +package liquid + +import ( + "blockbook/bchain" + "blockbook/bchain/coins/btc" + "encoding/json" + + "github.com/golang/glog" +) + +// LiquidRPC is an interface to JSON-RPC bitcoind service. +type LiquidRPC struct { + *btc.BitcoinRPC +} + +// NewLiquidParserRPC returns new LiquidRPC instance. +func NewLiquidParserRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { + b, err := btc.NewBitcoinRPC(config, pushHandler) + if err != nil { + return nil, err + } + + s := &LiquidRPC{ + b.(*btc.BitcoinRPC), + } + s.RPCMarshaler = btc.JSONMarshalerV2{} + s.ChainConfig.SupportsEstimateFee = false + + return s, nil +} + +// Initialize initializes GameCreditsRPC instance. +func (b *LiquidRPC) Initialize() error { + chainName, err := b.GetChainInfoAndInitializeMempool(b) + if err != nil { + return err + } + + glog.Info("Chain name ", chainName) + params := GetChainParams(chainName) + + // always create parser + b.Parser = NewLiquidParser(params, b.ChainConfig) + + // parameters for getInfo request + if params.Net == MainnetMagic { + b.Testnet = false + b.Network = "livenet" + } else { + b.Testnet = true + b.Network = "testnet" + } + + glog.Info("rpc: block chain ", params.Name) + + return nil +} diff --git a/configs/coins/liquid.json b/configs/coins/liquid.json new file mode 100644 index 00000000..1887dd53 --- /dev/null +++ b/configs/coins/liquid.json @@ -0,0 +1,65 @@ +{ + "coin": { + "name": "Liquid", + "shortcut": "L-BTC", + "label": "Liquid", + "alias": "Liquid" + }, + "ports": { + "backend_rpc": 8046, + "backend_message_queue": 38346, + "blockbook_internal": 9046, + "blockbook_public": 9146 + }, + "ipc": { + "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_user": "rpc", + "rpc_pass": "rpc", + "rpc_timeout": 25, + "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" + }, + "backend": { + "package_name": "backend-liquid", + "package_revision": "satoshilabs-1", + "system_user": "liquid", + "version": "3.14.1.21", + "binary_url": "https://github.com/Blockstream/liquid/releases/download/liquid.3.14.1.21/liquid-3.14.1.21-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "ea2836aa267b32b29e890acdd5e724b4be225c34891fd26426ce741c12c1e166", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": [ + "bin/test_elements" + ], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/liquidd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "bitcoin_like.conf", + "client_config_file": "bitcoin_like_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + } + }, + "blockbook": { + "package_name": "blockbook-liquid", + "system_user": "blockbook-liquid", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "additional_params": {} + } + }, + "meta": { + "package_maintainer": "Jakub Matys", + "package_maintainer_email": "jakub.matys@satoshilabs.com" + } +}