Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RPC Whitelist Feature from #12248 #12763

Closed
wants to merge 10 commits into
base: master
from

Conversation

@JeremyRubin
Copy link
Contributor

JeremyRubin commented Mar 23, 2018

Summary

This patch adds the RPC whitelisting feature requested in #12248. RPC Whitelists help enforce application policies for services being built on top of Bitcoin Core (e.g., your Lightning Node maybe shouldn't be adding new peers). The aim of this PR is not to make it advisable to connect your Bitcoin node to arbitrary services, but to reduce risk and prevent unintended access.

Using RPC Whitelists

The way it works is you specify (in your bitcoin.conf) configurations such as

rpcauth=user1:4cc74397d6e9972e5ee7671fd241$11849357f26a5be7809c68a032bc2b16ab5dcf6348ef3ed1cf30dae47b8bcc71
rpcauth=user2:181b4a25317bff60f3749adee7d6bca0$d9c331474f1322975fa170a2ffbcb176ba11644211746b27c1d317f265dd4ada
rpcauth=user3:a6c8a511b53b1edcf69c36984985e$13cfba0e626db19061c9d61fa58e712d0319c11db97ad845fa84517f454f6675
rpcwhitelist=user1:getnetworkinfo
rpcwhitelist=user2:getnetworkinfo,getwalletinfo, getbestblockhash
rpcwhitelistdefault=0

Now user1 can only call getnetworkinfo, user2 can only call getnetworkinfo or getwalletinfo, while user3 can still call all RPCs.

If any rpcwhitelist is set, act as if all users are subject to whitelists unless rpcwhitelistdefault is set to 0. If rpcwhitelistdefault is set to 1 and no rpcwhitelist is set, act as if all users are subject to whitelists.

Review Request

In addition to normal review, would love specific review from someone working on LN (e.g., @Roasbeef) and someone working on an infrastructure team at an exchange (e.g., @jimpo) to check that this works well with their system.

Notes

The rpc list is spelling sensitive -- whitespace is stripped though. Spelling errors fail towards the RPC call being blocked, which is safer.

It was unclear to me if HTTPReq_JSONRPC is the best function to patch this functionality into, or if it would be better to place it in exec or somewhere else.

It was also unclear to me if it would be preferred to cache the whitelists on startup or parse them on every RPC as is done with multiUserAuthorized. I opted for the cached approach as I thought it was a bit cleaner.

Future Work

In a future PR, I would like to add an inheritance scheme. This seemed more controversial so I didn't want to include that here. Inheritance semantics are tricky, but it would also make these whitelists easier to read.

It also might be good to add a getrpcwhitelist command to facilitate permission discovery.

Before Merge

I'm opening the PR now for general feedback, but I should add a couple tests before merge. I'd prefer to do documentation as a separate PR, but it can be added in this PR if preferred.

@kallewoof
Copy link
Member

kallewoof left a comment

utACK 788b174c33896065065d1ab376d6451b0f7575cd

src/httprpc.cpp Outdated

for (const std::string& strRPCWhitelist : gArgs.GetArgs("-rpcwhitelist")) {
std::string strUser = strRPCWhitelist.substr(0, strRPCWhitelist.find(':'));
std::string strWhitelist = strRPCWhitelist.substr(strRPCWhitelist.find(':') + 1);

This comment has been minimized.

@kallewoof

kallewoof Mar 23, 2018

Member

This behavior may be fine, but this will silently turn "foo" into strUser == strWhitelist == "foo".

This comment has been minimized.

@promag

promag Mar 23, 2018

Member

Agree, it should check for correct format.

@NicolasDorier

This comment has been minimized.

Copy link
Member

NicolasDorier commented Mar 23, 2018

Concept ACK. This is very welcome. My block explorer only rely on sendrawtransaction.
Would like a way to have my block explorer restrict itself at runtime to make the life of my users easier, but this might come later.

@instagibbs

This comment has been minimized.

Copy link
Member

instagibbs commented Mar 23, 2018

concept ACK

@instagibbs

This comment has been minimized.

Copy link
Member

instagibbs commented Mar 23, 2018

@RHavar you may be interested?

@randolf

This comment has been minimized.

Copy link
Contributor

randolf commented Mar 23, 2018

Concept ACK.

@RHavar

This comment has been minimized.

Copy link
Contributor

RHavar commented Mar 23, 2018

@instagibbs To be honest, I don't see it being that useful for me (but I don't have any objections to it either)

