Skip to content

Commit fa61846

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 fa61846

File tree

6 files changed

+1219
-1
lines changed

6 files changed

+1219
-1
lines changed

deployment/address_book.go

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

0 commit comments

Comments
 (0)