Skip to content

Commit a540a1b

Browse files
Merge #4093
4093: Check reference at snapshot creation r=jordanschalm a=jordanschalm This PR extends #4086, adding checks to snapshot creation to differentiate between exceptions and unknown blocks in BlockSignerDecoder. - Updates `protocol.Snapshot` to guarantee that `ErrUnknownSnapshotReference` is returned in all cases where a snapshot's reference is unknown - Add existence checking methods to `storage/common`, `storage.Cache` - Updates BlockSignerDecoder to differentiate between errors from an unknown input block and exception - Misc: marks all unexpected errors in `storage/common` as exceptions - Misc: update `flow-emu` version to include onflow/flow-emulator#280 (avoid changes to `Headers` being breaking) Co-authored-by: Jordan Schalm <[email protected]>
2 parents 8080657 + d8940e1 commit a540a1b

File tree

28 files changed

+418
-102
lines changed

28 files changed

+418
-102
lines changed

access/validator.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import (
55
"fmt"
66

77
"github.com/onflow/flow-go/crypto"
8+
"github.com/onflow/flow-go/state"
89

910
"github.com/onflow/cadence/runtime/parser"
1011

1112
"github.com/onflow/flow-go/model/flow"
1213
"github.com/onflow/flow-go/state/protocol"
13-
"github.com/onflow/flow-go/storage"
1414
)
1515

