Skip to content

Commit b3c1eda

Browse files
fix: Migrate legacy addressbook from Chainlink
Files migrated from chainlink repo - address_book.go - address_book_test.go - address_book_labels.go -> renamed to labels - address_book_labels_test.go JIRA: https://smartcontract-it.atlassian.net/browse/CLD-149
1 parent 46c12eb commit b3c1eda

File tree

6 files changed

+1223
-1
lines changed

6 files changed

+1223
-1
lines changed

addressbook/address_book.go

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
package addressbook
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"sync"
7+
8+
"golang.org/x/exp/maps"
9+
10+
"github.com/Masterminds/semver/v3"
11+
"github.com/ethereum/go-ethereum/common"
12+
"github.com/pkg/errors"
13+
chainsel "github.com/smartcontractkit/chain-selectors"
14+
)
15+
16+
var (
17+
ErrInvalidChainSelector = errors.New("invalid chain selector")
18+
ErrInvalidAddress = errors.New("invalid address")
19+
ErrChainNotFound = errors.New("chain not found")
20+
)
21+
22+
// ContractType is a simple string type for identifying contract types.
23+
type ContractType string
24+
25+
func (ct ContractType) String() string {
26+
return string(ct)
27+
}
28+
29+
var (
30+
Version0_5_0 = *semver.MustParse("0.5.0")
31+
Version1_0_0 = *semver.MustParse("1.0.0")
32+
Version1_1_0 = *semver.MustParse("1.1.0")
33+
Version1_2_0 = *semver.MustParse("1.2.0")
34+
Version1_5_0 = *semver.MustParse("1.5.0")
35+
Version1_5_1 = *semver.MustParse("1.5.1")
36+
Version1_6_0 = *semver.MustParse("1.6.0")
37+
)
38+
39+
type TypeAndVersion struct {
40+
Type ContractType `json:"Type"`
41+
Version semver.Version `json:"Version"`
42+
Labels LabelSet `json:"Labels,omitempty"`
43+
}
44+
45+
func (tv TypeAndVersion) String() string {
46+
if len(tv.Labels) == 0 {
47+
return fmt.Sprintf("%s %s", tv.Type, tv.Version.String())
48+
}
49+
50+
// Use the LabelSet's String method for sorted labels
51+
sortedLabels := tv.Labels.String()
52+
return fmt.Sprintf("%s %s %s",
53+
tv.Type,
54+
tv.Version.String(),
55+
sortedLabels,
56+
)
57+
}
58+
59+
func (tv TypeAndVersion) Equal(other TypeAndVersion) bool {
60+
// Compare Type
61+
if tv.Type != other.Type {
62+
return false
63+
}
64+
// Compare Versions
65+
if !tv.Version.Equal(&other.Version) {
66+
return false
67+
}
68+
// Compare Labels
69+
return tv.Labels.Equal(other.Labels)
70+
}
71+
72+
func MustTypeAndVersionFromString(s string) TypeAndVersion {
73+
tv, err := TypeAndVersionFromString(s)
74+
if err != nil {
75+
panic(err)
76+
}
77+
return tv
78+
}
79+
80+
// Note this will become useful for validation. When we want
81+
// to assert an onchain call to typeAndVersion yields whats expected.
82+
func TypeAndVersionFromString(s string) (TypeAndVersion, error) {
83+
parts := strings.Fields(s) // Ignores consecutive spaces
84+
if len(parts) < 2 {
85+
return TypeAndVersion{}, fmt.Errorf("invalid type and version string: %s", s)
86+
}
87+
v, err := semver.NewVersion(parts[1])
88+
if err != nil {
89+
return TypeAndVersion{}, err
90+
}
91+
labels := make(LabelSet)
92+
if len(parts) > 2 {
93+
labels = NewLabelSet(parts[2:]...)
94+
}
95+
return TypeAndVersion{
96+
Type: ContractType(parts[0]),
97+
Version: *v,
98+
Labels: labels,
99+
}, nil
100+
}
101+
102+
func NewTypeAndVersion(t ContractType, v semver.Version) TypeAndVersion {
103+
return TypeAndVersion{
104+
Type: t,
105+
Version: v,
106+
Labels: make(LabelSet), // empty set,
107+
}
108+
}
109+
110+
// AddressBook is a simple interface for storing and retrieving contract addresses across
111+
// chains. It is family agnostic as the keys are chain selectors.
112+
// We store rather than derive typeAndVersion as some contracts do not support it.
113+
// For ethereum addresses are always stored in EIP55 format.
114+
type AddressBook interface {
115+
Save(chainSelector uint64, address string, tv TypeAndVersion) error
116+
Addresses() (map[uint64]map[string]TypeAndVersion, error)
117+
AddressesForChain(chain uint64) (map[string]TypeAndVersion, error)
118+
// Allows for merging address books (e.g. new deployments with existing ones)
119+
Merge(other AddressBook) error
120+
Remove(ab AddressBook) error
121+
}
122+
123+
type AddressesByChain map[uint64]map[string]TypeAndVersion
124+
125+
type AddressBookMap struct {
126+
addressesByChain AddressesByChain
127+
mtx sync.RWMutex
128+
}
129+
130+
// Save will save an address for a given chain selector. It will error if there is a conflicting existing address.
131+
func (m *AddressBookMap) save(chainSelector uint64, address string, typeAndVersion TypeAndVersion) error {
132+
family, err := chainsel.GetSelectorFamily(chainSelector)
133+
if err != nil {
134+
return errors.Wrapf(ErrInvalidChainSelector, "chain selector %d", chainSelector)
135+
}
136+
if family == chainsel.FamilyEVM {
137+
if address == "" || address == common.HexToAddress("0x0").Hex() {
138+
return errors.Wrap(ErrInvalidAddress, "address cannot be empty")
139+
}
140+
if common.IsHexAddress(address) {
141+
// IMPORTANT: WE ALWAYS STANDARDIZE ETHEREUM ADDRESS STRINGS TO EIP55
142+
address = common.HexToAddress(address).Hex()
143+
} else {
144+
return errors.Wrapf(ErrInvalidAddress, "address %s is not a valid Ethereum address, only Ethereum addresses supported for EVM chains", address)
145+
}
146+
}
147+
148+
// TODO NONEVM-960: Add validation for non-EVM chain addresses
149+
150+
if typeAndVersion.Type == "" {
151+
return errors.New("type cannot be empty")
152+
}
153+
154+
if _, exists := m.addressesByChain[chainSelector]; !exists {
155+
// First time chain add, create map
156+
m.addressesByChain[chainSelector] = make(map[string]TypeAndVersion)
157+
}
158+
if _, exists := m.addressesByChain[chainSelector][address]; exists {
159+
return fmt.Errorf("address %s already exists for chain %d", address, chainSelector)
160+
}
161+
m.addressesByChain[chainSelector][address] = typeAndVersion
162+
return nil
163+
}
164+
165+
// Save will save an address for a given chain selector. It will error if there is a conflicting existing address.
166+
// thread safety version of the save method
167+
func (m *AddressBookMap) Save(chainSelector uint64, address string, typeAndVersion TypeAndVersion) error {
168+
m.mtx.Lock()
169+
defer m.mtx.Unlock()
170+
return m.save(chainSelector, address, typeAndVersion)
171+
}
172+
173+
func (m *AddressBookMap) Addresses() (map[uint64]map[string]TypeAndVersion, error) {
174+
m.mtx.RLock()
175+
defer m.mtx.RUnlock()
176+
177+
// maps are mutable and pass via a pointer
178+
// creating a copy of the map to prevent concurrency
179+
// read and changes outside object-bound
180+
return m.cloneAddresses(m.addressesByChain), nil
181+
}
182+
183+
func (m *AddressBookMap) AddressesForChain(chainSelector uint64) (map[string]TypeAndVersion, error) {
184+
_, err := chainsel.GetChainIDFromSelector(chainSelector)
185+
if err != nil {
186+
return nil, errors.Wrapf(ErrInvalidChainSelector, "chain selector %d", chainSelector)
187+
}
188+
189+
m.mtx.RLock()
190+
defer m.mtx.RUnlock()
191+
192+
if _, exists := m.addressesByChain[chainSelector]; !exists {
193+
return nil, errors.Wrapf(ErrChainNotFound, "chain selector %d", chainSelector)
194+
}
195+
196+
// maps are mutable and pass via a pointer
197+
// creating a copy of the map to prevent concurrency
198+
// read and changes outside object-bound
199+
return maps.Clone(m.addressesByChain[chainSelector]), nil
200+
}
201+
202+
// Merge will merge the addresses from another address book into this one.
203+
// It will error on any existing addresses.
204+
func (m *AddressBookMap) Merge(ab AddressBook) error {
205+
addresses, err := ab.Addresses()
206+
if err != nil {
207+
return err
208+
}
209+
210+
m.mtx.Lock()
211+
defer m.mtx.Unlock()
212+
213+
for chainSelector, chainAddresses := range addresses {
214+
for address, typeAndVersion := range chainAddresses {
215+
if err := m.save(chainSelector, address, typeAndVersion); err != nil {
216+
return err
217+
}
218+
}
219+
}
220+
return nil
221+
}
222+
223+
// Remove removes the address book addresses specified via the argument from the AddressBookMap.
224+
// Errors if all the addresses in the given address book are not contained in the AddressBookMap.
225+
func (m *AddressBookMap) Remove(ab AddressBook) error {
226+
addresses, err := ab.Addresses()
227+
if err != nil {
228+
return err
229+
}
230+
231+
m.mtx.Lock()
232+
defer m.mtx.Unlock()
233+
234+
// State of m.addressesByChain storage must not be changed in case of an error
235+
// need to do double iteration over the address book. First validation, second actual deletion
236+
for chainSelector, chainAddresses := range addresses {
237+
for address := range chainAddresses {
238+
if _, exists := m.addressesByChain[chainSelector][address]; !exists {
239+
return errors.New("AddressBookMap does not contain address from the given address book")
240+
}
241+
}
242+
}
243+
244+
for chainSelector, chainAddresses := range addresses {
245+
for address := range chainAddresses {
246+
delete(m.addressesByChain[chainSelector], address)
247+
}
248+
}
249+
250+
return nil
251+
}
252+
253+
// cloneAddresses creates a deep copy of map[uint64]map[string]TypeAndVersion object
254+
func (m *AddressBookMap) cloneAddresses(input map[uint64]map[string]TypeAndVersion) map[uint64]map[string]TypeAndVersion {
255+
result := make(map[uint64]map[string]TypeAndVersion)
256+
for chainSelector, chainAddresses := range input {
257+
result[chainSelector] = maps.Clone(chainAddresses)
258+
}
259+
return result
260+
}
261+
262+
// TODO: Maybe could add an environment argument
263+
// which would ensure only mainnet/testnet chain selectors are used
264+
// for further safety?
265+
func NewMemoryAddressBook() *AddressBookMap {
266+
return &AddressBookMap{
267+
addressesByChain: make(map[uint64]map[string]TypeAndVersion),
268+
}
269+
}
270+
271+
func NewMemoryAddressBookFromMap(addressesByChain map[uint64]map[string]TypeAndVersion) *AddressBookMap {
272+
return &AddressBookMap{
273+
addressesByChain: addressesByChain,
274+
}
275+
}
276+
277+
// SearchAddressBook search an address book for a given chain and contract type and return the first matching address.
278+
func SearchAddressBook(ab AddressBook, chain uint64, typ ContractType) (string, error) {
279+
addrs, err := ab.AddressesForChain(chain)
280+
if err != nil {
281+
return "", err
282+
}
283+
284+
for addr, tv := range addrs {
285+
if tv.Type == typ {
286+
return addr, nil
287+
}
288+
}
289+
290+
return "", errors.New("not found")
291+
}
292+
293+
func AddressBookContains(ab AddressBook, chain uint64, addrToFind string) (bool, error) {
294+
addrs, err := ab.AddressesForChain(chain)
295+
if err != nil {
296+
return false, err
297+
}
298+
299+
for addr := range addrs {
300+
if addr == addrToFind {
301+
return true, nil
302+
}
303+
}
304+
305+
return false, nil
306+
}
307+
308+
type typeVersionKey struct {
309+
Type ContractType
310+
Version string
311+
Labels string // store labels in a canonical form (comma-joined sorted list)
312+
}
313+
314+
func tvKey(tv TypeAndVersion) typeVersionKey {
315+
sortedLabels := tv.Labels.String()
316+
return typeVersionKey{
317+
Type: tv.Type,
318+
Version: tv.Version.String(),
319+
Labels: sortedLabels,
320+
}
321+
}
322+
323+
// EnsureDeduped ensures that each contract in the bundle only appears once
324+
// in the address map. It returns an error if there are more than one instance of a contract.
325+
// Returns true if every value in the bundle is found once, false otherwise.
326+
func EnsureDeduped(addrs map[string]TypeAndVersion, bundle []TypeAndVersion) (bool, error) {
327+
var (
328+
grouped = toTypeAndVersionMap(addrs)
329+
found = make([]TypeAndVersion, 0)
330+
)
331+
for _, btv := range bundle {
332+
key := tvKey(btv)
333+
matched, ok := grouped[key]
334+
if ok {
335+
found = append(found, btv)
336+
}
337+
if len(matched) > 1 {
338+
return false, fmt.Errorf("found more than one instance of contract %s v%s (labels=%s)",
339+
key.Type, key.Version, key.Labels)
340+
}
341+
}
342+
343+
// Indicate if each TypeAndVersion in the bundle is found at least once
344+
return len(found) == len(bundle), nil
345+
}
346+
347+
// toTypeAndVersionMap groups contract addresses by unique TypeAndVersion.
348+
func toTypeAndVersionMap(addrs map[string]TypeAndVersion) map[typeVersionKey][]string {
349+
tvkMap := make(map[typeVersionKey][]string)
350+
for k, v := range addrs {
351+
tvkMap[tvKey(v)] = append(tvkMap[tvKey(v)], k)
352+
}
353+
return tvkMap
354+
}
355+
356+
// AddLabel adds a string to the LabelSet in the TypeAndVersion.
357+
func (tv *TypeAndVersion) AddLabel(label string) {
358+
if tv.Labels == nil {
359+
tv.Labels = make(LabelSet)
360+
}
361+
tv.Labels.Add(label)
362+
}

0 commit comments

Comments
 (0)