Add getmediantimepast RPC call (#3921)

* Add `getmediantimepast` RPC call

* update docs

* respond to the PR comments
This commit is contained in:
rorp 2021-12-22 14:15:30 -08:00 committed by GitHub
parent 0c625346e7
commit 4b07629d56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 128 additions and 6 deletions

View File

@ -87,6 +87,9 @@ object ConsoleCli {
case gbh: GetBlockHeader => gbh.copy(hash = hash)
case other => other
}))),
cmd("getmediantimepast")
.action((_, conf) => conf.copy(command = GetMedianTimePast))
.text(s"Get the median time past"),
cmd("decoderawtransaction")
.action((_, conf) =>
conf.copy(command = DecodeRawTransaction(EmptyTransaction)))
@ -1692,6 +1695,8 @@ object ConsoleCli {
val requestParam: RequestParam = command match {
case GetInfo =>
RequestParam("getinfo")
case GetMedianTimePast =>
RequestParam("getmediantimepast")
case GetUtxos =>
RequestParam("getutxos")
case ListReservedUtxos =>
@ -2341,6 +2346,8 @@ object CliCommand {
case class GetBlockHeader(hash: DoubleSha256DigestBE)
extends AppServerCliCommand
case object GetMedianTimePast extends AppServerCliCommand
case class DecodeRawTransaction(transaction: Transaction)
extends AppServerCliCommand

View File

@ -268,6 +268,20 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
}
}
"return the median time past" in {
(mockChainApi.getMedianTimePast: () => Future[Long])
.expects()
.returning(Future.successful(1234567890L))
val route =
chainRoutes.handleCommand(ServerCommand("getmediantimepast", Arr()))
Get() ~> route ~> check {
assert(contentType == `application/json`)
assert(responseAs[String] == """{"result":1234567890,"error":null}""")
}
}
"return the wallet's balance" in {
(mockWalletApi
.getBalance()(_: ExecutionContext))

View File

@ -95,6 +95,13 @@ case class ChainRoutes(chain: ChainApi, network: BitcoinNetwork)(implicit
Server.httpSuccess(info.toJson)
}
}
case ServerCommand("getmediantimepast", _) =>
complete {
chain.getMedianTimePast().map { mtp =>
Server.httpSuccess(mtp)
}
}
}
}

View File

