URI: 
       tUse attr.s instead of namedtuples for channel config - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 757467782a7c36332754d433d846b400b4aa770e
   DIR parent 9bd633fb0bd96b098596ccb55bea7a6fabf4465b
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Thu, 30 Jan 2020 18:09:32 +0100
       
       Use attr.s instead of namedtuples for channel config
       
       Diffstat:
         M contrib/requirements/requirements.… |       1 +
         M electrum/lnchannel.py               |      77 ++++++++++++++++++-------------
         M electrum/lnpeer.py                  |      19 +++++++------------
         M electrum/lnsweep.py                 |       2 +-
         M electrum/lnutil.py                  |     115 +++++++++++++++----------------
         M electrum/tests/test_lnchannel.py    |      25 +++++++++++--------------
         M electrum/tests/test_lnutil.py       |       6 +++---
       
       7 files changed, 123 insertions(+), 122 deletions(-)
       ---
   DIR diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt
       t@@ -12,3 +12,4 @@ bitstring
        pycryptodomex>=3.7
        jsonrpcserver
        jsonrpcclient
       +attrs
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -29,6 +29,7 @@ import json
        from enum import IntEnum
        from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable, Sequence, TYPE_CHECKING
        import time
       +import threading
        
        from . import ecc
        from .util import bfh, bh2u
       t@@ -99,6 +100,8 @@ class ChannelJsonEncoder(json.JSONEncoder):
                    return o.serialize()
                if isinstance(o, set):
                    return list(o)
       +        if hasattr(o, 'to_json') and callable(o.to_json):
       +            return o.to_json()
                return super().default(o)
        
        RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
       t@@ -110,7 +113,7 @@ class RemoteCtnTooFarInFuture(Exception): pass
        def decodeAll(d, local):
            for k, v in d.items():
                if k == 'revocation_store':
       -            yield (k, RevocationStore.from_json_obj(v))
       +            yield (k, RevocationStore(v))
                elif k.endswith("_basepoint") or k.endswith("_key"):
                    if local:
                        yield (k, Keypair(**dict(decodeAll(v, local))))
       t@@ -152,6 +155,7 @@ class Channel(Logger):
                self.lnworker = lnworker  # type: Optional[LNWallet]
                self.sweep_address = sweep_address
                assert 'local_state' not in state
       +        self.db_lock = self.lnworker.wallet.storage.db.lock if self.lnworker else threading.RLock()
                self.config = {}
                self.config[LOCAL] = state["local_config"]
                if type(self.config[LOCAL]) is not LocalConfig:
       t@@ -181,6 +185,7 @@ class Channel(Logger):
                self.peer_state = peer_states.DISCONNECTED
                self.sweep_info = {}  # type: Dict[str, Dict[str, SweepInfo]]
                self._outgoing_channel_update = None  # type: Optional[bytes]
       +        self.revocation_store = RevocationStore(state["revocation_store"])
        
            def get_feerate(self, subject, ctn):
                return self.hm.get_feerate(subject, ctn)
       t@@ -211,12 +216,13 @@ class Channel(Logger):
                return out
        
            def open_with_first_pcp(self, remote_pcp, remote_sig):
       -        self.config[REMOTE] = self.config[REMOTE]._replace(current_per_commitment_point=remote_pcp,
       -                                                           next_per_commitment_point=None)
       -        self.config[LOCAL] = self.config[LOCAL]._replace(current_commitment_signature=remote_sig)
       -        self.hm.channel_open_finished()
       -        self.peer_state = peer_states.GOOD
       -        self.set_state(channel_states.OPENING)
       +        with self.db_lock:
       +            self.config[REMOTE].current_per_commitment_point=remote_pcp
       +            self.config[REMOTE].next_per_commitment_point=None
       +            self.config[LOCAL].current_commitment_signature=remote_sig
       +            self.hm.channel_open_finished()
       +            self.peer_state = peer_states.GOOD
       +            self.set_state(channel_states.OPENING)
        
            def set_state(self, state):
                """ set on-chain state """
       t@@ -279,7 +285,8 @@ class Channel(Logger):
                self._check_can_pay(htlc.amount_msat)
                if htlc.htlc_id is None:
                    htlc = htlc._replace(htlc_id=self.hm.get_next_htlc_id(LOCAL))
       -        self.hm.send_htlc(htlc)
       +        with self.db_lock:
       +            self.hm.send_htlc(htlc)
                self.logger.info("add_htlc")
                return htlc
        
       t@@ -300,7 +307,8 @@ class Channel(Logger):
                    raise RemoteMisbehaving('Remote dipped below channel reserve.' +\
                            f' Available at remote: {self.available_to_spend(REMOTE)},' +\
                            f' HTLC amount: {htlc.amount_msat}')
       -        self.hm.recv_htlc(htlc)
       +        with self.db_lock:
       +            self.hm.recv_htlc(htlc)
                self.logger.info("receive_htlc")
                return htlc
        
       t@@ -346,9 +354,8 @@ class Channel(Logger):
                    htlcsigs.append((ctx_output_idx, htlc_sig))
                htlcsigs.sort()
                htlcsigs = [x[1] for x in htlcsigs]
       -
       -        self.hm.send_ctx()
       -
       +        with self.db_lock:
       +            self.hm.send_ctx()
                return sig_64, htlcsigs
        
            def receive_new_commitment(self, sig, htlc_sigs):
       t@@ -395,11 +402,10 @@ class Channel(Logger):
                                     pcp=pcp,
                                     ctx=pending_local_commitment,
                                     ctx_output_idx=ctx_output_idx)
       -
       -        self.hm.recv_ctx()
       -        self.config[LOCAL]=self.config[LOCAL]._replace(
       -            current_commitment_signature=sig,
       -            current_htlc_signatures=htlc_sigs_string)
       +        with self.db_lock:
       +            self.hm.recv_ctx()
       +            self.config[LOCAL].current_commitment_signature=sig
       +            self.config[LOCAL].current_htlc_signatures=htlc_sigs_string
        
            def verify_htlc(self, *, htlc: UpdateAddHtlc, htlc_sig: bytes, htlc_direction: Direction,
                            pcp: bytes, ctx: Transaction, ctx_output_idx: int) -> None:
       t@@ -429,7 +435,8 @@ class Channel(Logger):
                if not self.signature_fits(new_ctx):
                    # this should never fail; as receive_new_commitment already did this test
                    raise Exception("refusing to revoke as remote sig does not fit")
       -        self.hm.send_rev()
       +        with self.db_lock:
       +            self.hm.send_rev()
                received = self.hm.received_in_ctn(new_ctn)
                sent = self.hm.sent_in_ctn(new_ctn)
                if self.lnworker:
       t@@ -451,13 +458,12 @@ class Channel(Logger):
                if cur_point != derived_point:
                    raise Exception('revoked secret not for current point')
        
       -        self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret)
       -        ##### start applying fee/htlc changes
       -        self.hm.recv_rev()
       -        self.config[REMOTE]=self.config[REMOTE]._replace(
       -            current_per_commitment_point=self.config[REMOTE].next_per_commitment_point,
       -            next_per_commitment_point=revocation.next_per_commitment_point,
       -        )
       +        with self.db_lock:
       +            self.revocation_store.add_next_entry(revocation.per_commitment_secret)
       +            ##### start applying fee/htlc changes
       +            self.hm.recv_rev()
       +            self.config[REMOTE].current_per_commitment_point=self.config[REMOTE].next_per_commitment_point
       +            self.config[REMOTE].next_per_commitment_point=revocation.next_per_commitment_point
        
            def balance(self, whose, *, ctx_owner=HTLCOwner.LOCAL, ctn=None):
                """
       t@@ -548,7 +554,7 @@ class Channel(Logger):
                        secret = None
                        point = conf.current_per_commitment_point
                    else:
       -                secret = conf.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
       +                secret = self.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
                        point = secret_to_pubkey(int.from_bytes(secret, 'big'))
                else:
                    secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
       t@@ -624,15 +630,18 @@ class Channel(Logger):
                htlc = log['adds'][htlc_id]
                assert htlc.payment_hash == sha256(preimage)
                assert htlc_id not in log['settles']
       -        self.hm.recv_settle(htlc_id)
       +        with self.db_lock:
       +            self.hm.recv_settle(htlc_id)
        
            def fail_htlc(self, htlc_id):
                self.logger.info("fail_htlc")
       -        self.hm.send_fail(htlc_id)
       +        with self.db_lock:
       +            self.hm.send_fail(htlc_id)
        
            def receive_fail_htlc(self, htlc_id):
                self.logger.info("receive_fail_htlc")
       -        self.hm.recv_fail(htlc_id)
       +        with self.db_lock:
       +            self.hm.recv_fail(htlc_id)
        
            def pending_local_fee(self):
                return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs())
       t@@ -641,10 +650,11 @@ class Channel(Logger):
                # feerate uses sat/kw
                if self.constraints.is_initiator != from_us:
                    raise Exception(f"Cannot update_fee: wrong initiator. us: {from_us}")
       -        if from_us:
       -            self.hm.send_update_fee(feerate)
       -        else:
       -            self.hm.recv_update_fee(feerate)
       +        with self.db_lock:
       +            if from_us:
       +                self.hm.send_update_fee(feerate)
       +            else:
       +                self.hm.recv_update_fee(feerate)
        
            def to_save(self):
                to_save = {
       t@@ -656,6 +666,7 @@ class Channel(Logger):
                        "funding_outpoint": self.funding_outpoint,
                        "node_id": self.node_id,
                        "log": self.hm.to_save(),
       +                "revocation_store": self.revocation_store,
                        "onion_keys": str_bytes_dict_to_save(self.onion_keys),
                        "state": self._state.name,
                        "data_loss_protect_remote_pcp": str_bytes_dict_to_save(self.data_loss_protect_remote_pcp),
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -548,7 +548,6 @@ class Peer(Logger):
                if remote_to_self_delay > MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED:
                    raise Exception(f"Remote Lightning peer reports to_self_delay={remote_to_self_delay}," +
                            f" which is above Electrums required maximum ({MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED})")
       -        their_revocation_store = RevocationStore()
                remote_config = RemoteConfig(
                    payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
                    multisig_key=OnlyPubkeyKeypair(payload["funding_pubkey"]),
       t@@ -564,7 +563,6 @@ class Peer(Logger):
                    htlc_minimum_msat = htlc_min,
                    next_per_commitment_point=remote_per_commitment_point,
                    current_per_commitment_point=None,
       -            revocation_store=their_revocation_store,
                )
                # replace dummy output in funding tx
                redeem_script = funding_output_script(local_config, remote_config)
       t@@ -592,6 +590,7 @@ class Peer(Logger):
                    "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth),
                    "remote_update": None,
                    "state": channel_states.PREOPENING.name,
       +            "revocation_store": {},
                }
                chan = Channel(chan_dict,
                               sweep_address=self.lnworker.sweep_address,
       t@@ -645,7 +644,6 @@ class Peer(Logger):
                funding_idx = int.from_bytes(funding_created['funding_output_index'], 'big')
                funding_txid = bh2u(funding_created['funding_txid'][::-1])
                channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx)
       -        their_revocation_store = RevocationStore()
                remote_balance_sat = funding_sat * 1000 - push_msat
                remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') # TODO validate
                remote_reserve_sat = self.validate_remote_reserve(payload['channel_reserve_satoshis'], remote_dust_limit_sat, funding_sat)
       t@@ -669,12 +667,12 @@ class Peer(Logger):
                            htlc_minimum_msat=int.from_bytes(payload['htlc_minimum_msat'], 'big'), # TODO validate
                            next_per_commitment_point=payload['first_per_commitment_point'],
                            current_per_commitment_point=None,
       -                    revocation_store=their_revocation_store,
                        ),
                        "local_config": local_config,
                        "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth),
                        "remote_update": None,
                        "state": channel_states.PREOPENING.name,
       +                "revocation_store": {},
                }
                chan = Channel(chan_dict,
                               sweep_address=self.lnworker.sweep_address,
       t@@ -740,9 +738,8 @@ class Peer(Logger):
                    if oldest_unrevoked_remote_ctn == 0:
                        last_rev_secret = 0
                    else:
       -                revocation_store = chan.config[REMOTE].revocation_store
                        last_rev_index = oldest_unrevoked_remote_ctn - 1
       -                last_rev_secret = revocation_store.retrieve_secret(RevocationStore.START_INDEX - last_rev_index)
       +                last_rev_secret = chan.revocation_store.retrieve_secret(RevocationStore.START_INDEX - last_rev_index)
                    latest_secret, latest_point = chan.get_secret_and_point(LOCAL, latest_local_ctn)
                    self.send_message(
                        "channel_reestablish",
       t@@ -895,10 +892,8 @@ class Peer(Logger):
                if not chan.config[LOCAL].funding_locked_received:
                    our_next_point = chan.config[REMOTE].next_per_commitment_point
                    their_next_point = payload["next_per_commitment_point"]
       -            new_remote_state = chan.config[REMOTE]._replace(next_per_commitment_point=their_next_point)
       -            new_local_state = chan.config[LOCAL]._replace(funding_locked_received = True)
       -            chan.config[REMOTE]=new_remote_state
       -            chan.config[LOCAL]=new_local_state
       +            chan.config[REMOTE].next_per_commitment_point = their_next_point
       +            chan.config[LOCAL].funding_locked_received = True
                    self.lnworker.save_channel(chan)
                if chan.short_channel_id:
                    self.mark_open(chan)
       t@@ -913,9 +908,9 @@ class Peer(Logger):
                    # don't announce our channels
                    # FIXME should this be a field in chan.local_state maybe?
                    return
       -            chan.config[LOCAL]=chan.config[LOCAL]._replace(was_announced=True)
       -            coro = self.handle_announcements(chan)
       +            chan.config[LOCAL].was_announced = True
                    self.lnworker.save_channel(chan)
       +            coro = self.handle_announcements(chan)
                    asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
        
            @log_exceptions
   DIR diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py
       t@@ -293,7 +293,7 @@ def analyze_ctx(chan: 'Channel', ctx: Transaction):
                is_revocation = False
            elif ctn < oldest_unrevoked_remote_ctn:  # breach
                try:
       -            per_commitment_secret = their_conf.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
       +            per_commitment_secret = chan.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
                except UnableToDeriveSecret:
                    return
                their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -7,6 +7,7 @@ import json
        from collections import namedtuple
        from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
        import re
       +import attr
        
        from aiorpcx import NetAddress
        
       t@@ -37,55 +38,56 @@ LN_MAX_FUNDING_SAT = pow(2, 24) - 1
        def ln_dummy_address():
            return redeem_script_to_address('p2wsh', '')
        
       -class Keypair(NamedTuple):
       -    pubkey: bytes
       -    privkey: bytes
       -
       -
       -class OnlyPubkeyKeypair(NamedTuple):
       -    pubkey: bytes
       -
       -
       -# NamedTuples cannot subclass NamedTuples :'(   https://github.com/python/typing/issues/427
       -class LocalConfig(NamedTuple):
       -    # shared channel config fields (DUPLICATED code!!)
       -    payment_basepoint: 'Keypair'
       -    multisig_key: 'Keypair'
       -    htlc_basepoint: 'Keypair'
       -    delayed_basepoint: 'Keypair'
       -    revocation_basepoint: 'Keypair'
       -    to_self_delay: int
       -    dust_limit_sat: int
       -    max_htlc_value_in_flight_msat: int
       -    max_accepted_htlcs: int
       -    initial_msat: int
       -    reserve_sat: int
       -    # specific to "LOCAL" config
       -    per_commitment_secret_seed: bytes
       -    funding_locked_received: bool
       -    was_announced: bool
       -    current_commitment_signature: Optional[bytes]
       -    current_htlc_signatures: bytes
       -
       -
       -class RemoteConfig(NamedTuple):
       -    # shared channel config fields (DUPLICATED code!!)
       -    payment_basepoint: Union['Keypair', 'OnlyPubkeyKeypair']
       -    multisig_key: Union['Keypair', 'OnlyPubkeyKeypair']
       -    htlc_basepoint: Union['Keypair', 'OnlyPubkeyKeypair']
       -    delayed_basepoint: Union['Keypair', 'OnlyPubkeyKeypair']
       -    revocation_basepoint: Union['Keypair', 'OnlyPubkeyKeypair']
       -    to_self_delay: int
       -    dust_limit_sat: int
       -    max_htlc_value_in_flight_msat: int
       -    max_accepted_htlcs: int
       -    initial_msat: int
       -    reserve_sat: int
       -    # specific to "REMOTE" config
       -    htlc_minimum_msat: int
       -    next_per_commitment_point: bytes
       -    revocation_store: 'RevocationStore'
       -    current_per_commitment_point: Optional[bytes]
       +
       +class StoredAttr:
       +
       +    def to_json(self):
       +        return dict(vars(self))
       +
       +
       +@attr.s
       +class OnlyPubkeyKeypair(StoredAttr):
       +    pubkey = attr.ib(type=bytes)
       +
       +@attr.s
       +class Keypair(OnlyPubkeyKeypair):
       +    privkey = attr.ib(type=bytes)
       +
       +@attr.s
       +class Config(StoredAttr):
       +    # shared channel config fields
       +    payment_basepoint = attr.ib(type=OnlyPubkeyKeypair)
       +    multisig_key = attr.ib(type=OnlyPubkeyKeypair)
       +    htlc_basepoint = attr.ib(type=OnlyPubkeyKeypair)
       +    delayed_basepoint = attr.ib(type=OnlyPubkeyKeypair)
       +    revocation_basepoint = attr.ib(type=OnlyPubkeyKeypair)
       +    to_self_delay = attr.ib(type=int)
       +    dust_limit_sat = attr.ib(type=int)
       +    max_htlc_value_in_flight_msat = attr.ib(type=int)
       +    max_accepted_htlcs = attr.ib(type=int)
       +    initial_msat = attr.ib(type=int)
       +    reserve_sat = attr.ib(type=int)
       +
       +@attr.s
       +class LocalConfig(Config):
       +    per_commitment_secret_seed = attr.ib(type=bytes)
       +    funding_locked_received = attr.ib(type=bool)
       +    was_announced = attr.ib(type=bool)
       +    current_commitment_signature = attr.ib(type=bytes)
       +    current_htlc_signatures = attr.ib(type=bytes)
       +
       +@attr.s
       +class RemoteConfig(Config):
       +    htlc_minimum_msat = attr.ib(type=int)
       +    next_per_commitment_point = attr.ib(type=bytes)
       +    current_per_commitment_point = attr.ib(default=None, type=bytes)
       +
       +#@attr.s
       +#class FeeUpdate(StoredAttr):
       +#    rate = attr.ib(type=int)  # in sat/kw
       +#    ctn_local = attr.ib(default=None, type=int)
       +#    ctn_remote = attr.ib(default=None, type=int)
       +
        
        
        class FeeUpdate(NamedTuple):
       t@@ -187,9 +189,13 @@ class RevocationStore:
        
            START_INDEX = 2 ** 48 - 1
        
       -    def __init__(self):
       +    def __init__(self, storage):
                self.buckets = [None] * 49
                self.index = self.START_INDEX
       +        if storage:
       +            decode = lambda to_decode: ShachainElement(bfh(to_decode[0]), int(to_decode[1]))
       +            self.buckets = [k if k is None else decode(k) for k in storage["buckets"]]
       +            self.index = storage["index"]
        
            def add_next_entry(self, hsh):
                new_element = ShachainElement(index=self.index, secret=hsh)
       t@@ -197,7 +203,6 @@ class RevocationStore:
                for i in range(0, bucket):
                    this_bucket = self.buckets[i]
                    e = shachain_derive(new_element, this_bucket.index)
       -
                    if e != this_bucket:
                        raise Exception("hash is not derivable: {} {} {}".format(bh2u(e.secret), bh2u(this_bucket.secret), this_bucket.index))
                self.buckets[bucket] = new_element
       t@@ -218,14 +223,6 @@ class RevocationStore:
            def serialize(self):
                return {"index": self.index, "buckets": [[bh2u(k.secret), k.index] if k is not None else None for k in self.buckets]}
        
       -    @staticmethod
       -    def from_json_obj(decoded_json_obj):
       -        store = RevocationStore()
       -        decode = lambda to_decode: ShachainElement(bfh(to_decode[0]), int(to_decode[1]))
       -        store.buckets = [k if k is None else decode(k) for k in decoded_json_obj["buckets"]]
       -        store.index = decoded_json_obj["index"]
       -        return store
       -
            def __eq__(self, o):
                return type(o) is RevocationStore and self.serialize() == o.serialize()
        
   DIR diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py
       t@@ -45,7 +45,6 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
            assert local_amount > 0
            assert remote_amount > 0
            channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index)
       -    their_revocation_store = lnpeer.RevocationStore()
        
            return {
                    "channel_id":channel_id,
       t@@ -67,7 +66,6 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
        
                        next_per_commitment_point=nex,
                        current_per_commitment_point=cur,
       -                revocation_store=their_revocation_store,
                    ),
                    "local_config":lnpeer.LocalConfig(
                        payment_basepoint=privkeys[0],
       t@@ -96,6 +94,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
                    "node_id":other_node_id,
                    'onion_keys': {},
                    'state': 'PREOPENING',
       +            'revocation_store': {},
            }
        
        def bip32(sequence):
       t@@ -151,14 +150,16 @@ def create_test_channels(feerate=6000, local=None, remote=None):
            assert len(a_htlc_sigs) == 0
            assert len(b_htlc_sigs) == 0
        
       -    alice.config[LOCAL] = alice.config[LOCAL]._replace(current_commitment_signature=sig_from_bob)
       -    bob.config[LOCAL] = bob.config[LOCAL]._replace(current_commitment_signature=sig_from_alice)
       +    alice.config[LOCAL].current_commitment_signature = sig_from_bob
       +    bob.config[LOCAL].current_commitment_signature = sig_from_alice
        
            alice_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
            bob_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
        
       -    alice.config[REMOTE] = alice.config[REMOTE]._replace(next_per_commitment_point=bob_second, current_per_commitment_point=bob_first)
       -    bob.config[REMOTE] = bob.config[REMOTE]._replace(next_per_commitment_point=alice_second, current_per_commitment_point=alice_first)
       +    alice.config[REMOTE].next_per_commitment_point = bob_second
       +    alice.config[REMOTE].current_per_commitment_point = bob_first
       +    bob.config[REMOTE].next_per_commitment_point = alice_second
       +    bob.config[REMOTE].current_per_commitment_point = alice_first
        
            alice.hm.channel_open_finished()
            bob.hm.channel_open_finished()
       t@@ -663,15 +664,11 @@ class TestChanReserve(ElectrumTestCase):
                bob_min_reserve = 6 * one_bitcoin_in_msat // 1000
                # bob min reserve was decided by alice, but applies to bob
        
       -        alice_channel.config[LOCAL] =\
       -            alice_channel.config[LOCAL]._replace(reserve_sat=bob_min_reserve)
       -        alice_channel.config[REMOTE] =\
       -            alice_channel.config[REMOTE]._replace(reserve_sat=alice_min_reserve)
       +        alice_channel.config[LOCAL].reserve_sat = bob_min_reserve
       +        alice_channel.config[REMOTE].reserve_sat = alice_min_reserve
        
       -        bob_channel.config[LOCAL] =\
       -            bob_channel.config[LOCAL]._replace(reserve_sat=alice_min_reserve)
       -        bob_channel.config[REMOTE] =\
       -            bob_channel.config[REMOTE]._replace(reserve_sat=bob_min_reserve)
       +        bob_channel.config[LOCAL].reserve_sat = alice_min_reserve
       +        bob_channel.config[REMOTE].reserve_sat = bob_min_reserve
        
                self.alice_channel = alice_channel
                self.bob_channel = bob_channel
   DIR diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py
       t@@ -422,7 +422,7 @@ class TestLNUtil(ElectrumTestCase):
                ]
        
                for test in tests:
       -            receiver = RevocationStore()
       +            receiver = RevocationStore({})
                    for insert in test["inserts"]:
                        secret = bytes.fromhex(insert["secret"])
        
       t@@ -445,14 +445,14 @@ class TestLNUtil(ElectrumTestCase):
        
            def test_shachain_produce_consume(self):
                seed = bitcoin.sha256(b"shachaintest")
       -        consumer = RevocationStore()
       +        consumer = RevocationStore({})
                for i in range(10000):
                    secret = get_per_commitment_secret_from_seed(seed, RevocationStore.START_INDEX - i)
                    try:
                        consumer.add_next_entry(secret)
                    except Exception as e:
                        raise Exception("iteration " + str(i) + ": " + str(e))
       -            if i % 1000 == 0: self.assertEqual(consumer.serialize(), RevocationStore.from_json_obj(json.loads(json.dumps(consumer.serialize()))).serialize())
       +            if i % 1000 == 0: self.assertEqual(consumer.serialize(), RevocationStore(json.loads(json.dumps(consumer.serialize()))).serialize())
        
            def test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate(self):
                to_local_msat = 6988000000