12 "github.com/vapor/account"
13 "github.com/vapor/blockchain/pseudohsm"
14 "github.com/vapor/blockchain/signers"
15 "github.com/vapor/blockchain/txbuilder"
16 "github.com/vapor/common"
17 "github.com/vapor/consensus"
18 "github.com/vapor/crypto/ed25519/chainkd"
19 dbm "github.com/vapor/database/leveldb"
20 "github.com/vapor/errors"
21 "github.com/vapor/protocol/bc"
22 "github.com/vapor/protocol/bc/types"
25 // MockBlock mock a block
26 func MockBlock(txs []*types.Tx) *types.Block {
28 BlockHeader: types.BlockHeader{Timestamp: uint64(time.Now().Nanosecond())},
33 func MockSimpleUtxo(index uint64, assetID *bc.AssetID, amount uint64, ctrlProg *account.CtrlProgram) *account.UTXO {
35 ctrlProg = &account.CtrlProgram{
39 ControlProgram: []byte{81},
44 utxo := &account.UTXO{
45 OutputID: bc.Hash{V0: 1},
46 SourceID: bc.Hash{V0: 1},
50 ControlProgram: ctrlProg.ControlProgram,
51 ControlProgramIndex: ctrlProg.KeyIndex,
52 AccountID: ctrlProg.AccountID,
53 Address: ctrlProg.Address,
60 func AddTxOutput(assetID bc.AssetID, amount uint64, controlProgram []byte) *types.TxOutput {
61 out := types.NewIntraChainOutput(assetID, amount, controlProgram)
65 func BuildTx(baseUtxo *account.UTXO, signer *signers.Signer) (*txbuilder.Template, error) {
66 tplBuilder, err := CreateTxBuilder(baseUtxo, signer)
71 tpl, _, err := tplBuilder.Build()
79 func CreateTxBuilder(baseUtxo *account.UTXO, signer *signers.Signer) (*txbuilder.TemplateBuilder, error) {
80 tplBuilder := txbuilder.NewBuilder(time.Now())
81 txOutput := AddTxOutput(baseUtxo.AssetID, 100, baseUtxo.ControlProgram)
82 tplBuilder.AddOutput(txOutput)
83 return tplBuilder, nil
86 func MockTxsP2PKH(acctMgr *account.Manager, xPub chainkd.XPub, multiTypeAccount bool) ([]*types.Tx, error) {
88 accts := []*account.Account{}
89 for i := uint32(1); i < 32; i = i + 1 + rand.Uint32()%5 {
90 alias := fmt.Sprintf("testAccount%d", i)
91 deriveRule := signers.BIP0044
93 deriveRule = uint8(rand.Uint32() % 2)
95 acct, err := account.CreateAccount([]chainkd.XPub{xPub}, 1, alias, uint64(i), deriveRule)
100 if err := acctMgr.SaveAccount(acct); err != nil {
104 accts = append(accts, acct)
107 for _, acct := range accts {
108 for i := uint32(1); i < 256; i = i + 1 + rand.Uint32()%16 {
109 controlProg, err := account.CreateCtrlProgram(acct, uint64(i), false)
114 if err := acctMgr.SaveControlPrograms(controlProg); err != nil {
118 utxo := MockSimpleUtxo(0, consensus.BTMAssetID, 1000000000, controlProg)
119 tpl, err := BuildTx(utxo, acct.Signer)
124 txs = append(txs, tpl.Transaction)
131 func TestXPubsRecoveryLock(t *testing.T) {
132 dirPath, err := ioutil.TempDir(".", "")
136 defer os.RemoveAll(dirPath)
138 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
139 hsm, err := pseudohsm.New(dirPath)
144 xpub, _, err := hsm.XCreate("test_pub", "password", "en")
149 acctMgr := account.NewManager(testDB, nil)
150 recoveryMgr := newRecoveryManager(testDB, acctMgr)
151 recoveryMgr.state = newRecoveryState()
152 recoveryMgr.state.XPubs = []chainkd.XPub{xpub.XPub}
153 recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
155 recoveryMgr.state.StartTime = time.Now()
156 recoveryMgr.commitStatusInfo()
158 if err := recoveryMgr.AcctResurrect([]chainkd.XPub{xpub.XPub}); err != nil {
159 t.Fatal("TestXPubsRecoveryLock err:", err)
162 if err := recoveryMgr.AcctResurrect([]chainkd.XPub{xpub.XPub}); err != errors.Root(ErrRecoveryBusy) {
163 t.Fatal("TestXPubsRecoveryLock err:", err)
166 if err := recoveryMgr.LoadStatusInfo(); err != errors.Root(ErrRecoveryBusy) {
167 t.Fatal("TestXPubsRecoveryLock err:", err)
170 recoveryMgr.stopXPubsRec()
171 if err := recoveryMgr.LoadStatusInfo(); err != nil {
172 t.Fatal("TestXPubsRecoveryLock err:", err)
174 recoveryMgr.finished()
175 if err := recoveryMgr.AcctResurrect([]chainkd.XPub{xpub.XPub}); err != nil {
176 t.Fatal("TestXPubsRecoveryLock err:", err)
180 func TestExtendScanAddresses(t *testing.T) {
181 dirPath, err := ioutil.TempDir(".", "")
185 defer os.RemoveAll(dirPath)
187 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
188 hsm, err := pseudohsm.New(dirPath)
193 xpub, _, err := hsm.XCreate("test_pub", "password", "en")
198 acctMgr := account.NewManager(testDB, nil)
199 recoveryMgr := newRecoveryManager(testDB, acctMgr)
200 acc1 := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
201 acc2 := &account.Account{ID: "testB", Alias: "test2"}
202 acc3 := &account.Account{ID: "testC", Alias: "test3", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 2, DeriveRule: 3}}
203 acc4 := &account.Account{ID: "testD", Alias: "test4", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 3, DeriveRule: signers.BIP0032}}
205 recoveryMgr.state.stateForScope(acc1)
206 recoveryMgr.state.stateForScope(acc3)
207 recoveryMgr.state.stateForScope(acc4)
210 acct *account.Account
214 {acc1, nil, addrRecoveryWindow * 2},
215 {acc2, ErrInvalidAcctID, addrRecoveryWindow * 2},
216 {acc3, signers.ErrDeriveRule, addrRecoveryWindow * 2},
217 {acc4, nil, addrRecoveryWindow * 3},
220 for _, c := range cases {
221 if err := recoveryMgr.extendScanAddresses(c.acct.ID, true); err != c.err {
222 t.Fatal("extend scan addresses err:", err)
225 if err := recoveryMgr.extendScanAddresses(c.acct.ID, false); err != c.err {
226 t.Fatal("extend scan addresses err:", err)
229 if uint64(len(recoveryMgr.addresses)) != c.addressLen {
230 t.Fatalf("extend scan addresses err: len:%d,want:%d", len(recoveryMgr.addresses), c.addressLen)
235 func TestRecoveryFromXPubs(t *testing.T) {
236 dirPath, err := ioutil.TempDir(".", "")
240 defer os.RemoveAll(dirPath)
242 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
243 recoveryDB := dbm.NewDB("recdb", "leveldb", dirPath)
244 hsm, err := pseudohsm.New(dirPath)
249 xpub, _, err := hsm.XCreate("test_pub", "password", "en")
254 acctMgr := account.NewManager(testDB, nil)
255 txs, err := MockTxsP2PKH(acctMgr, xpub.XPub, false)
256 recAcctMgr := account.NewManager(recoveryDB, nil)
257 recoveryMgr := newRecoveryManager(recoveryDB, recAcctMgr)
263 {[]chainkd.XPub{xpub.XPub}, nil},
264 {[]chainkd.XPub{xpub.XPub, xpub.XPub}, signers.ErrDupeXPub},
265 {[]chainkd.XPub{}, signers.ErrNoXPubs},
268 for _, c := range cases {
269 if err := recoveryMgr.AcctResurrect(c.xPubs); errors.Root(err) != c.err {
270 t.Fatal("recovery from XPubs err:", err)
274 recoveryMgr.finished()
277 if err := recoveryMgr.FilterRecoveryTxs(MockBlock(txs)); err != nil {
278 t.Fatal("recovery from XPubs err:", err)
281 Accounts, err := acctMgr.ListAccounts("")
283 t.Fatal("recovery from XPubs err:", err)
286 for _, acct := range Accounts {
287 tmp, err := recAcctMgr.GetAccountByXPubsIndex(acct.XPubs, acct.KeyIndex)
289 t.Fatal("recovery from XPubs err:", err)
293 t.Fatal("accout recovery from xpubs err:", acct.KeyIndex)
296 if acctMgr.GetBip44ContractIndex(acct.ID, true) != recAcctMgr.GetBip44ContractIndex(tmp.ID, true) {
297 t.Fatal("bip44 internal address index recovery from xpubs err")
300 if acctMgr.GetBip44ContractIndex(acct.ID, false) != recAcctMgr.GetBip44ContractIndex(tmp.ID, false) {
301 t.Fatal("bip44 external address index recovery from xpubs err")
305 recoveryMgr.finished()
309 func TestRecoveryByRescanAccount(t *testing.T) {
310 dirPath, err := ioutil.TempDir(".", "")
314 defer os.RemoveAll(dirPath)
316 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
317 recoveryDB := dbm.NewDB("recdb", "leveldb", dirPath)
318 hsm, err := pseudohsm.New(dirPath)
323 xpub, _, err := hsm.XCreate("test_pub", "password", "en")
328 acctMgr := account.NewManager(testDB, nil)
329 txs, err := MockTxsP2PKH(acctMgr, xpub.XPub, true)
331 t.Fatal("recovery by rescan account err:", err)
334 allAccounts, err := acctMgr.ListAccounts("")
336 t.Fatal("recovery by rescan account err:", err)
339 recAcctMgr := account.NewManager(recoveryDB, nil)
340 for _, acct := range allAccounts {
341 if err := recAcctMgr.SaveAccount(acct); err != nil {
342 t.Fatal("recovery by rescan account err:", err)
346 recoveryMgr := newRecoveryManager(recoveryDB, recAcctMgr)
348 acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: 3}}
351 accounts []*account.Account
355 {[]*account.Account{acct}, signers.ErrDeriveRule},
358 for _, c := range cases {
359 if err := recoveryMgr.AddrResurrect(c.accounts); errors.Root(err) != c.err {
360 t.Fatal("recovery by rescan account err:", err)
366 recoveryMgr.FilterRecoveryTxs(MockBlock(txs))
367 accounts, err := acctMgr.ListAccounts("")
369 t.Fatal("recovery from XPubs err:", err)
372 for _, acct := range accounts {
373 tmp, err := recAcctMgr.GetAccountByXPubsIndex(acct.XPubs, acct.KeyIndex)
375 t.Fatal("recovery from XPubs err:", err)
379 t.Fatal("accout recovery from xpubs err:", acct.KeyIndex)
382 if acctMgr.GetBip44ContractIndex(acct.ID, true) != recAcctMgr.GetBip44ContractIndex(tmp.ID, true) {
383 t.Fatal("bip44 internal address index recovery from xpubs err")
386 if acctMgr.GetBip44ContractIndex(acct.ID, false) != recAcctMgr.GetBip44ContractIndex(tmp.ID, false) {
387 t.Fatal("bip44 external address index recovery from xpubs err")
390 if acctMgr.GetContractIndex(acct.ID) != recAcctMgr.GetContractIndex(tmp.ID) {
391 t.Fatal("bip32 address index recovery from xpubs err")
398 func TestReportFound(t *testing.T) {
399 dirPath, err := ioutil.TempDir(".", "")
403 defer os.RemoveAll(dirPath)
405 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
406 hsm, err := pseudohsm.New(dirPath)
411 xpub1, _, err := hsm.XCreate("test_pub1", "password", "en")
416 xpub2, _, err := hsm.XCreate("test_pub2", "password", "en")
421 acctMgr := account.NewManager(testDB, nil)
422 recoveryMgr := newRecoveryManager(testDB, acctMgr)
423 acc1 := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub1.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
424 acc2 := &account.Account{ID: "testB", Alias: "test2", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub2.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0032}}
425 acc3 := &account.Account{ID: "testC", Alias: "test3", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub2.XPub}, KeyIndex: 2, DeriveRule: signers.BIP0044}}
427 cp1 := &account.CtrlProgram{AccountID: acc1.ID, Address: "address1", KeyIndex: 10, Change: false}
428 cp2 := &account.CtrlProgram{AccountID: acc1.ID, Address: "address1", KeyIndex: 20, Change: true}
429 cp3 := &account.CtrlProgram{AccountID: acc2.ID, Address: "address1", KeyIndex: 30, Change: false}
430 cp4 := &account.CtrlProgram{AccountID: acc2.ID, Address: "address1", KeyIndex: 40, Change: true}
431 cp5 := &account.CtrlProgram{AccountID: acc3.ID, Address: "address1", KeyIndex: 50, Change: false}
432 cp6 := &account.CtrlProgram{AccountID: acc3.ID, Address: "address1", KeyIndex: 60, Change: true}
434 if err := acctMgr.SaveAccount(acc2); err != nil {
435 t.Fatal("ReportFound test err:", err)
438 if err := acctMgr.SaveAccount(acc3); err != nil {
439 t.Fatal("ReportFound test err:", err)
442 recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
443 recoveryMgr.state.XPubs = []chainkd.XPub{xpub1.XPub}
444 recoveryMgr.state.stateForScope(acc1)
445 recoveryMgr.state.stateForScope(acc2)
446 recoveryMgr.state.stateForScope(acc3)
449 acct *account.Account
450 cp *account.CtrlProgram
452 status *addressRecoveryState
455 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 1, 1}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 139, 11}, Account: acc1}},
457 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 1, 1}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 159, 31}, Account: acc2}},
459 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 149, 21}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 139, 11}, Account: acc1}},
461 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 169, 41}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 159, 31}, Account: acc2}},
463 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 1, 1}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 179, 51}, Account: acc3}},
465 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 189, 61}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 179, 51}, Account: acc3}},
468 for _, c := range cases {
469 if err := recoveryMgr.reportFound(c.acct, c.cp); err != c.err {
470 t.Fatal("ReportFound test err:", err, c.acct.ID)
473 status, ok := recoveryMgr.state.AccountsStatus[c.acct.ID]
475 t.Fatal("ReportFound test err: can not find status")
477 if !reflect.DeepEqual(status, c.status) {
478 t.Log(c.status.Account, c.status.InternalBranch, c.status.ExternalBranch)
479 t.Log(status.Account, status.InternalBranch, status.ExternalBranch)
480 t.Fatal("ReportFound test err: recovery status error")
485 func TestLoadStatusInfo(t *testing.T) {
486 dirPath, err := ioutil.TempDir(".", "")
490 defer os.RemoveAll(dirPath)
492 testDB := dbm.NewDB("testdb", "leveldb", "temp")
493 defer os.RemoveAll("temp")
495 hsm, err := pseudohsm.New(dirPath)
500 xpub, _, err := hsm.XCreate("test_pub", "password", "en")
505 acctMgr := account.NewManager(testDB, nil)
506 recoveryMgr := newRecoveryManager(testDB, acctMgr)
507 // StatusInit init recovery status manager.
508 recoveryMgr.state = newRecoveryState()
509 recoveryMgr.state.XPubs = []chainkd.XPub{xpub.XPub}
510 recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
512 recoveryMgr.state.StartTime = time.Now()
513 if err := recoveryMgr.LoadStatusInfo(); err != nil {
514 t.Fatal("TestLoadStatusInfo err:", err)
517 recoveryMgr.commitStatusInfo()
519 recoveryMgrRestore := newRecoveryManager(testDB, acctMgr)
520 if err := recoveryMgrRestore.LoadStatusInfo(); err != nil {
521 t.Fatal("TestLoadStatusInfo err:", err)
524 if !reflect.DeepEqual(recoveryMgrRestore.state.XPubsStatus, recoveryMgr.state.XPubsStatus) {
525 t.Fatalf("TestLoadStatusInfo XPubsStatus reload err")
528 if !reflect.DeepEqual(recoveryMgrRestore.state.XPubs, recoveryMgr.state.XPubs) {
529 t.Fatalf("TestLoadStatusInfo XPubs reload err")
532 if !reflect.DeepEqual(recoveryMgrRestore.state.AccountsStatus, recoveryMgr.state.AccountsStatus) {
533 t.Fatalf("TestLoadStatusInfo AccountsStatus reload err")
536 if !recoveryMgrRestore.state.StartTime.Equal(recoveryMgr.state.StartTime) {
537 t.Fatalf("TestLoadStatusInfo StartTime reload err")
540 acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: 3}}
541 recoveryMgr.state.AccountsStatus[acct.ID] = newAddressRecoveryState(addrRecoveryWindow, acct)
542 if err := recoveryMgr.commitStatusInfo(); err != nil {
543 t.Fatal("TestLoadStatusInfo err:", err)
545 if err := recoveryMgr.LoadStatusInfo(); err == nil {
546 t.Fatal("TestLoadStatusInfo err")
549 recoveryMgr.state = nil
550 if err := recoveryMgr.commitStatusInfo(); err != nil {
551 t.Fatal("TestLoadStatusInfo err:", err)
554 if err := recoveryMgr.LoadStatusInfo(); err == nil {
555 t.Fatal("TestLoadStatusInfo err")
559 func TestLock(t *testing.T) {
560 dirPath, err := ioutil.TempDir(".", "")
564 defer os.RemoveAll(dirPath)
566 testDB := dbm.NewDB("testdb", "leveldb", "temp")
567 defer os.RemoveAll("temp")
569 acctMgr := account.NewManager(testDB, nil)
570 recoveryMgr := newRecoveryManager(testDB, acctMgr)
571 if !recoveryMgr.tryStartXPubsRec() {
572 t.Fatal("recovery manager try lock test err")
575 if recoveryMgr.tryStartXPubsRec() {
576 t.Fatal("recovery manager relock test err")
579 recoveryMgr.stopXPubsRec()
581 if !recoveryMgr.tryStartXPubsRec() {
582 t.Fatal("recovery manager try lock test err")
586 func TestStateForScope(t *testing.T) {
587 state := newRecoveryState()
588 acc1 := &account.Account{ID: "test1", Alias: "testA"}
589 state.stateForScope(acc1)
590 if !reflect.DeepEqual(state.AccountsStatus[acc1.ID].Account, acc1) {
591 t.Fatal("state for scope test err")
594 acc2 := &account.Account{ID: "test1", Alias: "testB"}
595 state.stateForScope(acc2)
597 if reflect.DeepEqual(state.AccountsStatus[acc2.ID].Account, acc2) {
598 t.Fatal("state for scope test err")
601 acc3 := &account.Account{ID: "test2", Alias: "testC"}
602 state.stateForScope(acc3)
603 if !reflect.DeepEqual(state.AccountsStatus[acc3.ID].Account, acc3) {
604 t.Fatal("state for scope test err")
608 func bip44ContractIndexKey(accountID string, change bool) []byte {
609 contractIndexPrefix := []byte("ContractIndex")
610 key := append(contractIndexPrefix, accountID...)
612 return append(key, []byte{1}...)
614 return append(key, []byte{0}...)
617 func TestContractIndexResidue(t *testing.T) {
618 dirPath, err := ioutil.TempDir(".", "")
622 defer os.RemoveAll(dirPath)
624 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
625 hsm, err := pseudohsm.New(dirPath)
630 xpub1, _, err := hsm.XCreate("test_pub1", "password", "en")
635 contractIndexResidue := uint64(5)
636 acctMgr := account.NewManager(testDB, nil)
637 recoveryMgr := newRecoveryManager(testDB, acctMgr)
638 acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub1.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
640 cp1 := &account.CtrlProgram{AccountID: acct.ID, Address: "address1", KeyIndex: 10, Change: false}
642 setContractIndexKey := func(acctMgr *account.Manager, accountID string, change bool) {
643 testDB.Set(bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(contractIndexResidue))
646 delAccount := func(acctMgr *account.Manager, accountID string, change bool) {
647 acctMgr.DeleteAccount(accountID)
650 recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
651 recoveryMgr.state.XPubs = []chainkd.XPub{xpub1.XPub}
652 recoveryMgr.state.stateForScope(acct)
655 acct *account.Account
656 cp *account.CtrlProgram
657 preProcess func(acctMgr *account.Manager, accountID string, change bool)
661 {acct, cp1, setContractIndexKey, nil, 5},
662 {acct, cp1, delAccount, nil, 10},
665 for _, c := range cases {
666 if c.preProcess != nil {
667 c.preProcess(acctMgr, c.acct.ID, c.cp.Change)
670 if err := acctMgr.SaveAccount(acct); err != nil {
671 t.Fatal("ReportFound test err:", err)
674 if err := recoveryMgr.reportFound(c.acct, c.cp); err != c.err {
675 t.Fatal("ContractIndexResidue test err:", err, c.acct.ID)
677 cps, err := acctMgr.ListControlProgram()
679 t.Fatal("list control program err:", err)
683 for _, cp := range cps {
684 if cp.Address == "" || cp.AccountID != c.acct.ID {
690 if cpNum != c.wantCPNum {
691 t.Fatal("Test contract index residue cp num err want:", c.wantCPNum, " got:", cpNum)