From c942a19a91d9fff52063a7678f35ad6c19f5310f Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Tue, 2 Apr 2024 20:44:29 -0500 Subject: [PATCH] Add an account builder [#3587] --- cmd/accumulated/cmd_init_network.go | 6 +- cmd/accumulated/run/devnet.go | 5 +- internal/core/execute/multi/multi_test.go | 2 +- .../core/execute/v1/simulator/simulator.go | 3 +- internal/node/daemon/init.go | 2 +- internal/node/genesis/bootstrap.go | 12 +- pkg/build/account.go | 414 ++++++++++++++++++ pkg/build/key_page_entry.go | 2 +- pkg/build/parser.go | 84 ++-- pkg/build/signature.go | 9 +- pkg/build/transaction.go | 6 +- pkg/types/record/key_hash.go | 2 + test/e2e/exec_process_test.go | 2 +- test/e2e/msg_block_anchor_test.go | 4 +- test/e2e/net_anchors_test.go | 38 +- test/e2e/regression2_test.go | 4 +- test/e2e/sequence2_test.go | 2 +- test/e2e/sig_general_test.go | 2 +- test/encoding/api_test.go | 2 +- test/encoding/executor_test.go | 2 +- test/simulator/options.go | 222 +++++++--- test/simulator/simulator.go | 2 +- test/simulator/simulator_test.go | 2 +- test/validate/main_test.go | 2 +- 24 files changed, 688 insertions(+), 143 deletions(-) create mode 100644 pkg/build/account.go diff --git a/cmd/accumulated/cmd_init_network.go b/cmd/accumulated/cmd_init_network.go index ad5805cbc..1b270cf4d 100644 --- a/cmd/accumulated/cmd_init_network.go +++ b/cmd/accumulated/cmd_init_network.go @@ -207,17 +207,17 @@ func initNetworkLocalFS(cmd *cobra.Command, netInit *accumulated.NetworkInit) { func buildGenesis(network *accumulated.NetworkInit) map[string][]byte { var factomAddresses func() (io.Reader, error) - var snapshots []func() (ioutil2.SectionReader, error) + var snapshots []func(*core.GlobalValues) (ioutil2.SectionReader, error) if flagInit.FactomAddresses != "" { factomAddresses = func() (io.Reader, error) { return os.Open(flagInit.FactomAddresses) } } for _, filename := range flagInit.Snapshots { filename := filename // See docs/developer/rangevarref.md - snapshots = append(snapshots, func() (ioutil2.SectionReader, error) { return os.Open(filename) }) + snapshots = append(snapshots, func(*core.GlobalValues) (ioutil2.SectionReader, error) { return os.Open(filename) }) } if flagInit.FaucetSeed != "" { b := createFaucet(strings.Split(flagInit.FaucetSeed, " ")) - snapshots = append(snapshots, func() (ioutil2.SectionReader, error) { + snapshots = append(snapshots, func(*core.GlobalValues) (ioutil2.SectionReader, error) { return ioutil2.NewBuffer(b), nil }) } diff --git a/cmd/accumulated/run/devnet.go b/cmd/accumulated/run/devnet.go index a80f0e32d..ebd8dff5f 100644 --- a/cmd/accumulated/run/devnet.go +++ b/cmd/accumulated/run/devnet.go @@ -23,6 +23,7 @@ import ( "github.com/multiformats/go-multiaddr" "gitlab.com/accumulatenetwork/accumulate/exp/faucet" "gitlab.com/accumulatenetwork/accumulate/exp/ioutil" + "gitlab.com/accumulatenetwork/accumulate/internal/core" "gitlab.com/accumulatenetwork/accumulate/internal/logging" "gitlab.com/accumulatenetwork/accumulate/internal/node/genesis" "gitlab.com/accumulatenetwork/accumulate/pkg/errors" @@ -262,8 +263,8 @@ func (d *DevnetConfiguration) buildGenesis(inst *Instance, cfg *Config, nodes [] GenesisGlobals: v, OperatorKeys: [][]byte{mainPubKey}, ConsensusParams: tmtypes.DefaultConsensusParams(), - Snapshots: []func() (ioutil.SectionReader, error){ - func() (ioutil.SectionReader, error) { return ioutil.NewBuffer(faucetSnapshot), nil }, + Snapshots: []func(*core.GlobalValues) (ioutil.SectionReader, error){ + func(*core.GlobalValues) (ioutil.SectionReader, error) { return ioutil.NewBuffer(faucetSnapshot), nil }, }, }) if err != nil { diff --git a/internal/core/execute/multi/multi_test.go b/internal/core/execute/multi/multi_test.go index 5425cb33b..0e9a6d1f0 100644 --- a/internal/core/execute/multi/multi_test.go +++ b/internal/core/execute/multi/multi_test.go @@ -36,7 +36,7 @@ func TestVersionSwitch(t *testing.T) { sim := NewSim(t, simulator.SimpleNetwork(t.Name(), 3, 1), // TODO Change to 3 after fixing anchor healing simulator.GenesisWith(GenesisTime, g), - simulator.SkipProposalCheck, // FIXME should not be necessary + simulator.SkipProposalCheck(), // FIXME should not be necessary ) alice := AccountUrl("alice") diff --git a/internal/core/execute/v1/simulator/simulator.go b/internal/core/execute/v1/simulator/simulator.go index 75dfbf4cd..4a1c64e19 100644 --- a/internal/core/execute/v1/simulator/simulator.go +++ b/internal/core/execute/v1/simulator/simulator.go @@ -50,7 +50,6 @@ type SimulatorOptions struct { LogLevels string OpenDB func(partition string, nodeIndex int, logger log.Logger) *database.Database FactomAddresses func() (io.Reader, error) - Snapshots []func() (ioutil2.SectionReader, error) } type Simulator struct { @@ -316,7 +315,7 @@ func (s *Simulator) InitFromGenesisWith(values *core.GlobalValues) { // The simulator only runs one DNN so set the threshold low values.Globals.ValidatorAcceptThreshold.Set(1, 1000) - genDocs, err := accumulated.BuildGenesisDocs(s.netInit, values, GenesisTime, s.Logger, s.opts.FactomAddresses, s.opts.Snapshots) + genDocs, err := accumulated.BuildGenesisDocs(s.netInit, values, GenesisTime, s.Logger, s.opts.FactomAddresses, nil) require.NoError(s, err) // Execute bootstrap after the entire network is known diff --git a/internal/node/daemon/init.go b/internal/node/daemon/init.go index 40619f7ca..1ce7ddf6a 100644 --- a/internal/node/daemon/init.go +++ b/internal/node/daemon/init.go @@ -196,7 +196,7 @@ func ConfigureNodePorts(node *NodeInit, cfg *config.Config, part protocol.Partit cfg.Accumulate.API.ListenAddress = node.Listen().Scheme("http").PartitionType(part).AccumulateAPI().String() } -func BuildGenesisDocs(network *NetworkInit, globals *core.GlobalValues, time time.Time, logger log.Logger, factomAddresses func() (io.Reader, error), snapshots []func() (ioutil2.SectionReader, error)) (map[string][]byte, error) { +func BuildGenesisDocs(network *NetworkInit, globals *core.GlobalValues, time time.Time, logger log.Logger, factomAddresses func() (io.Reader, error), snapshots []func(*core.GlobalValues) (ioutil2.SectionReader, error)) (map[string][]byte, error) { docs := map[string][]byte{} var operators [][]byte netinfo := new(protocol.NetworkDefinition) diff --git a/internal/node/genesis/bootstrap.go b/internal/node/genesis/bootstrap.go index 13dfefb9c..129a5b284 100644 --- a/internal/node/genesis/bootstrap.go +++ b/internal/node/genesis/bootstrap.go @@ -55,7 +55,7 @@ type InitOpts struct { // Preloaded data FactomAddresses func() (io.Reader, error) - Snapshots []func() (ioutil2.SectionReader, error) + Snapshots []func(*core.GlobalValues) (ioutil2.SectionReader, error) // Flags IncludeHistoryFromSnapshots bool @@ -64,6 +64,7 @@ type InitOpts struct { func Init(snapshotWriter io.WriteSeeker, opts InitOpts) error { // Initialize globals gg := core.NewGlobals(opts.GenesisGlobals) + opts.GenesisGlobals = gg // Build the routing table var bvns []string @@ -460,7 +461,7 @@ func (b *bootstrap) unpackSnapshots() error { var accounts []*url.URL seen := map[[32]byte]bool{} for _, open := range b.Snapshots { - file, err := open() + file, err := open(b.GenesisGlobals) if err != nil { return errors.UnknownError.Wrap(err) } @@ -485,6 +486,11 @@ func (b *bootstrap) unpackSnapshots() error { return false, nil } + // Skip system accounts + if _, ok := protocol.ParsePartitionUrl(u); ok { + return false, nil + } + // Track ACME issued if e.Key.Len() == 3 && e.Key.Get(2) == "Main" { acct, _ := protocol.UnmarshalAccount(e.Value) @@ -565,7 +571,7 @@ func (b *bootstrap) unpackSnapshots() error { // Restore messages for _, open := range b.Snapshots { - file, err := open() + file, err := open(b.GenesisGlobals) if err != nil { return errors.UnknownError.Wrap(err) } diff --git a/pkg/build/account.go b/pkg/build/account.go new file mode 100644 index 000000000..07746fe7e --- /dev/null +++ b/pkg/build/account.go @@ -0,0 +1,414 @@ +// Copyright 2024 The Accumulate Authors +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +package build + +import ( + "crypto/ed25519" + "fmt" + + "gitlab.com/accumulatenetwork/accumulate/internal/database" + "gitlab.com/accumulatenetwork/accumulate/internal/database/record" + "gitlab.com/accumulatenetwork/accumulate/pkg/errors" + "gitlab.com/accumulatenetwork/accumulate/pkg/types/address" + "gitlab.com/accumulatenetwork/accumulate/pkg/url" + "gitlab.com/accumulatenetwork/accumulate/protocol" +) + +type DbResolver = func(*url.URL) database.Updater +type accountBuildStep func(dbr DbResolver) error +type accountBuildSteps []accountBuildStep + +func (a *accountBuildSteps) add(step accountBuildStep) { + *a = append(*a, step) +} + +func (a *accountBuildSteps) execute(dbr DbResolver) error { + for i, step := range *a { + _ = i + err := step(dbr) + if err != nil { + return err + } + } + *a = nil + return nil +} + +type baseAccountBuilder struct { + parser + prev *baseAccountBuilder + url *url.URL + steps *accountBuildSteps +} + +func (b *baseAccountBuilder) String() string { + return b.url.String() +} + +func (b *baseAccountBuilder) ensureSteps() *accountBuildSteps { + if b.steps != nil { + return b.steps + } + if b.prev != nil { + b.steps = b.prev.ensureSteps() + } else { + b.steps = new(accountBuildSteps) + } + return b.steps +} + +func (b *baseAccountBuilder) addStep(step accountBuildStep) { + b.ensureSteps().add(func(dbr DbResolver) error { + if len(b.errs) > 0 { + return b.err() + } + return step(dbr) + }) +} + +func (b *baseAccountBuilder) parent() baseAccountBuilder { + parent, ok := b.url.Parent() + if !ok { + panic(fmt.Errorf("%v is a root account", b.url)) + } + if b.prev != nil && b.prev.url.Equal(parent) { + return *b.prev + } + return baseAccountBuilder{prev: b, url: parent} +} + +func (b *baseAccountBuilder) child(path ...string) baseAccountBuilder { + return baseAccountBuilder{prev: b, url: b.parseUrl(b.url, path...)} +} + +type AccountBuilder[T protocol.Account] struct { + baseAccountBuilder +} + +func (b *AccountBuilder[T]) addAcctStep(fn func(r *database.Account) error) { + b.baseAccountBuilder.addStep(func(dbr DbResolver) error { + return dbr(b.url).Update(func(batch *database.Batch) error { + return fn(batch.Account(b.url)) + }) + }) +} + +func (b *AccountBuilder[T]) Create(account T) *AccountBuilder[T] { + b.addStep(func(dbr DbResolver) error { + if account.GetUrl() == nil { + switch a := any(account).(type) { + case *protocol.ADI: + a.Url = b.url + case *protocol.TokenAccount: + a.Url = b.url + case *protocol.KeyBook: + a.Url = b.url + case *protocol.KeyPage: + a.Url = b.url + case *protocol.DataAccount: + a.Url = b.url + case *protocol.TokenIssuer: + a.Url = b.url + default: + return fmt.Errorf("missing account URL (%T)", account) + } + + } else if !account.GetUrl().Equal(b.url) { + return fmt.Errorf("wrong URL: want %v, got %v", b.url, account.GetUrl()) + } + + // Inherit the identity's authorities if the account defines + // authorities, hash none, and we're not on Baikonur + ledger, err := loadAccount[*protocol.SystemLedger](dbr, protocol.DnUrl().JoinPath(protocol.Ledger)) + if err != nil { + return err + } + inheritAuth := true && + !ledger.ExecutorVersion.V2BaikonurEnabled() && + !b.url.IsRootIdentity() + + switch acct := any(account).(type) { + case *protocol.KeyBook: + acct.AddAuthority(acct.Url) + + case *protocol.KeyPage: + bookUrl, index, ok := protocol.ParseKeyPageUrl(b.url) + if !ok { + return fmt.Errorf("invalid key page URL %v", b.url) + } + + book, err := loadAccount[*protocol.KeyBook](dbr, bookUrl) + if err != nil { + return err + } + if book.PageCount < index+1 { + book.PageCount = index + 1 + err = dbr(bookUrl).Update(func(batch *database.Batch) error { + return batch.Account(bookUrl).Main().Put(book) + }) + if err != nil { + return err + } + } + + case protocol.FullAccount: + if inheritAuth && len(acct.GetAuth().Authorities) == 0 { + adi, err := loadAccount[*protocol.ADI](dbr, b.url.Identity()) + if err != nil { + return err + } + acct.GetAuth().Authorities = adi.Authorities + } + } + + return dbr(b.url).Update(func(batch *database.Batch) error { + return batch.Account(b.url).Main().Put(account) + }) + }) + return b +} + +func (b *AccountBuilder[T]) Update(fn func(T)) *AccountBuilder[T] { + if b.url == nil { + b.errorf(errors.BadRequest, "no URL specified") + return b + } + b.addAcctStep(func(r *database.Account) error { + var account T + err := r.Main().GetAs(&account) + if err != nil { + return err + } + fn(account) + return r.Main().Put(account) + }) + return b +} + +func (b *AccountBuilder[T]) Build(dbr DbResolver) error { + if len(b.errs) > 0 { + return b.err() + } + return b.steps.execute(dbr) +} + +func (b *AccountBuilder[T]) Load(dbr DbResolver) (T, error) { + return loadAccount[T](dbr, b.url) +} + +func loadAccount[T protocol.Account](dbr DbResolver, u *url.URL) (T, error) { + var account T + return account, dbr(u).View(func(batch *database.Batch) error { + return batch.Account(u).Main().GetAs(&account) + }) +} + +type IdentityBuilder struct{ AccountBuilder[*protocol.ADI] } + +func Identity(base any, path ...string) *IdentityBuilder { + if s, ok := base.(string); ok { + base = protocol.AccountUrl(s) + } + b := &IdentityBuilder{} + b.url = b.parseUrl(base, path...) + return b +} + +func (b *IdentityBuilder) Identity() *IdentityBuilder { + return &IdentityBuilder{AccountBuilder[*protocol.ADI]{b.parent()}} +} + +func (b *IdentityBuilder) Create(bookPath ...string) *IdentityBuilder { + c := b.Book(bookPath...).Create() + b.AccountBuilder.Create(&protocol.ADI{ + AccountAuth: protocol.AccountAuth{ + Authorities: []protocol.AuthorityEntry{ + {Url: c.url}, + }, + }, + }) + return b +} + +func (b *IdentityBuilder) Update(fn func(adi *protocol.ADI)) *IdentityBuilder { + b.AccountBuilder.Update(fn) + return b +} + +type KeyBookBuilder struct { + AccountBuilder[*protocol.KeyBook] +} + +func (b *IdentityBuilder) Book(path ...string) *KeyBookBuilder { + if len(path) == 0 { + path = []string{"book"} + } + return &KeyBookBuilder{AccountBuilder[*protocol.KeyBook]{b.child(path...)}} +} + +func (b *KeyBookBuilder) Identity() *IdentityBuilder { + return &IdentityBuilder{AccountBuilder[*protocol.ADI]{b.parent()}} +} + +func (b *KeyBookBuilder) Create() *KeyBookBuilder { + b.AccountBuilder.Create(&protocol.KeyBook{}) + return b +} + +func (b *KeyBookBuilder) Update(fn func(book *protocol.KeyBook)) *KeyBookBuilder { + b.AccountBuilder.Update(fn) + return b +} + +func (b *KeyBookBuilder) Page(page int) *KeyPageBuilder { + return &KeyPageBuilder{AccountBuilder[*protocol.KeyPage]{b.child(fmt.Sprint(page))}} +} + +type KeyPageBuilder struct { + AccountBuilder[*protocol.KeyPage] +} + +func (b *KeyPageBuilder) Book() *KeyBookBuilder { + return &KeyBookBuilder{AccountBuilder[*protocol.KeyBook]{b.parent()}} +} + +func (b *KeyPageBuilder) Create() *KeyPageBuilder { + b.AccountBuilder.Create(&protocol.KeyPage{ + Version: 1, + }) + return b +} + +func (b *KeyPageBuilder) Update(fn func(page *protocol.KeyPage)) *KeyPageBuilder { + b.AccountBuilder.Update(fn) + return b +} + +func (b *KeyPageBuilder) AddEntry(entry protocol.KeySpec) *KeyPageBuilder { + return b.Update(func(page *protocol.KeyPage) { + page.AddKeySpec(&entry) + }) +} + +func (b *KeyPageBuilder) AddKey(key any) *KeyPageBuilder { + hash := b.hashKey(b.parsePublicKey(key, 0)) + return b.AddEntry(protocol.KeySpec{PublicKeyHash: hash}) +} + +func (b *KeyPageBuilder) AddDelegate(base any, path ...string) *KeyPageBuilder { + u := b.parseUrl(base, path...) + return b.AddEntry(protocol.KeySpec{Delegate: u}) +} + +func (b *KeyPageBuilder) AddCredits(amount any) *KeyPageBuilder { + a := b.parseAmount(amount, 2) + return b.Update(func(page *protocol.KeyPage) { + page.CreditCredits(a.Uint64()) + }) +} + +func (b *KeyPageBuilder) GenerateKey(typ protocol.SignatureType, seedParts ...any) address.Address { + seed := record.NewKey(b.url). + Append(seedParts...). + Hash() + + switch typ { + case protocol.SignatureTypeED25519, + protocol.SignatureTypeLegacyED25519, + protocol.SignatureTypeRCD1: + sk := ed25519.NewKeyFromSeed(seed[:]) + addr := address.FromED25519PrivateKey(sk) + addr.Type = typ + b.AddKey(addr) + return addr + default: + b.errorf(errors.BadRequest, "generating %v keys is not supported", typ) + return nil + } +} + +func (b *KeyPageBuilder) SetAcceptThreshold(v uint64) *KeyPageBuilder { + return b.Update(func(page *protocol.KeyPage) { + page.AcceptThreshold = v + }) +} + +func (b *KeyPageBuilder) SetRejectThreshold(v uint64) *KeyPageBuilder { + return b.Update(func(page *protocol.KeyPage) { + page.RejectThreshold = v + }) +} + +func (b *KeyPageBuilder) SetResponseThreshold(v uint64) *KeyPageBuilder { + return b.Update(func(page *protocol.KeyPage) { + page.ResponseThreshold = v + }) +} + +type TokenAccountBuilder struct { + AccountBuilder[*protocol.TokenAccount] +} + +func (b *IdentityBuilder) Tokens(path ...string) *TokenAccountBuilder { + return &TokenAccountBuilder{AccountBuilder[*protocol.TokenAccount]{b.child(path...)}} +} + +func (b *TokenAccountBuilder) Identity() *IdentityBuilder { + return &IdentityBuilder{AccountBuilder[*protocol.ADI]{b.parent()}} +} + +func (b *TokenAccountBuilder) Create(issuer any, path ...string) *TokenAccountBuilder { + b.AccountBuilder.Create(&protocol.TokenAccount{ + TokenUrl: b.parseUrl(issuer, path...), + }) + return b +} + +func (b *TokenAccountBuilder) Update(fn func(acct *protocol.TokenAccount)) *TokenAccountBuilder { + b.AccountBuilder.Update(fn) + return b +} + +func (b *TokenAccountBuilder) Add(amount any) *TokenAccountBuilder { + b.addStep(func(dbr DbResolver) error { + acct, err := loadAccount[*protocol.TokenAccount](dbr, b.url) + if err != nil { + return err + } + if acct.TokenUrl == nil { + return fmt.Errorf("%v has no TokenUrl", b.url) + } + + var precision uint64 + if protocol.AcmeUrl().Equal(acct.TokenUrl) { + // acc://ACME may not exist yet if this is called before genesis + precision = protocol.AcmePrecisionPower + + } else { + issuer, err := loadAccount[*protocol.TokenIssuer](dbr, acct.TokenUrl) + if err != nil { + return err + } + precision = issuer.Precision + } + + var p parser + amt := p.parseAmount(amount, precision) + if len(p.errs) > 0 { + return p.err() + } + + if !acct.CreditTokens(amt) { + return fmt.Errorf("invalid amount %v", amount) + } + + return dbr(b.url).Update(func(batch *database.Batch) error { + return batch.Account(b.url).Main().Put(acct) + }) + }) + return b +} diff --git a/pkg/build/key_page_entry.go b/pkg/build/key_page_entry.go index 3a0438479..14e0de23d 100644 --- a/pkg/build/key_page_entry.go +++ b/pkg/build/key_page_entry.go @@ -29,7 +29,7 @@ func (b KeyPageEntryBuilder[T]) Hash(hash any) KeyPageEntryBuilder[T] { } func (b KeyPageEntryBuilder[T]) Key(key any, typ protocol.SignatureType) KeyPageEntryBuilder[T] { - b.entry.KeyHash = b.hashKey(b.parsePublicKey(key), typ) + b.entry.KeyHash = b.hashKey(b.parsePublicKey(key, typ)) return b } diff --git a/pkg/build/parser.go b/pkg/build/parser.go index 7220e8e12..f9d2bb66a 100644 --- a/pkg/build/parser.go +++ b/pkg/build/parser.go @@ -8,8 +8,6 @@ package build import ( "crypto/ed25519" - "crypto/sha256" - "encoding/hex" "fmt" "math/big" "strconv" @@ -19,6 +17,7 @@ import ( tmed25519 "github.com/cometbft/cometbft/crypto/ed25519" "gitlab.com/accumulatenetwork/accumulate/pkg/client/signing" "gitlab.com/accumulatenetwork/accumulate/pkg/errors" + "gitlab.com/accumulatenetwork/accumulate/pkg/types/address" "gitlab.com/accumulatenetwork/accumulate/pkg/url" "gitlab.com/accumulatenetwork/accumulate/protocol" ) @@ -56,7 +55,10 @@ func (p *parser) err() error { } func (p *parser) record(err ...error) { - p.errs = append(p.errs, err...) + errs := make([]error, 0, len(p.errs)+len(err)) + errs = append(errs, p.errs...) + errs = append(errs, err...) + p.errs = errs } func (p *parser) errorf(code errors.Status, format string, args ...interface{}) { @@ -169,52 +171,68 @@ func (p *parser) parseHash(v any) []byte { } } -func (p *parser) parsePublicKey(key any) []byte { +func (p *parser) parsePublicKey(key any, typ protocol.SignatureType) address.Address { switch key := key.(type) { + case address.Address: + return key case ed25519.PrivateKey: - return key[32:] + return address.FromED25519PrivateKey(key) case ed25519.PublicKey: - return key + return address.FromED25519PublicKey(key) case tmed25519.PrivKey: - return key[32:] + return address.FromED25519PrivateKey(key) case tmed25519.PubKey: - return key + return address.FromED25519PublicKey(key) case []byte: - return key + if typ == protocol.SignatureTypeUnknown { + if len(key) == 32 || len(key) == 64 { + typ = protocol.SignatureTypeED25519 + } else { + return &address.Unknown{Value: key} + } + } + + if len(key) == 64 && (typ == protocol.SignatureTypeED25519 || typ == protocol.SignatureTypeLegacyED25519 || typ == protocol.SignatureTypeRCD1) { + return &address.PrivateKey{ + Key: key, + PublicKey: address.PublicKey{ + Type: typ, + Key: key[32:], + }, + } + } + + return &address.PublicKey{ + Type: typ, + Key: key, + } + case string: - b, err := hex.DecodeString(key) + addr, err := address.Parse(key) if err != nil { - p.errorf(errors.BadRequest, "parse key as hex: %w", err) + p.record(err) return nil } - return b + if uk, ok := addr.(*address.Unknown); ok && typ != protocol.SignatureTypeUnknown { + return &address.PublicKeyHash{ + Type: typ, + Hash: uk.Value, + } + } + return addr default: p.errorf(errors.BadRequest, "unsupported key type %T", key) - return nil + return &address.Unknown{} } } -func (p *parser) hashKey(key []byte, typ protocol.SignatureType) []byte { - switch typ { - case protocol.SignatureTypeLegacyED25519, - protocol.SignatureTypeED25519: - return hash(key) - case protocol.SignatureTypeRCD1: - return protocol.GetRCDHashFromPublicKey(key, 1) - case protocol.SignatureTypeBTC, - protocol.SignatureTypeBTCLegacy: - return protocol.BTCHash(key) - case protocol.SignatureTypeETH: - return protocol.ETHhash(key) - default: - p.errorf(errors.BadRequest, "unsupported key type %v", typ) - return nil +func (p *parser) hashKey(addr address.Address) []byte { + hash, ok := addr.GetPublicKeyHash() + if ok { + return hash } -} - -func hash(b []byte) []byte { - h := sha256.Sum256(b) - return h[:] + p.errorf(errors.BadRequest, "cannot determine hash for %v", addr) + return nil } func (p *parser) parseAmount(v any, precision uint64) *big.Int { diff --git a/pkg/build/signature.go b/pkg/build/signature.go index 3bce79ada..2fdcbc0eb 100644 --- a/pkg/build/signature.go +++ b/pkg/build/signature.go @@ -121,8 +121,13 @@ func (b SignatureBuilder) Metadata(v any) SignatureBuilder { return b } -func (b SignatureBuilder) PrivateKey(key []byte) SignatureBuilder { - b.signer.Signer = signing.PrivateKey(key) +func (b SignatureBuilder) PrivateKey(key any) SignatureBuilder { + addr := b.parsePublicKey(key, 0) + if sk, ok := addr.GetPrivateKey(); ok { + b.signer.Signer = signing.PrivateKey(sk) + } else { + b.errorf(errors.BadRequest, "%v is not a private key", addr) + } return b } diff --git a/pkg/build/transaction.go b/pkg/build/transaction.go index b189249d4..476998a7e 100644 --- a/pkg/build/transaction.go +++ b/pkg/build/transaction.go @@ -84,7 +84,7 @@ func (b TransactionBuilder) CreateIdentity(url any, path ...string) CreateIdenti } func (b CreateIdentityBuilder) WithKey(key any, typ protocol.SignatureType) CreateIdentityBuilder { - return b.WithKeyHash(b.t.hashKey(b.t.parsePublicKey(key), typ)) + return b.WithKeyHash(b.t.hashKey(b.t.parsePublicKey(key, typ))) } func (b CreateIdentityBuilder) WithKeyHash(hash any) CreateIdentityBuilder { @@ -522,7 +522,7 @@ func (b CreateKeyBookBuilder) WithKeyHash(hash any) CreateKeyBookBuilder { } func (b CreateKeyBookBuilder) WithKey(key any, typ protocol.SignatureType) CreateKeyBookBuilder { - return b.WithKeyHash(b.t.hashKey(b.t.parsePublicKey(key), typ)) + return b.WithKeyHash(b.t.hashKey(b.t.parsePublicKey(key, typ))) } type AddCreditsBuilder struct { @@ -693,7 +693,7 @@ type UpdateKeyBuilder struct { func (b TransactionBuilder) UpdateKey(newKey any, typ protocol.SignatureType) UpdateKeyBuilder { c := UpdateKeyBuilder{t: b} - c.body.NewKeyHash = c.t.hashKey(c.t.parsePublicKey(newKey), typ) + c.body.NewKeyHash = c.t.hashKey(c.t.parsePublicKey(newKey, typ)) return c } diff --git a/pkg/types/record/key_hash.go b/pkg/types/record/key_hash.go index 4383bd053..f29d3df1c 100644 --- a/pkg/types/record/key_hash.go +++ b/pkg/types/record/key_hash.go @@ -114,6 +114,8 @@ func keyBytes(v interface{}) []byte { return []byte(v) case interface{ Bytes() []byte }: return v.Bytes() + case interface{ String() string }: + return []byte(v.String()) case interface{ AccountID() []byte }: return v.AccountID() case uint: diff --git a/test/e2e/exec_process_test.go b/test/e2e/exec_process_test.go index aebbeef13..2802f0ae9 100644 --- a/test/e2e/exec_process_test.go +++ b/test/e2e/exec_process_test.go @@ -27,7 +27,7 @@ func TestExecutorProcessResults(t *testing.T) { sim := NewSim(t, simulator.SimpleNetwork(t.Name(), 3, 3), // The node count must be > 1 simulator.Genesis(GenesisTime), - simulator.IgnoreCommitResults, // Screwing with the account balance will cause the BPT to differ, so instruct the simulator to ignore that + simulator.IgnoreCommitResults(), // Screwing with the account balance will cause the BPT to differ, so instruct the simulator to ignore that ) alice := AccountUrl("alice") diff --git a/test/e2e/msg_block_anchor_test.go b/test/e2e/msg_block_anchor_test.go index 3ff143fd0..10a071600 100644 --- a/test/e2e/msg_block_anchor_test.go +++ b/test/e2e/msg_block_anchor_test.go @@ -34,7 +34,7 @@ func TestAnchorThreshold(t *testing.T) { opts := []simulator.Option{ simulator.SimpleNetwork(t.Name(), bvnCount, valCount), simulator.Genesis(GenesisTime), - simulator.DisableAnchorHealing, + simulator.DisableAnchorHealing(), } // Capture the BVN's anchors and verify they're the same @@ -113,7 +113,7 @@ func TestAnchorPlaceholder(t *testing.T) { opts := []simulator.Option{ simulator.Genesis(GenesisTime), - simulator.DisableAnchorHealing, + simulator.DisableAnchorHealing(), } // One BVN, two nodes diff --git a/test/e2e/net_anchors_test.go b/test/e2e/net_anchors_test.go index e81a1c2f4..087b08a69 100644 --- a/test/e2e/net_anchors_test.go +++ b/test/e2e/net_anchors_test.go @@ -7,55 +7,51 @@ package e2e import ( - "math/big" "testing" "github.com/stretchr/testify/require" "gitlab.com/accumulatenetwork/accumulate/pkg/build" - "gitlab.com/accumulatenetwork/accumulate/pkg/url" . "gitlab.com/accumulatenetwork/accumulate/protocol" . "gitlab.com/accumulatenetwork/accumulate/test/harness" - . "gitlab.com/accumulatenetwork/accumulate/test/helpers" "gitlab.com/accumulatenetwork/accumulate/test/simulator" - acctesting "gitlab.com/accumulatenetwork/accumulate/test/testing" ) // TestDropInitialAnchor is a simple test that simulates adverse network // conditions causing anchors to be dropped when they're initially sent. func TestDropInitialAnchor(t *testing.T) { - alice := url.MustParse("alice") - bob := url.MustParse("bob") - aliceKey := acctesting.GenerateKey(alice) - bobKey := acctesting.GenerateKey(bob) + alice := build. + Identity("alice").Create(). + Tokens("tokens").Create("ACME").Add(1e9).Identity(). + Book().Page(1).Create().AddCredits(1e9).Book().Identity() + aliceKey := alice.Book().Page(1). + GenerateKey(SignatureTypeED25519) + + bob := build. + Identity("bob").Create(). + Tokens("tokens").Create("ACME").Identity() // Initialize sim := NewSim(t, simulator.SimpleNetwork(t.Name(), 3, 3), - simulator.Genesis(GenesisTime), + simulator.Genesis(GenesisTime).With(alice, bob), // Drop anchors when they are initially sent, instead relying on the // Conductor's anchor healing - simulator.DropInitialAnchor, + simulator.DropInitialAnchor(), ) - MakeIdentity(t, sim.DatabaseFor(alice), alice, aliceKey[32:]) - CreditCredits(t, sim.DatabaseFor(alice), alice.JoinPath("book", "1"), 1e9) - MakeAccount(t, sim.DatabaseFor(alice), &TokenAccount{Url: alice.JoinPath("tokens"), TokenUrl: AcmeUrl()}) - CreditTokens(t, sim.DatabaseFor(alice), alice.JoinPath("tokens"), big.NewInt(1e12)) - MakeIdentity(t, sim.DatabaseFor(bob), bob, bobKey[32:]) - MakeAccount(t, sim.DatabaseFor(bob), &TokenAccount{Url: bob.JoinPath("tokens"), TokenUrl: AcmeUrl()}) - // Execute - st := sim.BuildAndSubmitTxnSuccessfully( + st := sim.BuildAndSubmitSuccessfully( build.Transaction().For(alice, "tokens"). SendTokens(123, 0).To(bob, "tokens"). SignWith(alice, "book", "1").Version(1).Timestamp(1).PrivateKey(aliceKey)) sim.StepUntil( - Txn(st.TxID).Succeeds(), - Txn(st.TxID).Produced().Succeeds()) + Sig(st[1].TxID).Completes(), + Txn(st[0].TxID).Completes()) // Verify - account := GetAccount[*TokenAccount](t, sim.DatabaseFor(bob), bob.JoinPath("tokens")) + account, err := bob.Tokens("tokens").Load(sim.DatabaseFor) + require.NoError(t, err) require.Equal(t, 123, int(account.Balance.Int64())) } diff --git a/test/e2e/regression2_test.go b/test/e2e/regression2_test.go index cefaa1279..215ec5f87 100644 --- a/test/e2e/regression2_test.go +++ b/test/e2e/regression2_test.go @@ -767,7 +767,7 @@ func TestDifferentValidatorSignaturesV1(t *testing.T) { sim := NewSim(t, simulator.SimpleNetwork(t.Name(), 1, 3), simulator.GenesisWith(GenesisTime, g), - simulator.IgnoreDeliverResults, + simulator.IgnoreDeliverResults(), ) sim.StepN(10) @@ -822,7 +822,7 @@ func TestDifferentValidatorSignaturesV2(t *testing.T) { sim := NewSim(t, simulator.SimpleNetwork(t.Name(), 1, 3), simulator.GenesisWith(GenesisTime, g), - simulator.IgnoreDeliverResults, + simulator.IgnoreDeliverResults(), ) sim.StepN(10) diff --git a/test/e2e/sequence2_test.go b/test/e2e/sequence2_test.go index 41627919c..3ef748e48 100644 --- a/test/e2e/sequence2_test.go +++ b/test/e2e/sequence2_test.go @@ -40,7 +40,7 @@ func TestMissingSynthTxn(t *testing.T) { sim := NewSim(t, simulator.SimpleNetwork(t.Name(), 3, 3), simulator.GenesisWith(GenesisTime, globals), - simulator.SkipProposalCheck, // FIXME should not be necessary + simulator.SkipProposalCheck(), // FIXME should not be necessary simulator.CaptureDispatchedMessages(func(ctx context.Context, env *messaging.Envelope) (send bool, err error) { if didDrop { diff --git a/test/e2e/sig_general_test.go b/test/e2e/sig_general_test.go index c4efe7197..3ef3428d8 100644 --- a/test/e2e/sig_general_test.go +++ b/test/e2e/sig_general_test.go @@ -360,7 +360,7 @@ func TestSignatureErrors(t *testing.T) { sim := NewSim(t, simulator.SimpleNetwork(t.Name(), 3, 1), simulator.Genesis(GenesisTime), - simulator.UseABCI, + simulator.UseABCI(), ) MakeIdentity(t, sim.DatabaseFor(alice), alice, aliceKey[32:]) diff --git a/test/encoding/api_test.go b/test/encoding/api_test.go index a9662a1b9..b2519f14e 100644 --- a/test/encoding/api_test.go +++ b/test/encoding/api_test.go @@ -61,7 +61,7 @@ func TestAPIv2Consistency(t *testing.T) { return mem }), simulator.WithNetwork(testData.Network), - simulator.Deterministic, + simulator.Deterministic(), ) require.NoError(t, err) diff --git a/test/encoding/executor_test.go b/test/encoding/executor_test.go index 9e06896cb..930999adc 100644 --- a/test/encoding/executor_test.go +++ b/test/encoding/executor_test.go @@ -50,7 +50,7 @@ func TestExecutorConsistency(t *testing.T) { simulator.WithLogger(acctesting.NewTestLogger(t)), simulator.WithNetwork(testData.Network), simulator.SnapshotMap(testData.Genesis), - simulator.DropDispatchedMessages, + simulator.DropDispatchedMessages(), simulator.InitialAcmeSupply(nil), ) require.NoError(t, err) diff --git a/test/simulator/options.go b/test/simulator/options.go index ff997500d..bb39f3c38 100644 --- a/test/simulator/options.go +++ b/test/simulator/options.go @@ -16,113 +16,138 @@ import ( "time" "github.com/cometbft/cometbft/libs/log" - "gitlab.com/accumulatenetwork/accumulate/internal/core" + "gitlab.com/accumulatenetwork/accumulate/exp/ioutil" + "gitlab.com/accumulatenetwork/accumulate/internal/core/execute" + "gitlab.com/accumulatenetwork/accumulate/internal/database" + "gitlab.com/accumulatenetwork/accumulate/internal/database/record" "gitlab.com/accumulatenetwork/accumulate/internal/node/config" accumulated "gitlab.com/accumulatenetwork/accumulate/internal/node/daemon" ioutil2 "gitlab.com/accumulatenetwork/accumulate/internal/util/io" "gitlab.com/accumulatenetwork/accumulate/pkg/database/keyvalue" "gitlab.com/accumulatenetwork/accumulate/pkg/database/keyvalue/badger" "gitlab.com/accumulatenetwork/accumulate/pkg/errors" + "gitlab.com/accumulatenetwork/accumulate/pkg/types/network" + "gitlab.com/accumulatenetwork/accumulate/pkg/url" "gitlab.com/accumulatenetwork/accumulate/protocol" "gitlab.com/accumulatenetwork/accumulate/test/testing" ) -type Option func(*simFactory) error +type Option interface { + apply(*simFactory) error +} type OpenDatabaseFunc = func(partition *protocol.PartitionInfo, node int, logger log.Logger) keyvalue.Beginner type SnapshotFunc = func(partition string, network *accumulated.NetworkInit, logger log.Logger) (ioutil2.SectionReader, error) type RecordingFunc = func(partition string, node int) (io.WriteSeeker, error) +type optionFunc func(*simFactory) error + +func (fn optionFunc) apply(f *simFactory) error { return fn(f) } + func WithLogger(logger log.Logger) Option { - return func(f *simFactory) error { + return optionFunc(func(f *simFactory) error { f.logger = logger return nil - } + }) } // Deterministic attempts to run the simulator in a fully deterministic, // repeatable way. -func Deterministic(opts *simFactory) error { - opts.deterministic = true - return nil +func Deterministic() Option { + return optionFunc(func(opts *simFactory) error { + opts.deterministic = true + return nil + }) } // DropDispatchedMessages drops all internally dispatched messages. -func DropDispatchedMessages(opts *simFactory) error { - opts.dropDispatchedMessages = true - opts.dropInitialAnchor = true - opts.disableAnchorHealing = true - return nil +func DropDispatchedMessages() Option { + return optionFunc(func(opts *simFactory) error { + opts.dropDispatchedMessages = true + opts.dropInitialAnchor = true + opts.disableAnchorHealing = true + return nil + }) } // DropInitialAnchor drops anchors when they are initially submitted. -func DropInitialAnchor(opts *simFactory) error { - opts.dropInitialAnchor = true - return nil +func DropInitialAnchor() Option { + return optionFunc(func(opts *simFactory) error { + opts.dropInitialAnchor = true + return nil + }) } // DisableAnchorHealing disables healing of anchors after they are initially // submitted. -func DisableAnchorHealing(opts *simFactory) error { - opts.disableAnchorHealing = true - return nil +func DisableAnchorHealing() Option { + return optionFunc(func(opts *simFactory) error { + opts.disableAnchorHealing = true + return nil + }) } // CaptureDispatchedMessages allows the caller to capture internally dispatched // messages. func CaptureDispatchedMessages(fn DispatchInterceptor) Option { - return func(opts *simFactory) error { + return optionFunc(func(opts *simFactory) error { opts.interceptDispatchedMessages = fn return nil - } + }) } // SkipProposalCheck skips checking if each non-leader node agrees with the // leader's proposed block. -func SkipProposalCheck(opts *simFactory) error { - opts.skipProposalCheck = true - return nil +func SkipProposalCheck() Option { + return optionFunc(func(opts *simFactory) error { + opts.skipProposalCheck = true + return nil + }) } // IgnoreDeliverResults ignores inconsistencies in the result of DeliverTx. -func IgnoreDeliverResults(opts *simFactory) error { - opts.ignoreDeliverResults = true - return nil +func IgnoreDeliverResults() Option { + return optionFunc(func(opts *simFactory) error { + opts.ignoreDeliverResults = true + return nil + }) } // IgnoreCommitResults ignores inconsistencies in the result of Commit. -func IgnoreCommitResults(opts *simFactory) error { - opts.ignoreCommitResults = true - return nil +func IgnoreCommitResults() Option { + return optionFunc(func(opts *simFactory) error { + opts.ignoreCommitResults = true + return nil + }) } func WithNetwork(net *accumulated.NetworkInit) Option { - return func(opts *simFactory) error { + return optionFunc(func(opts *simFactory) error { opts.network = net return nil - } + }) } func WithDatabase(fn OpenDatabaseFunc) Option { - return func(opts *simFactory) error { + return optionFunc(func(opts *simFactory) error { opts.storeOpt = fn return nil - } + }) } func WithSnapshot(fn SnapshotFunc) Option { - return func(opts *simFactory) error { + return optionFunc(func(opts *simFactory) error { opts.snapshot = fn return nil - } + }) } // WithRecordings takes a function that returns files to write node recordings to. func WithRecordings(fn RecordingFunc) Option { - return func(opts *simFactory) error { + return optionFunc(func(opts *simFactory) error { opts.recordings = fn return nil - } + }) } func SimpleNetwork(name string, bvnCount, nodeCount int) Option { @@ -204,50 +229,129 @@ func SnapshotMap(snapshots map[string][]byte) Option { }) } -func Genesis(time time.Time) Option { +func Genesis(time time.Time) genesis { // By default run tests with the new executor version - return GenesisWithVersion(time, protocol.ExecutorVersionLatest) + return genesis{time: time} +} + +// DO NOT USE - use Genesis(time).WithVersion(version) +func GenesisWithVersion(time time.Time, version protocol.ExecutorVersion) genesis { + return Genesis(time).WithVersion(version) } -func GenesisWithVersion(time time.Time, version protocol.ExecutorVersion) Option { - values := new(core.GlobalValues) - values.ExecutorVersion = version - return GenesisWith(time, values) +// DO NOT USE - use Genesis(time).WithValues(values) +func GenesisWith(time time.Time, values *network.GlobalValues) genesis { + return Genesis(time).WithValues(values) } -func GenesisWith(time time.Time, values *core.GlobalValues) Option { - return WithSnapshot(genesis(time, values)) +type genesis struct { + time time.Time + values *network.GlobalValues + extra []DbBuilder } -func genesis(time time.Time, values *core.GlobalValues) SnapshotFunc { - if values == nil { - values = new(core.GlobalValues) +func (g genesis) WithValues(values *network.GlobalValues) genesis { + g.values = values + return g +} + +func (g genesis) WithVersion(version protocol.ExecutorVersion) genesis { + if g.values == nil { + g.values = new(network.GlobalValues) + } + g.values.ExecutorVersion = version + return g +} + +type DbBuilder interface { + Build(func(*url.URL) database.Updater) error +} + +func (g genesis) With(builders ...DbBuilder) genesis { + g.extra = append(g.extra, builders...) + return g +} + +func (g genesis) apply(opts *simFactory) error { + if g.values == nil { + g.values = new(network.GlobalValues) + g.values.ExecutorVersion = protocol.ExecutorVersionLatest } var genDocs map[string][]byte - return func(partition string, network *accumulated.NetworkInit, logger log.Logger) (ioutil2.SectionReader, error) { + opts.snapshot = func(partition string, net *accumulated.NetworkInit, logger log.Logger) (ioutil2.SectionReader, error) { + if genDocs != nil { + return ioutil2.NewBuffer(genDocs[partition]), nil + } + + var snapshots []func(*network.GlobalValues) (ioutil.SectionReader, error) + if len(g.extra) > 0 { + var extra []byte + snapshots = append(snapshots, func(globals *network.GlobalValues) (ioutil.SectionReader, error) { + if extra != nil { + return ioutil.NewBuffer(extra), nil + } + + db := database.OpenInMemory(nil) + db.SetObserver(execute.NewDatabaseObserver()) + + // Fake the system ledger + err := db.Update(func(batch *database.Batch) error { + ledger := &protocol.SystemLedger{ + Url: protocol.DnUrl().JoinPath(protocol.Ledger), + ExecutorVersion: globals.ExecutorVersion, + } + return batch.Account(ledger.Url).Main().Put(ledger) + }) + if err != nil { + return nil, err + } + + for _, b := range g.extra { + err := b.Build(func(*url.URL) database.Updater { return db }) + if err != nil { + return nil, err + } + } + + buf := new(ioutil.Buffer) + err = db.Collect(buf, nil, &database.CollectOptions{ + Predicate: func(r record.Record) (bool, error) { + // Do not create a BPT + if r.Key().Get(0) == "BPT" { + return false, nil + } + return true, nil + }, + }) + extra = buf.Bytes() + return ioutil.NewBuffer(extra), err + }) + } + var err error - if genDocs == nil { - genDocs, err = accumulated.BuildGenesisDocs(network, values, time, logger, nil, nil) - if err != nil { - return nil, errors.UnknownError.WithFormat("build genesis docs: %w", err) - } + genDocs, err = accumulated.BuildGenesisDocs(net, g.values, g.time, logger, nil, snapshots) + if err != nil { + return nil, errors.UnknownError.WithFormat("build genesis docs: %w", err) } return ioutil2.NewBuffer(genDocs[partition]), nil } + return nil } // InitialAcmeSupply overrides the default initial ACME supply. A value of nil // will disable setting the initial supply. func InitialAcmeSupply(v *big.Int) Option { - return func(f *simFactory) error { + return optionFunc(func(f *simFactory) error { f.initialSupply = v return nil - } + }) } -func UseABCI(opts *simFactory) error { - opts.abci = withABCI - return nil +func UseABCI() Option { + return optionFunc(func(opts *simFactory) error { + opts.abci = withABCI + return nil + }) } diff --git a/test/simulator/simulator.go b/test/simulator/simulator.go index 35244f1b6..c25c4fd98 100644 --- a/test/simulator/simulator.go +++ b/test/simulator/simulator.go @@ -54,7 +54,7 @@ func New(opts ...Option) (*Simulator, error) { initialSupply: big.NewInt(1e6 * protocol.AcmePrecision), } for _, opt := range opts { - err := opt(o) + err := opt.apply(o) if err != nil { return nil, errors.UnknownError.Wrap(err) } diff --git a/test/simulator/simulator_test.go b/test/simulator/simulator_test.go index 4f18ac6c3..397fa1c94 100644 --- a/test/simulator/simulator_test.go +++ b/test/simulator/simulator_test.go @@ -199,7 +199,7 @@ func TestSimulatorWithABCI(t *testing.T) { sim := NewSim(t, simulator.SimpleNetwork(t.Name(), 1, 1), simulator.Genesis(GenesisTime), - simulator.UseABCI, + simulator.UseABCI(), ) MakeIdentity(t, sim.DatabaseFor(alice), alice, aliceKey[32:]) diff --git a/test/validate/main_test.go b/test/validate/main_test.go index 27561dfe2..ae959dd80 100644 --- a/test/validate/main_test.go +++ b/test/validate/main_test.go @@ -190,7 +190,7 @@ func setupSim(t *testing.T, net *accumulated.NetworkInit) (*simulator.Simulator, // FIXME should not be necessary, but it is for the block hook that // drops an anchor - simulator.SkipProposalCheck, + simulator.SkipProposalCheck(), ) require.NoError(t, err)