diff --git a/src/main/resources/api/openapi.yaml b/src/main/resources/api/openapi.yaml index 85dfb9391a..0d49a2432d 100644 --- a/src/main/resources/api/openapi.yaml +++ b/src/main/resources/api/openapi.yaml @@ -4750,6 +4750,40 @@ paths: schema: $ref: '#/components/schemas/ApiError' + /wallet/getPrivateKey: + post: + security: + - ApiKeyAuth: [api_key] + summary: Get the private key corresponding to a known address + operationId: walletGetPrivateKey + tags: + - wallet + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ErgoAddress" + responses: + '200': + description: Successfully retrieved secret key + content: + application/json: + schema: + $ref: '#/components/schemas/DlogSecret' + '404': + description: Address not found in wallet database + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + /mining/candidate: get: security: diff --git a/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala b/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala index c4ddea6377..7669192f19 100644 --- a/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala +++ b/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala @@ -67,7 +67,8 @@ case class WalletApiRoute(readersHolder: ActorRef, signTransactionR ~ checkSeedR ~ rescanWalletR ~ - extractHintsR + extractHintsR ~ + getPrivateKeyR } } @@ -483,4 +484,17 @@ case class WalletApiRoute(readersHolder: ActorRef, } } + def getPrivateKeyR: Route = (path("getPrivateKey") & post & p2pkAddress) { p2pk => + withWalletOp(_.allExtendedPublicKeys()) { extKeys => + extKeys.find(_.key.value.equals(p2pk.pubkey.value)).map(_.path) match { + case Some(path) => + withWalletOp(_.getPrivateKeyFromPath(path)) { + case Success(secret) => ApiResponse(secret.w) + case Failure(f) => BadRequest(f.getMessage) + } + case None => NotExists("Address not found in wallet database.") + } + } + } + } diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala index a4b8e9199e..18b422095a 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala @@ -21,6 +21,7 @@ import org.ergoplatform.wallet.interpreter.TransactionHintsBag import org.ergoplatform.{ErgoAddressEncoder, ErgoApp, ErgoBox, GlobalConstants, P2PKAddress} import scorex.core.VersionTag import org.ergoplatform.network.ErgoNodeViewSynchronizer.ReceivableMessages.{ChangedMempool, ChangedState} +import org.ergoplatform.wallet.secrets.DerivationPath import scorex.core.utils.ScorexEncoding import scorex.util.{ModifierId, ScorexLogging} import sigmastate.Values.SigmaBoolean @@ -152,6 +153,12 @@ class ErgoWalletActor(settings: ErgoSettings, case ReadPublicKeys(from, until) => sender() ! state.walletVars.publicKeyAddresses.slice(from, until) + case ReadExtendedPublicKeys() => + sender() ! state.storage.readAllKeys() + + case GetPrivateKeyFromPath(path: DerivationPath) => + sender() ! ergoWalletService.getPrivateKeyFromPath(state, path) + case GetMiningPubKey => state.walletVars.trackedPubKeys.headOption match { case Some(pk) => @@ -619,6 +626,16 @@ object ErgoWalletActor extends ScorexLogging { */ final case class ReadPublicKeys(from: Int, until: Int) + /** + * Read all wallet public keys + */ + final case class ReadExtendedPublicKeys() + + /** + * Get the private key from seed based on a given derivation path + */ + final case class GetPrivateKeyFromPath(path: DerivationPath) + /** * Read wallet either from mnemonic or from secret storage */ diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletReader.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletReader.scala index 4fdcba607f..7e10887d71 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletReader.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletReader.scala @@ -17,9 +17,11 @@ import org.ergoplatform.wallet.boxes.ChainStatus import org.ergoplatform.wallet.boxes.ChainStatus.{OffChain, OnChain} import org.ergoplatform.wallet.Constants.ScanId import org.ergoplatform.wallet.interpreter.TransactionHintsBag +import org.ergoplatform.wallet.secrets.{DerivationPath, ExtendedPublicKey} import scorex.core.NodeViewComponent import scorex.util.ModifierId import sigmastate.Values.SigmaBoolean +import sigmastate.basics.DLogProtocol.DLogProverInput import scala.concurrent.Future import scala.util.Try @@ -72,6 +74,12 @@ trait ErgoWalletReader extends NodeViewComponent { def publicKeys(from: Int, to: Int): Future[Seq[P2PKAddress]] = (walletActor ? ReadPublicKeys(from, to)).mapTo[Seq[P2PKAddress]] + def allExtendedPublicKeys(): Future[Seq[ExtendedPublicKey]] = + (walletActor ? ReadExtendedPublicKeys()).mapTo[Seq[ExtendedPublicKey]] + + def getPrivateKeyFromPath(path: DerivationPath): Future[Try[DLogProverInput]] = + (walletActor ? GetPrivateKeyFromPath(path)).mapTo[Try[DLogProverInput]] + def walletBoxes(unspentOnly: Boolean, considerUnconfirmed: Boolean): Future[Seq[WalletBox]] = (walletActor ? GetWalletBoxes(unspentOnly, considerUnconfirmed)).mapTo[Seq[WalletBox]] diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala index 27a2916667..33885b6fcb 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala @@ -23,6 +23,7 @@ import org.ergoplatform.wallet.utils.FileUtils import scorex.util.encode.Base16 import scorex.util.{ModifierId, bytesToId} import sigmastate.Values.SigmaBoolean +import sigmastate.basics.DLogProtocol.DLogProverInput import java.io.FileNotFoundException import scala.util.{Failure, Success, Try} @@ -160,6 +161,14 @@ trait ErgoWalletService { */ def deriveKeyFromPath(state: ErgoWalletState, encodedPath: String, addrEncoder: ErgoAddressEncoder): Try[(P2PKAddress, ErgoWalletState)] + /** + * Get the secret key for a give derivation path. + * @param state current wallet state + * @param path derivation path from the master key + * @return Try of private key + */ + def getPrivateKeyFromPath(state: ErgoWalletState, path: DerivationPath): Try[DLogProverInput] + /** * Derive next key from master key * @param state current wallet state @@ -544,6 +553,18 @@ class ErgoWalletServiceImpl(override val ergoSettings: ErgoSettings) extends Erg Failure(new Exception("Unable to derive key from path, wallet is not initialized")) } + override def getPrivateKeyFromPath(state: ErgoWalletState, path: DerivationPath): Try[DLogProverInput] = + state.secretStorageOpt match { + case Some(secretStorage) if !secretStorage.isLocked => + val rootSecret = secretStorage.secret.get // unlocked means Some(secret) + Success(rootSecret.derive(path.toPrivateBranch).privateInput) + case Some(_) => + Failure(new Exception("Unable to derive key from path, wallet is locked")) + case None => + Failure(new Exception("Unable to derive key from path, wallet is not initialized")) + } + + override def deriveNextKey(state: ErgoWalletState, usePreEip3Derivation: Boolean): Try[(DeriveNextKeyResult, ErgoWalletState)] = state.secretStorageOpt match { case Some(secretStorage) if !secretStorage.isLocked => diff --git a/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala b/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala index ba5a5015cb..1f5cbfabf0 100644 --- a/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala +++ b/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala @@ -19,7 +19,7 @@ import org.ergoplatform.wallet.boxes.{ErgoBoxSerializer, ReplaceCompactCollectBo import org.ergoplatform.wallet.crypto.ErgoSignature import org.ergoplatform.wallet.interface4j.SecretString import org.ergoplatform.wallet.mnemonic.Mnemonic -import org.ergoplatform.wallet.secrets.ExtendedSecretKey +import org.ergoplatform.wallet.secrets.{DerivationPath, ExtendedSecretKey} import org.scalacheck.Gen import org.scalatest.BeforeAndAfterAll import scorex.db.{LDBKVStore, LDBVersionedStore} @@ -320,4 +320,25 @@ class ErgoWalletServiceSpec } } } + + property("it should derive private key correctly") { + withVersionedStore(2) { versionedStore => + withStore { store => + + val pass = SecretString.create(Random.nextString(10)) + val mnemonic = "edge talent poet tortoise trumpet dose" + + val walletService = new ErgoWalletServiceImpl(settings) + val ws1 = initialState(store, versionedStore) + val ws2 = walletService.initWallet(ws1, settings, pass, Some(SecretString.create(mnemonic))).get._2 + ws2.secretStorageOpt.get.unlock(pass) + + val path = DerivationPath.fromEncoded("m/44/1/1/0/0").get + val sk = ws2.secretStorageOpt.get.secret.get + val pk = sk.derive(path).publicKey + + walletService.getPrivateKeyFromPath(ws2, pk.path).get.w shouldBe sk.derive(path).privateInput.w + } + } + } }