1616
type Blocks interface {
@@ -29,7 +29,7 @@ func NewProtocolStateBlocks(state protocol.State) *ProtocolStateBlocks {
2929
func (b *ProtocolStateBlocks) HeaderByID(id flow.Identifier) (*flow.Header, error) {
3030
header, err := b.state.AtBlockID(id).Head()
3131
if err != nil {
32-
if errors.Is(err, storage.ErrNotFound) {
32+
if errors.Is(err, state.ErrUnknownSnapshotReference) {
3333
return nil, nil
3434
}
3535

consensus/hotstuff/committee.go

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,31 +25,31 @@ import (
2525
// So for validating votes/timeouts we use *ByEpoch methods.
2626
//
2727
// Since the voter committee is considered static over an epoch:
28-
// * we can query identities by view
29-
// * we don't need the full block ancestry prior to validating messages
28+
// - we can query identities by view
29+
// - we don't need the full block ancestry prior to validating messages
3030
type Replicas interface {
3131

3232
// LeaderForView returns the identity of the leader for a given view.
3333
// CAUTION: per liveness requirement of HotStuff, the leader must be fork-independent.
3434
// Therefore, a node retains its proposer view slots even if it is slashed.
3535
// Its proposal is simply considered invalid, as it is not from a legitimate participant.
3636
// Returns the following expected errors for invalid inputs:
37-
// * model.ErrViewForUnknownEpoch if no epoch containing the given view is known
37+
// - model.ErrViewForUnknownEpoch if no epoch containing the given view is known
3838
LeaderForView(view uint64) (flow.Identifier, error)
3939

4040
// QuorumThresholdForView returns the minimum total weight for a supermajority
4141
// at the given view. This weight threshold is computed using the total weight
4242
// of the initial committee and is static over the course of an epoch.
4343
// Returns the following expected errors for invalid inputs:
44-
// * model.ErrViewForUnknownEpoch if no epoch containing the given view is known
44+
// - model.ErrViewForUnknownEpoch if no epoch containing the given view is known
4545
QuorumThresholdForView(view uint64) (uint64, error)
4646

4747
// TimeoutThresholdForView returns the minimum total weight of observed timeout objects
4848
// required to safely timeout for the given view. This weight threshold is computed
4949
// using the total weight of the initial committee and is static over the course of
5050
// an epoch.
5151
// Returns the following expected errors for invalid inputs:
52-
// * model.ErrViewForUnknownEpoch if no epoch containing the given view is known
52+
// - model.ErrViewForUnknownEpoch if no epoch containing the given view is known
5353
TimeoutThresholdForView(view uint64) (uint64, error)
5454

5555
// Self returns our own node identifier.
@@ -60,23 +60,23 @@ type Replicas interface {
6060

6161
// DKG returns the DKG info for epoch given by the input view.
6262
// Returns the following expected errors for invalid inputs:
63-
// * model.ErrViewForUnknownEpoch if no epoch containing the given view is known
63+
// - model.ErrViewForUnknownEpoch if no epoch containing the given view is known
6464
DKG(view uint64) (DKG, error)
6565

6666
// IdentitiesByEpoch returns a list of the legitimate HotStuff participants for the epoch
6767
// given by the input view.
6868
// The returned list of HotStuff participants:
69-
// * contains nodes that are allowed to submit votes or timeouts within the given epoch
69+
// - contains nodes that are allowed to submit votes or timeouts within the given epoch
7070
// (un-ejected, non-zero weight at the beginning of the epoch)
71-
// * is ordered in the canonical order
72-
// * contains no duplicates.
71+
// - is ordered in the canonical order
72+
// - contains no duplicates.
7373
//
7474
// CAUTION: DO NOT use this method for validating block proposals.
7575
// CAUTION: This method considers epochs outside of Previous, Current, Next, w.r.t. the
7676
// finalized block, to be unknown. https://github.com/onflow/flow-go/issues/4085
7777
//
7878
// Returns the following expected errors for invalid inputs:
79-
// * model.ErrViewForUnknownEpoch if no epoch containing the given view is known
79+
// - model.ErrViewForUnknownEpoch if no epoch containing the given view is known
8080
//
8181
// TODO: should return identity skeleton https://github.com/dapperlabs/flow-go/issues/6232
8282
IdentitiesByEpoch(view uint64) (flow.IdentityList, error)
@@ -87,10 +87,10 @@ type Replicas interface {
8787
// finalized block, to be unknown. https://github.com/onflow/flow-go/issues/4085
8888
//
8989
// ERROR conditions:
90-
// * model.InvalidSignerError if participantID does NOT correspond to an authorized HotStuff participant at the specified block.
90+
// - model.InvalidSignerError if participantID does NOT correspond to an authorized HotStuff participant at the specified block.
9191
//
9292
// Returns the following expected errors for invalid inputs:
93-
// * model.ErrViewForUnknownEpoch if no epoch containing the given view is known
93+
// - model.ErrViewForUnknownEpoch if no epoch containing the given view is known
9494
//
9595
// TODO: should return identity skeleton https://github.com/dapperlabs/flow-go/issues/6232
9696
IdentityByEpoch(view uint64, participantID flow.Identifier) (*flow.Identity, error)
@@ -102,25 +102,27 @@ type Replicas interface {
102102
// For validating proposals, we use *ByBlock methods.
103103
//
104104
// Since the proposer committee can change at any block:
105-
// * we query by block ID
106-
// * we must have incorporated the full block ancestry prior to validating messages
105+
// - we query by block ID
106+
// - we must have incorporated the full block ancestry prior to validating messages
107107
type DynamicCommittee interface {
108108
Replicas
109109

110110
// IdentitiesByBlock returns a list of the legitimate HotStuff participants for the given block.
111111
// The returned list of HotStuff participants:
112-
// * contains nodes that are allowed to submit proposals, votes, and timeouts
112+
// - contains nodes that are allowed to submit proposals, votes, and timeouts
113113
// (un-ejected, non-zero weight at current block)
114-
// * is ordered in the canonical order
115-
// * contains no duplicates.
114+
// - is ordered in the canonical order
115+
// - contains no duplicates.
116116
//
117-
// No errors are expected during normal operation.
117+
// ERROR conditions:
118+
// - state.ErrUnknownSnapshotReference if the blockID is for an unknown block
118119
IdentitiesByBlock(blockID flow.Identifier) (flow.IdentityList, error)
119120

120121
// IdentityByBlock returns the full Identity for specified HotStuff participant.
121122
// The node must be a legitimate HotStuff participant with NON-ZERO WEIGHT at the specified block.
122123
// ERROR conditions:
123-
// * model.InvalidSignerError if participantID does NOT correspond to an authorized HotStuff participant at the specified block.
124+
// - model.InvalidSignerError if participantID does NOT correspond to an authorized HotStuff participant at the specified block.
125+
// - state.ErrUnknownSnapshotReference if the blockID is for an unknown block
124126
IdentityByBlock(blockID flow.Identifier, participantID flow.Identifier) (*flow.Identity, error)
125127
}
126128

@@ -132,8 +134,8 @@ type BlockSignerDecoder interface {
132134
// consensus committee has reached agreement on validity of parent block. Consequently, the
133135
// returned IdentifierList contains the consensus participants that signed the parent block.
134136
// Expected Error returns during normal operations:
135-
// - signature.InvalidSignerIndicesError if signer indices included in the header do
136-
// not encode a valid subset of the consensus committee
137+
// - signature.InvalidSignerIndicesError if signer indices included in the header do
138+
// not encode a valid subset of the consensus committee
137139
DecodeSignerIDs(header *flow.Header) (flow.IdentifierList, error)
138140
}
139141

consensus/hotstuff/committees/consensus_committee.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,22 +189,27 @@ func NewConsensusCommittee(state protocol.State, me flow.Identifier) (*Consensus
189189

190190
// IdentitiesByBlock returns the identities of all authorized consensus participants at the given block.
191191
// The order of the identities is the canonical order.
192-
// No errors are expected during normal operation.
192+
// ERROR conditions:
193+
// - state.ErrUnknownSnapshotReference if the blockID is for an unknown block
193194
func (c *Consensus) IdentitiesByBlock(blockID flow.Identifier) (flow.IdentityList, error) {
194195
il, err := c.state.AtBlockID(blockID).Identities(filter.IsVotingConsensusCommitteeMember)
195-
return il, err
196+
if err != nil {
197+
return nil, fmt.Errorf("could not identities at block %x: %w", blockID, err) // state.ErrUnknownSnapshotReference or exception
198+
}
199+
return il, nil
196200
}
197201

198202
// IdentityByBlock returns the identity of the node with the given node ID at the given block.
199203
// ERROR conditions:
200204
// - model.InvalidSignerError if participantID does NOT correspond to an authorized HotStuff participant at the specified block.
205+
// - state.ErrUnknownSnapshotReference if the blockID is for an unknown block
201206
func (c *Consensus) IdentityByBlock(blockID flow.Identifier, nodeID flow.Identifier) (*flow.Identity, error) {
202207
identity, err := c.state.AtBlockID(blockID).Identity(nodeID)
203208
if err != nil {
204209
if protocol.IsIdentityNotFound(err) {
205210
return nil, model.NewInvalidSignerErrorf("id %v is not a valid node id: %w", nodeID, err)
206211
}
207-
return nil, fmt.Errorf("could not get identity for node ID %x: %w", nodeID, err)
212+
return nil, fmt.Errorf("could not get identity for node ID %x: %w", nodeID, err) // state.ErrUnknownSnapshotReference or exception
208213
}
209214
if !filter.IsVotingConsensusCommitteeMember(identity) {
210215
return nil, model.NewInvalidSignerErrorf("node %v is not an authorized hotstuff voting participant", nodeID)

consensus/hotstuff/signature/block_signer_decoder.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var _ hotstuff.BlockSignerDecoder = (*BlockSignerDecoder)(nil)
2929
// Expected Error returns during normal operations:
3030
// - signature.InvalidSignerIndicesError if signer indices included in the header do
3131
// not encode a valid subset of the consensus committee
32+
// - state.ErrUnknownSnapshotReference if the input header is not a known incorporated block.
3233
func (b *BlockSignerDecoder) DecodeSignerIDs(header *flow.Header) (flow.IdentifierList, error) {
3334
// root block does not have signer indices
3435
if header.ParentVoterIndices == nil && header.View == 0 {
@@ -41,10 +42,12 @@ func (b *BlockSignerDecoder) DecodeSignerIDs(header *flow.Header) (flow.Identifi
4142
if errors.Is(err, model.ErrViewForUnknownEpoch) {
4243
// possibly, we request epoch which is far behind in the past, in this case we won't have it in cache.
4344
// try asking by parent ID
45+
// TODO: this assumes no identity table changes within epochs, must be changed for Dynamic Protocol State
46+
// See https://github.com/onflow/flow-go/issues/4085
4447
members, err = b.IdentitiesByBlock(header.ParentID)
4548
if err != nil {
4649
return nil, fmt.Errorf("could not retrieve identities for block %x with QC view %d for parent %x: %w",
47-
header.ID(), header.ParentView, header.ParentID, err)
50+
header.ID(), header.ParentView, header.ParentID, err) // state.ErrUnknownSnapshotReference or exception
4851
}
4952
} else {
5053
return nil, fmt.Errorf("unexpected error retrieving identities for block %v: %w", header.ID(), err)

consensus/hotstuff/signature/block_signer_decoder_test.go

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package signature
22

33
import (
44
"errors"
5-
"fmt"
65
"testing"
76

87
"github.com/stretchr/testify/mock"
@@ -14,6 +13,7 @@ import (
1413
"github.com/onflow/flow-go/model/flow"
1514
"github.com/onflow/flow-go/model/flow/order"
1615
"github.com/onflow/flow-go/module/signature"
16+
"github.com/onflow/flow-go/state"
1717
"github.com/onflow/flow-go/utils/unittest"
1818
)
1919

@@ -65,31 +65,57 @@ func (s *blockSignerDecoderSuite) Test_RootBlock() {
6565
require.Empty(s.T(), ids)
6666
}
6767

68-
// Test_UnexpectedCommitteeException verifies that `BlockSignerDecoder`
68+
// Test_CommitteeException verifies that `BlockSignerDecoder`
6969
// does _not_ erroneously interpret an unexpected exception from the committee as
7070
// a sign of an unknown block, i.e. the decoder should _not_ return an `model.ErrViewForUnknownEpoch` or `signature.InvalidSignerIndicesError`
71-
func (s *blockSignerDecoderSuite) Test_UnexpectedCommitteeException() {
72-
exception := errors.New("unexpected exception")
71+
func (s *blockSignerDecoderSuite) Test_CommitteeException() {
72+
s.Run("ByEpoch exception", func() {
73+
exception := errors.New("unexpected exception")
74+
*s.committee = *hotstuff.NewDynamicCommittee(s.T())
75+
s.committee.On("IdentitiesByEpoch", mock.Anything).Return(nil, exception)
76+
77+
ids, err := s.decoder.DecodeSignerIDs(s.block.Header)
78+
require.Empty(s.T(), ids)
79+
require.NotErrorIs(s.T(), err, model.ErrViewForUnknownEpoch)
80+
require.False(s.T(), signature.IsInvalidSignerIndicesError(err))
81+
require.ErrorIs(s.T(), err, exception)
82+
})
83+
s.Run("ByBlock exception", func() {
84+
exception := errors.New("unexpected exception")
85+
*s.committee = *hotstuff.NewDynamicCommittee(s.T())
86+
s.committee.On("IdentitiesByEpoch", mock.Anything).Return(nil, model.ErrViewForUnknownEpoch)
87+
s.committee.On("IdentitiesByBlock", mock.Anything).Return(nil, exception)
88+
89+
ids, err := s.decoder.DecodeSignerIDs(s.block.Header)
90+
require.Empty(s.T(), ids)
91+
require.NotErrorIs(s.T(), err, model.ErrViewForUnknownEpoch)
92+
require.False(s.T(), signature.IsInvalidSignerIndicesError(err))
93+
require.ErrorIs(s.T(), err, exception)
94+
})
95+
}
96+
97+
// Test_UnknownEpoch_KnownBlock tests handling of a block from an un-cached epoch but
98+
// where the block is known - should return identities for block.
99+
func (s *blockSignerDecoderSuite) Test_UnknownEpoch_KnownBlock() {
73100
*s.committee = *hotstuff.NewDynamicCommittee(s.T())
74-
s.committee.On("IdentitiesByEpoch", mock.Anything).Return(nil, exception)
101+
s.committee.On("IdentitiesByEpoch", s.block.Header.ParentView).Return(nil, model.ErrViewForUnknownEpoch)
102+
s.committee.On("IdentitiesByBlock", s.block.Header.ParentID).Return(s.allConsensus, nil)
75103

76104
ids, err := s.decoder.DecodeSignerIDs(s.block.Header)
77-
require.Empty(s.T(), ids)
78-
require.NotErrorIs(s.T(), err, model.ErrViewForUnknownEpoch)
79-
require.False(s.T(), signature.IsInvalidSignerIndicesError(err))
80-
require.True(s.T(), errors.Is(err, exception))
105+
require.NoError(s.T(), err)
106+
require.Equal(s.T(), s.allConsensus.NodeIDs(), ids)
81107
}
82108

83-
// Test_UnknownEpoch tests handling of a block from an unknown epoch.
84-
// It should propagate the sentinel error model.ErrViewForUnknownEpoch from Committee.
85-
func (s *blockSignerDecoderSuite) Test_UnknownEpoch() {
109+
// Test_UnknownEpoch_UnknownBlock tests handling of a block from an un-cached epoch
110+
// where the block is unknown - should propagate state.ErrUnknownSnapshotReference.
111+
func (s *blockSignerDecoderSuite) Test_UnknownEpoch_UnknownBlock() {
86112
*s.committee = *hotstuff.NewDynamicCommittee(s.T())
87113
s.committee.On("IdentitiesByEpoch", s.block.Header.ParentView).Return(nil, model.ErrViewForUnknownEpoch)
88-
s.committee.On("IdentitiesByBlock", s.block.Header.ParentID).Return(nil, fmt.Errorf(""))
114+
s.committee.On("IdentitiesByBlock", s.block.Header.ParentID).Return(nil, state.ErrUnknownSnapshotReference)
89115

90116
ids, err := s.decoder.DecodeSignerIDs(s.block.Header)
117+
require.ErrorIs(s.T(), err, state.ErrUnknownSnapshotReference)
91118
require.Empty(s.T(), ids)
92-
require.Error(s.T(), err)
93119
}
94120

95121
// Test_InvalidIndices verifies that `BlockSignerDecoder` returns

engine/consensus/ingestion/core.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/onflow/flow-go/module/metrics"
1919
"github.com/onflow/flow-go/module/signature"
2020
"github.com/onflow/flow-go/module/trace"
21+
"github.com/onflow/flow-go/state"
2122
"github.com/onflow/flow-go/state/protocol"
2223
"github.com/onflow/flow-go/storage"
2324
)
@@ -158,7 +159,7 @@ func (e *Core) validateGuarantors(guarantee *flow.CollectionGuarantee) error {
158159
snapshot := e.state.AtBlockID(guarantee.ReferenceBlockID)
159160
cluster, err := snapshot.Epochs().Current().ClusterByChainID(guarantee.ChainID)
160161
// reference block not found
161-
if errors.Is(err, storage.ErrNotFound) {
162+
if errors.Is(err, state.ErrUnknownSnapshotReference) {
162163
return engine.NewUnverifiableInputError(
163164
"could not get clusters with chainID %v for unknown reference block (id=%x): %w", guarantee.ChainID, guarantee.ReferenceBlockID, err)
164165
}
@@ -212,7 +213,7 @@ func (e *Core) validateOrigin(originID flow.Identifier, guarantee *flow.Collecti
212213
valid, err := protocol.IsNodeAuthorizedWithRoleAt(refState, originID, flow.RoleCollection)
213214
if err != nil {
214215
// collection with an unknown reference block is unverifiable
215-
if errors.Is(err, storage.ErrNotFound) {
216+
if errors.Is(err, state.ErrUnknownSnapshotReference) {
216217
return engine.NewUnverifiableInputError("could not get origin (id=%x) for unknown reference block (id=%x): %w", originID, guarantee.ReferenceBlockID, err)
217218
}
218219
return fmt.Errorf("unexpected error checking collection origin %x at reference block %x: %w", originID, guarantee.ReferenceBlockID, err)

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ require (
136136
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de // indirect
137137
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect
138138
github.com/docker/go-units v0.5.0 // indirect
139-
github.com/dustin/go-humanize v1.0.0 // indirect
139+
github.com/dustin/go-humanize v1.0.1 // indirect
140140
github.com/elastic/gosigar v0.14.2 // indirect
141141
github.com/felixge/fgprof v0.9.3 // indirect
142142
github.com/flynn/noise v1.0.0 // indirect
@@ -207,7 +207,7 @@ require (
207207
github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect
208208
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
209209
github.com/mattn/go-colorable v0.1.13 // indirect
210-
github.com/mattn/go-isatty v0.0.16 // indirect
210+
github.com/mattn/go-isatty v0.0.17 // indirect
211211
github.com/mattn/go-pointer v0.0.1 // indirect
212212
github.com/mattn/go-runewidth v0.0.13 // indirect
213213
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,9 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
294294
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
295295
github.com/dop251/goja v0.0.0-20200219165308-d1232e640a87/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
296296
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
297-
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
298297
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
298+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
299+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
299300
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
300301
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
301302
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@@ -1069,8 +1070,9 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
10691070
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
10701071
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
10711072
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
1072-
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
10731073
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
1074+
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
1075+
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
10741076
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
10751077
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
10761078
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=

0 commit comments

Comments
 (0)