github.com/mit-dci/lit@v0.0.0-20221102210550-8c3d3b49f2ce/litrpc/lndcrpcclient.go (about) 1 package litrpc 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net" 8 "os" 9 "path/filepath" 10 "sync" 11 "time" 12 13 "github.com/mit-dci/lit/btcutil/hdkeychain" 14 "github.com/mit-dci/lit/coinparam" 15 "github.com/mit-dci/lit/crypto/koblitz" 16 "github.com/mit-dci/lit/lndc" 17 "github.com/mit-dci/lit/lnutil" 18 "github.com/mit-dci/lit/logging" 19 "github.com/mit-dci/lit/portxo" 20 ) 21 22 // LndcRpcClient can be used to remotely talk to a lit node over LNDC, making 23 // remote control instructions over the remote control interface. It uses a 24 // regular lndc.Conn to connect to lit over port 2448, and sends 25 // RemoteControlRpcRequestMsg to it, and receives RemoteControlRpcResponseMsg 26 type LndcRpcClient struct { 27 lnconn *lndc.Conn 28 requestNonce uint64 29 requestNonceMtx sync.Mutex 30 responseChannelMtx sync.Mutex 31 responseChannels map[uint64]chan lnutil.RemoteControlRpcResponseMsg 32 key *koblitz.PrivateKey 33 conMtx sync.Mutex 34 } 35 36 // LndcRpcCanConnectLocally checks if we can connect to lit using the normal 37 // home directory. In that case, we read from the privkey.hex and use a different 38 // derivation than the nodeID to determine the private key. This key is authorized 39 // by default for remote control. 40 func LndcRpcCanConnectLocally() bool { 41 litHomeDir := os.Getenv("HOME") + "/.lit" 42 return LndcRpcCanConnectLocallyWithHomeDir(litHomeDir) 43 } 44 45 // LndcRpcCanConnectLocallyWithHomeDir checks if we can connect to lit given the 46 // home directory. In that case, we read from the privkey.hex and use a different 47 // derivation than the nodeID to determine the private key. This key is authorized 48 // by default for remote control. 49 func LndcRpcCanConnectLocallyWithHomeDir(litHomeDir string) bool { 50 keyFilePath := filepath.Join(litHomeDir, "privkey.hex") 51 52 _, err := os.Stat(keyFilePath) 53 return (err == nil) 54 } 55 56 // NewLocalLndcRpcClient is an overload for NewLocalLndcRpcClientWithHomeDirAndPort 57 // using the default home dir and port 58 func NewLocalLndcRpcClient() (*LndcRpcClient, error) { 59 litHomeDir := os.Getenv("HOME") + "/.lit" 60 return NewLocalLndcRpcClientWithHomeDirAndPort(litHomeDir, 2448) 61 } 62 63 // NewLocalLndcRpcClientWithPort is an overload for 64 // NewLocalLndcRpcClientWithHomeDirAndPort using the default home dir 65 func NewLocalLndcRpcClientWithPort(port uint32) (*LndcRpcClient, error) { 66 litHomeDir := os.Getenv("HOME") + "/.lit" 67 return NewLocalLndcRpcClientWithHomeDirAndPort(litHomeDir, port) 68 } 69 70 // NewLocalLndcRpcClientWithHomeDirAndPort loads up privkey.hex, and derives 71 // the local lit node's address from it, as well as derives the default remote 72 // control private key from it. Then it will connect to the local lit instance. 73 func NewLocalLndcRpcClientWithHomeDirAndPort(litHomeDir string, port uint32) (*LndcRpcClient, error) { 74 keyFilePath := filepath.Join(litHomeDir, "privkey.hex") 75 privKey, err := lnutil.ReadKeyFile(keyFilePath) 76 if err != nil { 77 return nil, err 78 } 79 rootPrivKey, err := hdkeychain.NewMaster(privKey[:], &coinparam.TestNet3Params) 80 if err != nil { 81 return nil, err 82 } 83 84 var kg portxo.KeyGen 85 kg.Depth = 5 86 kg.Step[0] = 44 | 1<<31 87 kg.Step[1] = 513 | 1<<31 88 kg.Step[2] = 9 | 1<<31 89 kg.Step[3] = 1 | 1<<31 90 kg.Step[4] = 0 | 1<<31 91 key, err := kg.DerivePrivateKey(rootPrivKey) 92 if err != nil { 93 return nil, err 94 } 95 96 kg.Step[3] = 0 | 1<<31 97 localIDPriv, err := kg.DerivePrivateKey(rootPrivKey) 98 if err != nil { 99 logging.Errorf(err.Error()) 100 } 101 var localIDPub [33]byte 102 copy(localIDPub[:], localIDPriv.PubKey().SerializeCompressed()) 103 104 adr := fmt.Sprintf("%s@127.0.0.1:%d", lnutil.LitAdrFromPubkey(localIDPub), port) 105 localIDPriv = nil 106 107 return NewLndcRpcClient(adr, key) 108 } 109 110 // NewLndcRpcClient creates a new LNDC client using the given private key, which 111 // is arbitrary. It will then connect to the lit node specified in address, and 112 // can then exchange remote control calls with it. In order to succesfully 113 // execute command, the given key must be authorized in the lit instance we're 114 // connecting to. 115 func NewLndcRpcClient(address string, key *koblitz.PrivateKey) (*LndcRpcClient, error) { 116 var err error 117 118 cli := new(LndcRpcClient) 119 // Create a map of chan objects to receive returned responses on. These channels 120 // are sent to from the ReceiveLoop, and awaited in the Call method. 121 cli.responseChannels = make(map[uint64]chan lnutil.RemoteControlRpcResponseMsg) 122 123 //Parse the address we're connecting to 124 who, where := lnutil.ParseAdrString(address) 125 126 // If we couldn't deduce a URL, look it up on the tracker 127 if where == "" { 128 // TODO: Implement address lookups 129 err = fmt.Errorf("Tracker lookups not supported yet from LNDC proxy") 130 if err != nil { 131 return nil, err 132 } 133 } 134 135 // Dial a connection to the lit node 136 cli.lnconn, err = lndc.Dial(key, where, who, net.Dial) 137 if err != nil { 138 return nil, err 139 } 140 141 // Start the receive loop for reply messages 142 go cli.ReceiveLoop() 143 return cli, nil 144 } 145 146 func (cli *LndcRpcClient) Close() error { 147 return cli.lnconn.Close() 148 } 149 150 func (cli *LndcRpcClient) Call(serviceMethod string, args interface{}, reply interface{}) error { 151 var err error 152 153 // Generate a local unique nonce using the mutex 154 cli.requestNonceMtx.Lock() 155 cli.requestNonce++ 156 nonce := cli.requestNonce 157 cli.requestNonceMtx.Unlock() 158 159 // Create the channel to receive the reply on 160 cli.responseChannelMtx.Lock() 161 cli.responseChannels[nonce] = make(chan lnutil.RemoteControlRpcResponseMsg) 162 cli.responseChannelMtx.Unlock() 163 164 // Send the message in a goroutine 165 go func() { 166 msg := new(lnutil.RemoteControlRpcRequestMsg) 167 msg.Args, err = json.Marshal(args) 168 msg.Idx = nonce 169 msg.Method = serviceMethod 170 171 if err != nil { 172 logging.Fatal(err) 173 } 174 175 rawMsg := msg.Bytes() 176 cli.conMtx.Lock() 177 n, err := cli.lnconn.Write(rawMsg) 178 cli.conMtx.Unlock() 179 if err != nil { 180 logging.Fatal(err) 181 } 182 183 if n < len(rawMsg) { 184 logging.Fatal(fmt.Errorf("Did not write entire message to peer")) 185 } 186 }() 187 188 // If reply is nil the caller apparently doesn't care about the results. So we shouldn't wait for it 189 if reply != nil { 190 // If not nil, await the reply from the responseChannel for the nonce we sent out. 191 // the server will include the same nonce in its reply. 192 select { 193 case receivedReply := <-cli.responseChannels[nonce]: 194 { 195 if receivedReply.Error { 196 return errors.New(string(receivedReply.Result)) 197 } 198 199 err = json.Unmarshal(receivedReply.Result, &reply) 200 return err 201 } 202 case <-time.After(time.Second * 10): 203 // If no reply is received within 10 seconds, we time out the request. 204 // TODO: We could make this configurable in the call 205 return errors.New("RPC call timed out") 206 } 207 } 208 return nil 209 } 210 211 // ReceiveLoop reads messages from the LNDC connection and check if they are 212 // RPC responses 213 func (cli *LndcRpcClient) ReceiveLoop() { 214 for { 215 msg := make([]byte, 1<<24) 216 // log.Printf("read message from %x\n", l.RemoteLNId) 217 n, err := cli.lnconn.Read(msg) 218 if err != nil { 219 logging.Warnf("Error reading message from LNDC: %s\n", err.Error()) 220 cli.lnconn.Close() 221 return 222 } 223 msg = msg[:n] 224 // We only care about RPC responses (for now) 225 if msg[0] == lnutil.MSGID_REMOTE_RPCRESPONSE { 226 // Parse the received message 227 response, err := lnutil.NewRemoteControlRpcResponseMsgFromBytes(msg, 0) 228 if err != nil { 229 logging.Warnf("Error while receiving RPC response: %s\n", err.Error()) 230 cli.lnconn.Close() 231 return 232 } 233 234 // Find the response channel to send the reply to 235 responseChan, ok := cli.responseChannels[response.Idx] 236 if ok { 237 // Send the response, but don't depend on someone 238 // listening. The caller decides if he's interested in the 239 // reply and therefore, it could have not blocked and just 240 // ignore the return value. 241 select { 242 case responseChan <- response: 243 default: 244 } 245 246 // Clean up the channel to preserve memory. It's only used once. 247 cli.responseChannelMtx.Lock() 248 delete(cli.responseChannels, response.Idx) 249 cli.responseChannelMtx.Unlock() 250 251 } else { 252 logging.Errorf("Could not find response channel for index %d\n", response.Idx) 253 } 254 } 255 } 256 257 }