@ -236,6 +236,16 @@ class BlockchainRpcTest extends BitcoindFixturesCachedPairV17 {
}
}
it should "calculate median time past" in { nodePair =>
val client = nodePair.node1
for {
medianTime <- client.getMedianTimePast()
} yield {
val oneHourAgo = (System.currentTimeMillis() / 1000) - 60 * 60
assert(medianTime > oneHourAgo)
}
}
override def afterAll(): Unit = {
val stoppedF = pruneClientF.flatMap(BitcoindRpcTestUtil.stopServer)
val _ = Await.result(stoppedF, duration)

View File

@ -107,7 +107,8 @@ class BitcoindRpcClient(override val instance: BitcoindInstance)(implicit
}
/** Gets the number of compact filters in the database */
override def getFilterCount(): Future[Int] = ???
override def getFilterCount(): Future[Int] = Future.failed(
new UnsupportedOperationException(s"Not implemented: getFilterCount"))
/** Returns the block height of the given block stamp */
override def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] =
@ -129,6 +130,12 @@ class BitcoindRpcClient(override val instance: BitcoindInstance)(implicit
override def epochSecondToBlockHeight(time: Long): Future[Int] =
Future.successful(0)
override def getMedianTimePast(): Future[Long] = {
for {
info <- getBlockChainInfo
} yield info.mediantime.toLong
}
// Node Api
override def broadcastTransactions(

View File

@ -966,6 +966,56 @@ class ChainHandler(
def toChainHandlerCached: Future[ChainHandlerCached] = {
ChainHandler.toChainHandlerCached(this)
}
/** calculates the median time passed */
override def getMedianTimePast(): Future[Long] = {
/*
static constexpr int nMedianTimeSpan = 11;
int64_t GetMedianTimePast() const
{
int64_t pmedian[nMedianTimeSpan];
int64_t* pbegin = &pmedian[nMedianTimeSpan];
int64_t* pend = &pmedian[nMedianTimeSpan];
const CBlockIndex* pindex = this;
for (int i = 0; i < nMedianTimeSpan && pindex; i++, pindex = pindex->pprev)
*(--pbegin) = pindex->GetBlockTime();
std::sort(pbegin, pend);
return pbegin[(pend - pbegin)/2];
}
*/
val nMedianTimeSpan = 11
@tailrec
def getNTopHeaders(
n: Int,
acc: Vector[Future[Option[BlockHeaderDb]]]): Vector[
Future[Option[BlockHeaderDb]]] = {
if (n == 1)
acc
else {
val prev: Future[Option[BlockHeaderDb]] = acc.last.flatMap {
case None => Future.successful(None)
case Some(last) => getHeader(last.previousBlockHashBE)
}
getNTopHeaders(n - 1, acc :+ prev)
}
}
val top11 = getNTopHeaders(nMedianTimeSpan,
Vector(getBestBlockHeader().map(Option.apply)))
Future
.sequence(top11)
.map(_.collect { case Some(header) =>
header.time.toLong
})
.map { times =>
times.sorted.apply(times.size / 2)
}
}
}
object ChainHandler {

View File

@ -41,6 +41,8 @@ trait ChainQueryApi {
/** Gets the block height of the closest block to the given time */
def epochSecondToBlockHeight(time: Long): Future[Int]
/** calculates the median time passed */
def getMedianTimePast(): Future[Long]
}
object ChainQueryApi {

View File

@ -152,6 +152,7 @@ the `-p 9999:9999` port mapping on the docker container to adjust for this.
- `hash` - The block hash
- `decoderawtransaction` `tx` - `Decode the given raw hex transaction`
- `tx` - Transaction encoded in hex to decode
- `getmediantimepast` - Returns the median time past
### Wallet
- `rescan` `[options]` - Rescan for wallet UTXOs

View File

@ -59,6 +59,8 @@ trait ChainQueryApi {
def getFiltersBetweenHeights(
startHeight: Int,
endHeight: Int): Future[Vector[FilterResponse]]
def getMedianTimePast(): Future[Long]
}
```
@ -180,6 +182,8 @@ val chainApi = new ChainQueryApi {
Future.sequence(filterFs)
}
override def getMedianTimePast(): Future[Long] = bitcoind.getMedianTimePast()
}
// Finally, we can initialize our wallet with our own node api

View File

@ -81,6 +81,7 @@ val chainApi = new ChainQueryApi {
override def getFilterCount(): Future[Int] = Future.successful(0)
override def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] = Future.successful(0)
override def getFiltersBetweenHeights(startHeight: Int, endHeight: Int): Future[Vector[FilterResponse]] = Future.successful(Vector.empty)
override def getMedianTimePast(): Future[Long] = Future.successful(0L)
}
```

View File

@ -121,12 +121,16 @@ class NeutrinoNodeTest extends NodeTestWithCachedBitcoindPair {
//the send headers message.
for {
_ <- NodeTestUtil.awaitSync(node, bitcoind)
} yield {
val isCancelled = cancellable.cancel()
if (!isCancelled) {
logger.warn(s"Failed to cancel generating blocks on bitcoind")
_ = {
val isCancelled = cancellable.cancel()
if (!isCancelled) {
logger.warn(s"Failed to cancel generating blocks on bitcoind")
}
}
succeed
mtp1 <- bitcoind.getMedianTimePast()
mtp2 <- node.chainApiFromDb().flatMap(_.getMedianTimePast())
} yield {
assert(mtp1 == mtp2)
}
}
}

View File

@ -342,4 +342,7 @@ trait Node extends NodeApi with ChainQueryApi with P2PLogger {
override def epochSecondToBlockHeight(time: Long): Future[Int] =
chainApiFromDb().flatMap(_.epochSecondToBlockHeight(time))
override def getMedianTimePast(): Future[Long] =
chainApiFromDb().flatMap(_.getMedianTimePast())
}

View File

@ -165,5 +165,9 @@ trait BaseNodeTest extends BitcoinSFixture with EmbeddedPg {
override def epochSecondToBlockHeight(time: Long): Future[Int] =
Future.successful(0)
/** calculates the median time passed */
override def getMedianTimePast(): Future[Long] =
Future.successful(0L)
}
}

View File

@ -129,6 +129,10 @@ trait BaseWalletTest extends EmbeddedPg { _: Suite with BitcoinSAkkaAsyncTest =>
override def epochSecondToBlockHeight(time: Long): Future[Int] =
Future.successful(0)
/** calculates the median time passed */
override def getMedianTimePast(): Future[Long] =
Future.successful(0L)
}
}

View File

@ -299,6 +299,10 @@ object BitcoinSWalletTest extends WalletLogger {
override def epochSecondToBlockHeight(time: Long): Future[Int] =
Future.successful(0)
/** calculates the median time passed */
override def getMedianTimePast(): Future[Long] =
Future.successful(0L)
}
private[bitcoins] class RandomFeeProvider extends FeeRateApi {