Merge pull request #10182 from GeorgeTsagk/aux-feature-bits
Aux feature bits
This commit is contained in:
11 files changed
+225
-4
No files matched your search
@@ -213,6 +213,11 @@ type AuxComponents struct {
|
|||||||
// AuxContractResolver is an optional interface that can be used to
|
// AuxContractResolver is an optional interface that can be used to
|
||||||
// modify the way contracts are resolved.
|
// modify the way contracts are resolved.
|
||||||
AuxContractResolver fn.Option[lnwallet.AuxContractResolver]
|
AuxContractResolver fn.Option[lnwallet.AuxContractResolver]
|
||||||
|
|
||||||
|
// AuxChannelNegotiator is an optional interface that allows aux channel
|
||||||
|
// implementations to inject and process custom records over channel
|
||||||
|
// related wire messages.
|
||||||
|
AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator]
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultWalletImpl is the default implementation of our normal, btcwallet
|
// DefaultWalletImpl is the default implementation of our normal, btcwallet
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ a certain amount of msats.
|
|||||||
[allow](https://github.com/lightningnetwork/lnd/pull/10087)
|
[allow](https://github.com/lightningnetwork/lnd/pull/10087)
|
||||||
`conf_target=1`. Previously they required `conf_target >= 2`.
|
`conf_target=1`. Previously they required `conf_target >= 2`.
|
||||||
|
|
||||||
|
* A new AuxComponent was added named AuxChannelNegotiator. This component aids
|
||||||
|
with custom data communication for aux channels, by injecting and handling
|
||||||
|
data in channel related wire messages. See
|
||||||
|
[PR](https://github.com/lightningnetwork/lnd/pull/10182) for more info.
|
||||||
|
|
||||||
## RPC Additions
|
## RPC Additions
|
||||||
* When querying [`ForwardingEvents`](https://github.com/lightningnetwork/lnd/pull/9813)
|
* When querying [`ForwardingEvents`](https://github.com/lightningnetwork/lnd/pull/9813)
|
||||||
logs, the response now include the incoming and outgoing htlc indices of the payment
|
logs, the response now include the incoming and outgoing htlc indices of the payment
|
||||||
|
|||||||
@@ -568,6 +568,11 @@ type Config struct {
|
|||||||
// AuxResolver is an optional interface that can be used to modify the
|
// AuxResolver is an optional interface that can be used to modify the
|
||||||
// way contracts are resolved.
|
// way contracts are resolved.
|
||||||
AuxResolver fn.Option[lnwallet.AuxContractResolver]
|
AuxResolver fn.Option[lnwallet.AuxContractResolver]
|
||||||
|
|
||||||
|
// AuxChannelNegotiator is an optional interface that allows aux channel
|
||||||
|
// implementations to inject and process custom records over channel
|
||||||
|
// related wire messages.
|
||||||
|
AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manager acts as an orchestrator/bridge between the wallet's
|
// Manager acts as an orchestrator/bridge between the wallet's
|
||||||
@@ -4019,6 +4024,14 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen
|
|||||||
|
|
||||||
defer f.wg.Done()
|
defer f.wg.Done()
|
||||||
|
|
||||||
|
// Notify the aux hook that the specified peer just established a
|
||||||
|
// channel with us, identified by the given channel ID.
|
||||||
|
f.cfg.AuxChannelNegotiator.WhenSome(
|
||||||
|
func(acn lnwallet.AuxChannelNegotiator) {
|
||||||
|
acn.ProcessChannelReady(msg.ChanID, peer.PubKey())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// If we are in development mode, we'll wait for specified duration
|
// If we are in development mode, we'll wait for specified duration
|
||||||
// before processing the channel ready message.
|
// before processing the channel ready message.
|
||||||
if f.cfg.Dev != nil {
|
if f.cfg.Dev != nil {
|
||||||
|
|||||||
@@ -298,6 +298,11 @@ type ChannelLinkConfig struct {
|
|||||||
// used to manage the bandwidth of the link.
|
// used to manage the bandwidth of the link.
|
||||||
AuxTrafficShaper fn.Option[AuxTrafficShaper]
|
AuxTrafficShaper fn.Option[AuxTrafficShaper]
|
||||||
|
|
||||||
|
// AuxChannelNegotiator is an optional interface that allows aux channel
|
||||||
|
// implementations to inject and process custom records over channel
|
||||||
|
// related wire messages.
|
||||||
|
AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator]
|
||||||
|
|
||||||
// QuiescenceTimeout is the max duration that the channel can be
|
// QuiescenceTimeout is the max duration that the channel can be
|
||||||
// quiesced. Any dependent protocols (dynamic commitments, splicing,
|
// quiesced. Any dependent protocols (dynamic commitments, splicing,
|
||||||
// etc.) must finish their operations under this timeout value,
|
// etc.) must finish their operations under this timeout value,
|
||||||
@@ -987,6 +992,22 @@ func (l *channelLink) syncChanStates(ctx context.Context) error {
|
|||||||
// In any case, we'll then process their ChanSync message.
|
// In any case, we'll then process their ChanSync message.
|
||||||
l.log.Info("received re-establishment message from remote side")
|
l.log.Info("received re-establishment message from remote side")
|
||||||
|
|
||||||
|
// If we have an AuxChannelNegotiator we notify any external
|
||||||
|
// component for this message. This serves as a notification
|
||||||
|
// that the reestablish message was received.
|
||||||
|
l.cfg.AuxChannelNegotiator.WhenSome(
|
||||||
|
func(acn lnwallet.AuxChannelNegotiator) {
|
||||||
|
fundingPoint := l.channel.ChannelPoint()
|
||||||
|
cid := lnwire.NewChanIDFromOutPoint(
|
||||||
|
fundingPoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
acn.ProcessReestablish(
|
||||||
|
cid, l.cfg.Peer.PubKey(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
openedCircuits []CircuitKey
|
openedCircuits []CircuitKey
|
||||||
closedCircuits []CircuitKey
|
closedCircuits []CircuitKey
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
"github.com/lightningnetwork/lnd/tlv"
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -97,6 +98,10 @@ type AuxChanState struct {
|
|||||||
// funding output.
|
// funding output.
|
||||||
TapscriptRoot fn.Option[chainhash.Hash]
|
TapscriptRoot fn.Option[chainhash.Hash]
|
||||||
|
|
||||||
|
// PeerPubKey is the peer pub key of the peer we've established this
|
||||||
|
// channel with.
|
||||||
|
PeerPubKey route.Vertex
|
||||||
|
|
||||||
// CustomBlob is an optional blob that can be used to store information
|
// CustomBlob is an optional blob that can be used to store information
|
||||||
// specific to a custom channel type. This information is only created
|
// specific to a custom channel type. This information is only created
|
||||||
// at channel funding time, and after wards is to be considered
|
// at channel funding time, and after wards is to be considered
|
||||||
@@ -106,6 +111,8 @@ type AuxChanState struct {
|
|||||||
|
|
||||||
// NewAuxChanState creates a new AuxChanState from the given channel state.
|
// NewAuxChanState creates a new AuxChanState from the given channel state.
|
||||||
func NewAuxChanState(chanState *channeldb.OpenChannel) AuxChanState {
|
func NewAuxChanState(chanState *channeldb.OpenChannel) AuxChanState {
|
||||||
|
peerPub := chanState.IdentityPub.SerializeCompressed()
|
||||||
|
|
||||||
return AuxChanState{
|
return AuxChanState{
|
||||||
ChanType: chanState.ChanType,
|
ChanType: chanState.ChanType,
|
||||||
FundingOutpoint: chanState.FundingOutpoint,
|
FundingOutpoint: chanState.FundingOutpoint,
|
||||||
@@ -116,6 +123,7 @@ func NewAuxChanState(chanState *channeldb.OpenChannel) AuxChanState {
|
|||||||
RemoteChanCfg: chanState.RemoteChanCfg,
|
RemoteChanCfg: chanState.RemoteChanCfg,
|
||||||
ThawHeight: chanState.ThawHeight,
|
ThawHeight: chanState.ThawHeight,
|
||||||
TapscriptRoot: chanState.TapscriptRoot,
|
TapscriptRoot: chanState.TapscriptRoot,
|
||||||
|
PeerPubKey: route.Vertex(peerPub),
|
||||||
CustomBlob: chanState.CustomBlob,
|
CustomBlob: chanState.CustomBlob,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package lnwallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuxChannelNegotiator is an interface that allows aux channel implementations
|
||||||
|
// to inject or handle custom records in the init message that is used when
|
||||||
|
// establishing a connection with a peer. It may also notify the aux channel
|
||||||
|
// implementation for the channel ready or channel reestablish events, which
|
||||||
|
// mark the channel as ready to use.
|
||||||
|
type AuxChannelNegotiator interface {
|
||||||
|
// GetInitRecords is called when sending an init message to a peer.
|
||||||
|
// It returns custom records to include in the init message TLVs. The
|
||||||
|
// implementation can decide which records to include based on the peer
|
||||||
|
// identity.
|
||||||
|
GetInitRecords(peer route.Vertex) (lnwire.CustomRecords, error)
|
||||||
|
|
||||||
|
// ProcessInitRecords handles received init records from a peer. The
|
||||||
|
// implementation can store state internally to affect future
|
||||||
|
// channel operations with this peer.
|
||||||
|
ProcessInitRecords(peer route.Vertex,
|
||||||
|
customRecords lnwire.CustomRecords) error
|
||||||
|
|
||||||
|
// ProcessChannelReady handles the event of marking a channel identified
|
||||||
|
// by its channel ID as ready to use. We also provide the peer the
|
||||||
|
// channel was established with.
|
||||||
|
ProcessChannelReady(cid lnwire.ChannelID, peer route.Vertex)
|
||||||
|
|
||||||
|
// ProcessReestablish handles the received channel_reestablish message
|
||||||
|
// which marks a channel identified by its cid as ready to use again.
|
||||||
|
ProcessReestablish(cid lnwire.ChannelID, peer route.Vertex)
|
||||||
|
}
|
||||||
+29
-4
@@ -24,6 +24,10 @@ type Init struct {
|
|||||||
// Features field.
|
// Features field.
|
||||||
Features *RawFeatureVector
|
Features *RawFeatureVector
|
||||||
|
|
||||||
|
// CustomRecords maps TLV types to byte slices, storing arbitrary data
|
||||||
|
// intended for inclusion in the ExtraData field of the init message.
|
||||||
|
CustomRecords CustomRecords
|
||||||
|
|
||||||
// ExtraData is the set of data that was appended to this message to
|
// ExtraData is the set of data that was appended to this message to
|
||||||
// fill out the full maximum transport message size. These fields can
|
// fill out the full maximum transport message size. These fields can
|
||||||
// be used to specify optional data such as custom TLV fields.
|
// be used to specify optional data such as custom TLV fields.
|
||||||
@@ -35,7 +39,6 @@ func NewInitMessage(gf *RawFeatureVector, f *RawFeatureVector) *Init {
|
|||||||
return &Init{
|
return &Init{
|
||||||
GlobalFeatures: gf,
|
GlobalFeatures: gf,
|
||||||
Features: f,
|
Features: f,
|
||||||
ExtraData: make([]byte, 0),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +55,28 @@ var _ SizeableMessage = (*Init)(nil)
|
|||||||
//
|
//
|
||||||
// This is part of the lnwire.Message interface.
|
// This is part of the lnwire.Message interface.
|
||||||
func (msg *Init) Decode(r io.Reader, pver uint32) error {
|
func (msg *Init) Decode(r io.Reader, pver uint32) error {
|
||||||
return ReadElements(r,
|
var msgExtraData ExtraOpaqueData
|
||||||
|
|
||||||
|
err := ReadElements(r,
|
||||||
&msg.GlobalFeatures,
|
&msg.GlobalFeatures,
|
||||||
&msg.Features,
|
&msg.Features,
|
||||||
&msg.ExtraData,
|
&msgExtraData,
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
customRecords, _, extraDData, err := ParseAndExtractCustomRecords(
|
||||||
|
msgExtraData,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.CustomRecords = customRecords
|
||||||
|
msg.ExtraData = extraDData
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode serializes the target Init into the passed io.Writer observing
|
// Encode serializes the target Init into the passed io.Writer observing
|
||||||
@@ -72,7 +92,12 @@ func (msg *Init) Encode(w *bytes.Buffer, pver uint32) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return WriteBytes(w, msg.ExtraData)
|
extraData, err := MergeAndEncode(nil, msg.ExtraData, msg.CustomRecords)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return WriteBytes(w, extraData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MsgType returns the integer uniquely identifying this message type on the
|
// MsgType returns the integer uniquely identifying this message type on the
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package lnwire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestInitEncodeDecode checks that we can encode and decode an Init message
|
||||||
|
// to and from a byte stream.
|
||||||
|
func TestInitEncodeDecode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// These are the raw bytes that we expect to be generated from the
|
||||||
|
// sample Init message.
|
||||||
|
rawBytes := []byte{
|
||||||
|
// GlobalFeatures
|
||||||
|
0x00, 0x01, 0xc0,
|
||||||
|
|
||||||
|
// Features
|
||||||
|
0x00, 0x01, 0xc0,
|
||||||
|
|
||||||
|
// ExtraData - unknown odd-type TLV record.
|
||||||
|
0x6f, // type (111)
|
||||||
|
0x02, // length
|
||||||
|
0x79, 0x79, // value
|
||||||
|
|
||||||
|
// ExtraData - custom TLV record.
|
||||||
|
// TLV record for type 67676
|
||||||
|
0xfe, 0x00, 0x01, 0x08, 0x6c, // type (67676)
|
||||||
|
0x05, // length
|
||||||
|
0x01, 0x02, 0x03, 0x04, 0x05, // value
|
||||||
|
|
||||||
|
// ExtraData - custom TLV record.
|
||||||
|
// TLV record for type 67777
|
||||||
|
0xfe, 0x00, 0x01, 0x08, 0xc1, // type (67777)
|
||||||
|
0x03, // length
|
||||||
|
0x01, 0x02, 0x03, // value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new empty message and decode the raw bytes into it.
|
||||||
|
msg := &Init{}
|
||||||
|
r := bytes.NewReader(rawBytes)
|
||||||
|
err := msg.Decode(r, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NotNil(t, msg.GlobalFeatures)
|
||||||
|
require.NotNil(t, msg.Features)
|
||||||
|
require.NotNil(t, msg.CustomRecords)
|
||||||
|
require.NotNil(t, msg.ExtraData)
|
||||||
|
|
||||||
|
// Next, encode the message back into a new byte buffer.
|
||||||
|
var b bytes.Buffer
|
||||||
|
err = msg.Encode(&b, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The re-encoded bytes should be exactly the same as the original raw
|
||||||
|
// bytes.
|
||||||
|
require.Equal(t, rawBytes, b.Bytes())
|
||||||
|
}
|
||||||
@@ -1188,6 +1188,10 @@ func (msg *Init) RandTestMessage(t *rapid.T) Message {
|
|||||||
local.Set(bit)
|
local.Set(bit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ignoreRecords := fn.NewSet[uint64]()
|
||||||
|
|
||||||
|
msg.ExtraData = RandExtraOpaqueData(t, ignoreRecords)
|
||||||
|
|
||||||
return NewInitMessage(global, local)
|
return NewInitMessage(global, local)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -457,6 +457,11 @@ type Config struct {
|
|||||||
// used to modify the way the co-op close transaction is constructed.
|
// used to modify the way the co-op close transaction is constructed.
|
||||||
AuxChanCloser fn.Option[chancloser.AuxChanCloser]
|
AuxChanCloser fn.Option[chancloser.AuxChanCloser]
|
||||||
|
|
||||||
|
// AuxChannelNegotiator is an optional interface that allows aux channel
|
||||||
|
// implementations to inject and process custom records over channel
|
||||||
|
// related wire messages.
|
||||||
|
AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator]
|
||||||
|
|
||||||
// ShouldFwdExpEndorsement is a closure that indicates whether
|
// ShouldFwdExpEndorsement is a closure that indicates whether
|
||||||
// experimental endorsement signals should be set.
|
// experimental endorsement signals should be set.
|
||||||
ShouldFwdExpEndorsement func() bool
|
ShouldFwdExpEndorsement func() bool
|
||||||
@@ -1456,6 +1461,7 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint,
|
|||||||
DisallowQuiescence: p.cfg.DisallowQuiescence ||
|
DisallowQuiescence: p.cfg.DisallowQuiescence ||
|
||||||
!p.remoteFeatures.HasFeature(lnwire.QuiescenceOptional),
|
!p.remoteFeatures.HasFeature(lnwire.QuiescenceOptional),
|
||||||
AuxTrafficShaper: p.cfg.AuxTrafficShaper,
|
AuxTrafficShaper: p.cfg.AuxTrafficShaper,
|
||||||
|
AuxChannelNegotiator: p.cfg.AuxChannelNegotiator,
|
||||||
QuiescenceTimeout: p.cfg.QuiescenceTimeout,
|
QuiescenceTimeout: p.cfg.QuiescenceTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4545,6 +4551,19 @@ func (p *Brontide) handleInitMsg(msg *lnwire.Init) error {
|
|||||||
return fmt.Errorf("data loss protection required")
|
return fmt.Errorf("data loss protection required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have an AuxChannelNegotiator and the peer sent aux features,
|
||||||
|
// process them.
|
||||||
|
p.cfg.AuxChannelNegotiator.WhenSome(
|
||||||
|
func(acn lnwallet.AuxChannelNegotiator) {
|
||||||
|
err = acn.ProcessInitRecords(
|
||||||
|
p.cfg.PubKeyBytes, msg.CustomRecords.Copy(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not process init records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4605,6 +4624,30 @@ func (p *Brontide) sendInitMsg(legacyChan bool) error {
|
|||||||
features.RawFeatureVector,
|
features.RawFeatureVector,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// If we have an AuxChannelNegotiator, get custom feature bits to
|
||||||
|
// include in the init message.
|
||||||
|
p.cfg.AuxChannelNegotiator.WhenSome(
|
||||||
|
func(negotiator lnwallet.AuxChannelNegotiator) {
|
||||||
|
var auxRecords lnwire.CustomRecords
|
||||||
|
auxRecords, err = negotiator.GetInitRecords(
|
||||||
|
p.cfg.PubKeyBytes,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Warnf("Failed to get aux init features: "+
|
||||||
|
"%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedRecs := msg.CustomRecords.MergedCopy(auxRecords)
|
||||||
|
msg.CustomRecords = mergedRecs
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return p.writeMessage(msg)
|
return p.writeMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1628,6 +1628,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
|
|||||||
AuxFundingController: implCfg.AuxFundingController,
|
AuxFundingController: implCfg.AuxFundingController,
|
||||||
AuxSigner: implCfg.AuxSigner,
|
AuxSigner: implCfg.AuxSigner,
|
||||||
AuxResolver: implCfg.AuxContractResolver,
|
AuxResolver: implCfg.AuxContractResolver,
|
||||||
|
AuxChannelNegotiator: implCfg.AuxChannelNegotiator,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -4431,6 +4432,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
|
|||||||
AuxChanCloser: s.implCfg.AuxChanCloser,
|
AuxChanCloser: s.implCfg.AuxChanCloser,
|
||||||
AuxResolver: s.implCfg.AuxContractResolver,
|
AuxResolver: s.implCfg.AuxContractResolver,
|
||||||
AuxTrafficShaper: s.implCfg.TrafficShaper,
|
AuxTrafficShaper: s.implCfg.TrafficShaper,
|
||||||
|
AuxChannelNegotiator: s.implCfg.AuxChannelNegotiator,
|
||||||
ShouldFwdExpEndorsement: func() bool {
|
ShouldFwdExpEndorsement: func() bool {
|
||||||
if s.cfg.ProtocolOptions.NoExperimentalEndorsement() {
|
if s.cfg.ProtocolOptions.NoExperimentalEndorsement() {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in new issue
Block a user