The main advantage I can see from this change is that you could use the same bitcoin-core instance for multiple purposes, like where one only requires access to the bitcoin-related features, and the other might need wallet access.

That's probably more interesting for consumer-level users, who are running core on their own computer. For more commercial users, we'd just use different instances of bitcoin-core itself.

--

The PRC permission feature I'm more interested in is a lot more high-level; like applying spending limits. What I would like to do is be able to store say 300 BTC in my wallet, but never allow it drop below 250 BTC (by a particular RPC user). This way I could have only 50 BTC of "risk" (e.g. in case my service was hacked) but could benefit from having 300 BTC worth of coins (so coin-selection can do a much better job)

@instagibbs

This comment has been minimized.

Copy link
Member

instagibbs commented Mar 23, 2018

@RHavar this prevents privkey dumps, as a first step at least

@promag
Copy link
Member

promag left a comment

Concept ACK. Indeed a useful feature where a daemon can be shared for multiple purposes.

rpcwhitelist=user2:getnetworkinfo,getwalletinfo

Is this possible?

Doing -rpcwhitelist=user2:foo -rpcwhitelist=user2:bar will end with user2: ['bar']. Maybe it should merge or maybe we should not allow multiple methods in the same argument?

This PR could include some tests and update code style as per developer notes.

src/httprpc.cpp Outdated
if (whitelistedRPC.count(jreq.authUser)) {
for (unsigned int reqIdx = 0; reqIdx < valRequest.size(); reqIdx++) {
if (!valRequest[reqIdx].isObject()) {
throw JSONRPCError(RPC_INVALID_REQUEST, "Invalid Request object");

This comment has been minimized.

@promag

promag Mar 23, 2018

Member

New error, should have a test.

src/httprpc.cpp Outdated
@@ -63,6 +63,8 @@ class HTTPRPCTimerInterface : public RPCTimerInterface
static std::string strRPCUserColonPass;
/* Stored RPC timer interface (for unregistration) */
static std::unique_ptr<HTTPRPCTimerInterface> httpRPCTimerInterface;
/* RPC Auth Whitelist */
static std::map<std::string, std::set<std::string>> whitelistedRPC;

This comment has been minimized.

@promag

promag Mar 23, 2018

Member

rpc_whitelist?

src/httprpc.cpp Outdated

for (const std::string& strRPCWhitelist : gArgs.GetArgs("-rpcwhitelist")) {
std::string strUser = strRPCWhitelist.substr(0, strRPCWhitelist.find(':'));
std::string strWhitelist = strRPCWhitelist.substr(strRPCWhitelist.find(':') + 1);

This comment has been minimized.

@promag

promag Mar 23, 2018

Member

Agree, it should check for correct format.

@jonasschnelli

This comment has been minimized.

Copy link
Member

jonasschnelli commented Mar 25, 2018

Concept ACK

src/httprpc.h Outdated
@@ -7,6 +7,7 @@

#include <string>
#include <map>
#include <set>

This comment has been minimized.

@jonasschnelli

jonasschnelli Mar 25, 2018

Member

Why has that to be here?

This comment has been minimized.

@JeremyRubin

JeremyRubin Mar 26, 2018

Author Contributor

Wasn't sure, was just trying to follow local style. Not sure that string or map need to be there either?

This comment has been minimized.

@promag

promag Mar 26, 2018

Member

None needs to be there. If you want to remove, do it in a different commit.

@jimpo
Copy link
Contributor

jimpo left a comment

Concept ACK. I can't imagine this being a problem for exchanges, given that it's backwards compatible and uses standard bitcoind configuration logic. Definitely fine for Coinbase.

src/httprpc.cpp Outdated
if (whitelistedRPC.count(jreq.authUser) && !whitelistedRPC[jreq.authUser].count(jreq.strMethod)) {
LogPrintf("RPC User %s not allowed to call method %s\n", jreq.authUser, jreq.strMethod);
req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
req->WriteReply(HTTP_UNAUTHORIZED);

This comment has been minimized.

@jimpo

jimpo Mar 26, 2018

Contributor

I think the 403 Forbidden status is more appropriate. See https://stackoverflow.com/a/6937030.

@kallewoof
Copy link
Member

kallewoof left a comment

utACK

src/httprpc.cpp Outdated
auto pos = strRPCWhitelist.find(':');
std::string strUser = strRPCWhitelist.substr(0, pos);
std::set<std::string>& whitelist = rpc_whitelist[strUser];
if (pos != std::string::npos) {

This comment has been minimized.

@kallewoof

kallewoof Mar 27, 2018

Member

Should this maybe error on == case? It now silently ignores nocolonvalues.

This comment has been minimized.

@JeremyRubin

JeremyRubin Mar 27, 2018

Author Contributor

It actually doesn't silently ignore it -- it sets an empty whitelist.

@eklitzke
Copy link
Member

eklitzke left a comment

I support adding ACLs and all that, so concept ACK.

The implementation here is problematic though. In a whitelisting approach everything should be disabled by default. This does the opposite: in this PR users have access to all RPCs by default. Adding a whitelist policy for a user implicitly restricts access to other ACLs, which is very confusing. If I am $COMPANY and I add a new engineer to the team, it is going to be very bad if I create an rpcauth for the user without remembering to also set up their ACLs. The default behavior in this situation should be safe, i.e. deny by default if any policy is defined.

There's a huge range here from really complex authorization schemes to really simple things, but I would prefer something that has at least basic steps towards a real roles/ACL system. At the minimum I think this means there should be a way to declare a default list of ACLs, and then whitelists would expand on that.

src/httprpc.cpp Outdated
std::set<std::string>& whitelist = rpc_whitelist[strUser];
if (pos != std::string::npos) {
std::string strWhitelist = strRPCWhitelist.substr(pos + 1);
boost::split(whitelist, strWhitelist, boost::is_any_of(","));

This comment has been minimized.

@eklitzke

eklitzke Mar 27, 2018

Member

You can split with a std::istream or std::getline, no need for boost::split here.

This comment has been minimized.

@JeremyRubin

JeremyRubin Mar 27, 2018

Author Contributor

I'd prefer to leave boost::split -- this is an existing dependency throughout the codebase, at some point someone will likely clean them all up consistently with an addition to util.

I'm happy to make such a PR if there is demand for it.

@kallewoof

This comment has been minimized.

Copy link
Member

kallewoof commented Mar 27, 2018

Yeah it's probably better to check if size() > 0 and to do default-no-access if so.

@JeremyRubin

This comment has been minimized.

Copy link
Contributor Author

JeremyRubin commented Mar 27, 2018

@eklitzke

In the current scheme, a user by default has access to all RPCs to maintain backwards compatibility.

Once a file has specified that a user should have a whitelist, it defaults to everything being off.

I agree this is not ideal, but don't have a great workaround.

Here are two potential solutions:

We add:
-rpcwhitelistenable=<rpc 1>,<rpc 2>
-rpcwhitelistroot=

If rpcwhitelistenable is set, the by default any user has that whitelist (allowed to be empty)? If a user is marked rpcwhitelistroot, they have all RPCs enabled. If a user is marked rpcwhitelist=:blah, then they have blah.

Or, we can make it such that if any rpcwhitelist is set, all users default to having an empty whitelist.

Do you think this is a good tradeoff of complexity/dtrt? Which do you prefer?

@promag
The other detail (in a forthcoming patch) is that if rpcwhitelist is set multiple times for a single user, it should do the intersection of the specification (e.g., monotonically smaller whitelist) rather than the union. In cases where conflicting settings have been passed, it is safer to do less.

@sipa

This comment has been minimized.

Copy link
Member

sipa commented Mar 27, 2018

~~~I'm not sure this approach is ideal; it makes administrators responsible for knowing the list of all RPCs and their potential effects.~~~

EDIT: nevermind, that's a concern for blacklists, not whitelists.

@eklitzke

This comment has been minimized.

Copy link
Member

eklitzke commented Mar 28, 2018

@JeremyRubin I would add both. I guess my point is that if you need permissions at all you probably also want the ability to enable some kind of deny-by-default policy, to safeguard against a situation where you accidentally forget to lock down an account. You generally don't want to give your software engineers root access on production machines by default, and by the same token I don't think you would want to give people with bitcoind access root by default.

The way I imagine a typical setup would be something like this (ignore the made up syntax):

# Default permissions are for a bunch of read only rpcs
rpcallowed = getblock,getblockchaininfo

# Alice has the default permissions, plus stuff to admin the network
rpcallowed.alice = addnode,clearbanned,listbanned

# Bob has the default permissions, plus some basic access to the wallet
rpcallowed.bob = getbalance,getwalletinfo,getnewaddress

# Carol is an admin, she can access everything
rpcallowed.carol = !all

And semantics something like this:

  • If there are no rpcallowed lines at all then everyone can access everything, just like how bitcoin works today
  • If you set rpcallowed.alice (Alice's list) but forget to set rpcallowed (the default list) then it should deny by default (maybe with a special error message attached to the RPC warning about the bad configuration)

Admittedly there is an area ripe for feature creep, but I think the above would be relatively simple to implement and good enough for a lot of cases.

@eklitzke

This comment has been minimized.

Copy link
Member

eklitzke commented Mar 28, 2018

Second idea that's much more crazy/complex. I'll throw it out there though.

YAML documents have a syntax to reference other elements, which are really useful for these kinds of things. You can construct a few objects in your config and reference them elsewhere, which allows you to come up with some pseduo-role system:

---
policies:
  # a default policy
  - policy: &default
      - getblock
      - getblockchaininfo
      - getblockcount

  # network admin policy
  - policy: &networkadmin
      - addnode
      - clearbanned
      - getpeerinfo

users:
  # alice has default plus network admin
  - name: alice
    policies:
      - *default
      - *networkadmin

  # bob just has default policies
  - name: bob
    policies:
      - *default

  # syntax idea 1 for full access
  - name: carol
    admin: true

  # syntax idea 2 for full access, !all is implicitly defined
  - name: dave
    policies: [*all]

The &obj syntax introduces a name for a reference, and then *obj means "substitute the literal value for obj here". I'm not sure how it works in C++ YAML libraries, but the ones I've used in Python do the reference expansion within the YAML library. This makes it so your code can just work with dumb lists of objects, since they never see the object references.

The obvious drawback is that this would require linking against a YAML library. You could mitigate that by only having the user ACL list be in a YAML lib, so only users who actually want these feature would need to enable the YAML parser.

@JeremyRubin

This comment has been minimized.

Copy link
Contributor Author

JeremyRubin commented Mar 30, 2018

@eklitzke did you check out the issue? The original proposal covered doing an inheritance scheme. @gmaxwell suggested that we should avoid doing any fancy config file, in favor of just a simple list.

I do think that this could get overly verbose (especially if you want to grant network multiple times), but in general the paradigm should be to configure applications to manage multiple small credentials for specific purposes rather than one full-grant.

@JeremyRubin JeremyRubin force-pushed the JeremyRubin:whitelistrpc branch Apr 13, 2018

@JeremyRubin

This comment has been minimized.

Copy link
Contributor Author

JeremyRubin commented Apr 13, 2018

I've updated this PR and rebased.

The current version has the following changes over the previous:

  • Strip whitespace out of rpc list
  • Set-Intersect conflicting whitelists (i.e., so it is equivalent to checking multiple whitelists)
  • If any rpcwhitelist is set, act as if all users are subject to whitelists unless rpcwhitelistdefault is set to 0. If rpcwhitelistdefault is set to 1 and no rpcwhitelist is set, act as if all users are subject to whitelists.

Please let me know if these semantics are acceptable.

@kallewoof

This comment has been minimized.

Copy link
Member

kallewoof commented Apr 13, 2018

LGTM. Checked code, utACK.

@jimpo

This comment has been minimized.

Copy link
Contributor

jimpo commented May 3, 2018

I'm not really a fan of the rpcwhitelistdefault flag. I agree with the decision to provide backwards compatibility in the API by whitelisting everyone for everything if -rpcwhitelist is not set, but if it is, I think whitelists should be more explicit. Perhaps instead -rpcwhitelist=USER would whitelist USER for everything, whereas -rpcwhitelist=USER:ACTION1,ACTION2 would whitelist a user for specific actions.

@JeremyRubin

This comment has been minimized.

Copy link
Contributor Author

JeremyRubin commented May 7, 2018

-rpcwhitelist=USER is currently a null list, and i think should remain that.

We could introduce a new flag, e.g. rpcwhitelistallowall if that's the desired ability.

@laanwj

This comment has been minimized.

Copy link
Member

laanwj commented May 30, 2018

I've always been strongly against complex authorization schemes in bitcoind. A lot of extra security critical logic to maintain. I think the place for such things is a separate authorization proxy. This is more modular, more in line with "software should do one thing well", and it depends on what security system is required, nothing can accommodate every possible separation of privileges (see also: linux security modules).

That said, I like the simplicity of this scheme, simply specifying the calls that are allowed, and the small code impact, so Concept ACK.

@promag

This comment has been minimized.

Copy link
Member

promag commented May 30, 2018

The other detail (in a forthcoming patch) is that if rpcwhitelist is set multiple times for a single user, it should do the intersection of the specification (e.g., monotonically smaller whitelist) rather than the union. In cases where conflicting settings have been passed, it is safer to do less.

@JeremyRubin lgtm.

@JeremyRubin JeremyRubin force-pushed the JeremyRubin:whitelistrpc branch to 8c45d93 Jun 12, 2018

@JeremyRubin

This comment has been minimized.

Copy link
Contributor Author

JeremyRubin commented Jun 12, 2018

Rebased and added documentation.

Started working on tests but got a little stuck with the current test framework -- the test classes assume access to the RPCs, which doesn't let me cover all of the modes of use for rpc whitelists.

@JeremyRubin JeremyRubin reopened this Jun 18, 2018

@gmaxwell

This comment has been minimized.

Copy link
Member

gmaxwell commented Jun 18, 2018

@RHavar The thing you're interested in would almost just fit into this if there were RPCs to set spending velocity limits on wallets, and then just use this PR to deny the relevant clients from calling the interface to increase their limits.

The only gap I see with that is that is that it would be a per wallet not per rpc user limit. In any case, having wallet velocity limits even that you can just bypass by calling a function would go a long way to reducing mistake surface.

@JeremyRubin

This comment has been minimized.

Copy link
Contributor Author

JeremyRubin commented Jun 27, 2018

Tagging @jnewbery for tips on best way to use the test framework here. The issue is that in order to test whitelists properly, I need to disable RPC access...

Should I just write tests as a unit test (non-functional test)?

@MarcoFalke

This comment has been minimized.

Copy link
Member

MarcoFalke commented Jun 28, 2018

If any rpcwhitelist is set, act as if all users are subject to whitelists unless rpcwhitelistdefault is set to 0. If rpcwhitelistdefault is set to 1 and no rpcwhitelist is set, act as if all users are subject to whitelists.

Please update OP to reflect that.

Should I just write tests as a unit test (non-functional test)?

With regard to tests, I believe it is trivial to test the following [c.f. Attachment 1] in our existing functional tests:

  • No whitelist specified at all: Tested by all existing tests
  • A user withelisted
  • A user blocked because other users are whitelisted

Am I missing a test case?

Attachment 1
diff --git a/test/functional/rpc_users.py b/test/functional/rpc_users.py
index 1ef59da5ad..864cd5977c 100755
--- a/test/functional/rpc_users.py
+++ b/test/functional/rpc_users.py
@@ -24,15 +24,30 @@ class HTTPBasicsTest(BitcoinTestFramework):
     def set_test_params(self):
         self.num_nodes = 2
 
+    def setup_network(self):
+        self.setup_nodes()
+
     def setup_chain(self):
         super().setup_chain()
         #Append rpcauth to bitcoin.conf before initialization
+        whitelist = "rpcwhitelist=__cookie__:getblockcount,stop,getbestblockhash\n"
+
         rpcauth = "rpcauth=rt:93648e835a54c573682c2eb19f882535$7681e9c5b74bdd85e78166031d2058e1069b3ed7ed967c93fc63abba06f31144"
+        whitelist += "rpcwhitelist=rt:getbestblockhash\n"
+
         rpcauth2 = "rpcauth=rt2:f8607b1a88861fac29dfccf9b52ff9f$ff36a0c23c8c62b4846112e50fa888416e94c17bfd4c42f88fd8f55ec6a3137e"
+        whitelist += "rpcwhitelist=rt2:getbestblockhash\n"
+
+        rpcauth_blocked = "rpcauth=blocked:e66047993b1784b4dc4dddbccf9c297$fb62cda6ae6a31edf42d3d4b639a0d965e9bcea49bf1c2d61d423f373891daa1"
+        # not whitelisted
+
         rpcuser = "rpcuser=rpcuser💻"
+        whitelist += "rpcwhitelist=rpcuser💻:getbestblockhash\n"
+
         rpcpassword = "rpcpassword=rpcpassword🔑"
 
         self.user = ''.join(SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10))
+        whitelist += "rpcwhitelist={}:getbestblockhash\n".format(self.user)
         config = configparser.ConfigParser()
         config.read_file(open(self.options.configfile))
         gen_rpcauth = config['environment']['RPCAUTH']
@@ -43,7 +58,9 @@ class HTTPBasicsTest(BitcoinTestFramework):
 
         with open(os.path.join(get_datadir_path(self.options.tmpdir, 0), "bitcoin.conf"), 'a', encoding='utf8') as f:
             f.write(rpcauth+"\n")
+            f.write(whitelist)
             f.write(rpcauth2+"\n")
+            f.write(rpcauth_blocked+"\n")
             f.write(rpcauth3+"\n")
         with open(os.path.join(get_datadir_path(self.options.tmpdir, 1), "bitcoin.conf"), 'a', encoding='utf8') as f:
             f.write(rpcuser+"\n")
@@ -66,6 +83,9 @@ class HTTPBasicsTest(BitcoinTestFramework):
         password2 = "8/F3uMDw4KSEbw96U3CA1C4X05dkHDN2BPFjTgZW4KI="
         authpairnew = "rt:"+password
 
+        #Third authpair (not whitelisted)
+        password_blocked = "qpd5D9Y1mSavkBlIomhXlG4cqcRLveJvhyANVSxtk70="
+
         self.log.info('Correct...')
         headers = {"Authorization": "Basic " + str_to_b64str(authpair)}
 
@@ -123,6 +143,29 @@ class HTTPBasicsTest(BitcoinTestFramework):
         assert_equal(resp.status, 200)
         conn.close()
 
+        self.log.info('rt2 is not whitelisted on this specific rpc...')
+        authpairnew = "rt2:"+password2
+        headers = {"Authorization": "Basic " + str_to_b64str(authpairnew)}
+
+        conn = http.client.HTTPConnection(url.hostname, url.port)
+        conn.connect()
+        conn.request('POST', '/', '{"method": "getnetworkinfo"}', headers)
+        resp = conn.getresponse()
+        assert_equal(resp.status, 403)
+        conn.close()
+
+
+        self.log.info('Calling an rpc from a user with no whitelist...')
+        authpairnew = "blocked:" + password_blocked
+        headers = {"Authorization": "Basic " + str_to_b64str(authpairnew)}
+
+        conn = http.client.HTTPConnection(url.hostname, url.port)
+        conn.connect()
+        conn.request('POST', '/', '{"method": "getbestblockhash"}', headers)
+        resp = conn.getresponse()
+        assert_equal(resp.status, 403)
+        conn.close()
+
         #Wrong password for rt2
         self.log.info('Wrong...')
         authpairnew = "rt2:"+password2+"wrong"
@jnewbery

This comment has been minimized.

Copy link
Member

jnewbery commented Jun 28, 2018

@MarcoFalke's test looks good. Will review fully once it's been included in this PR.

@JeremyRubin

This comment has been minimized.

Copy link
Contributor Author

JeremyRubin commented Jun 28, 2018

Those tests look fine, and are close to where I got stuck.

Cases missing

  • Easy to Add:

    • Multiple RPC Whitelists specified, should intersect
  • Difficult:

    • No whitelist specified for any user, rpcwhitelistdefault=1
@MarcoFalke

This comment has been minimized.

Copy link
Member

MarcoFalke commented Jun 30, 2018

Difficult: No whitelist specified for any user, rpcwhitelistdefault=1

Indeed not trivial. Options I see is to rework the test framework to fall back to rest if rpc is not available or, as you mentioned, write a unit test.

@DrahtBot

This comment has been minimized.

Copy link
Contributor

DrahtBot commented Jul 10, 2018

Needs rebase
@MarcoFalke

This comment has been minimized.

Copy link
Member

MarcoFalke commented Jul 11, 2018

Merge conflict can probably be solved by a simple union merge.

@MarcoFalke

This comment has been minimized.

Copy link
Member

MarcoFalke commented Nov 8, 2018

@JeremyRubin Are you still working on this?

@fanquake

This comment has been minimized.

Copy link
Member

fanquake commented Mar 2, 2019

Closing, leaving Up for grabs.

@fanquake fanquake closed this Mar 2, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.