diff --git a/Docker/Stratis.CirrusD.TestNet/Dockerfile b/Docker/Stratis.CirrusD.TestNet/Dockerfile index 5d948208cb..0b94fbfa61 100644 --- a/Docker/Stratis.CirrusD.TestNet/Dockerfile +++ b/Docker/Stratis.CirrusD.TestNet/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 +FROM mcr.microsoft.com/dotnet/sdk:6.0 ARG APPVERSION=master ENV APPVERSION=${APPVERSION} diff --git a/Docker/Stratis.CirrusD/Dockerfile b/Docker/Stratis.CirrusD/Dockerfile index 3a8b3e79a6..2d48ad81e6 100644 --- a/Docker/Stratis.CirrusD/Dockerfile +++ b/Docker/Stratis.CirrusD/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 +FROM mcr.microsoft.com/dotnet/sdk:6.0 ARG APPVERSION=master ENV APPVERSION=${APPVERSION} diff --git a/Docker/Stratis.StraxD.TestNet/Dockerfile b/Docker/Stratis.StraxD.TestNet/Dockerfile index bb056e408a..6a71ca3e08 100644 --- a/Docker/Stratis.StraxD.TestNet/Dockerfile +++ b/Docker/Stratis.StraxD.TestNet/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 +FROM mcr.microsoft.com/dotnet/sdk:6.0 ARG APPVERSION=master ENV APPVERSION=${APPVERSION} diff --git a/Docker/Stratis.StraxD/Dockerfile b/Docker/Stratis.StraxD/Dockerfile index 2d38bf768e..a2300ae00b 100644 --- a/Docker/Stratis.StraxD/Dockerfile +++ b/Docker/Stratis.StraxD/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 +FROM mcr.microsoft.com/dotnet/sdk:6.0 ARG APPVERSION=master ENV APPVERSION=${APPVERSION} diff --git a/Scripts/LaunchSidechainMasternode.ps1 b/Scripts/LaunchSidechainMasternode.ps1 index dddce6ec8d..d1cb510802 100644 --- a/Scripts/LaunchSidechainMasternode.ps1 +++ b/Scripts/LaunchSidechainMasternode.ps1 @@ -240,6 +240,47 @@ function Check-TimeDifference } } +function Get-GethPeers +{ + $body = ConvertTo-Json -Compress @{ + + id = '1' + method = 'net_peerCount' + jsonrpc = '2.0' + } + + $result = Invoke-RestMethod -Uri http://localhost:8545 -Method Post -UseBasicParsing -Body $body -ContentType application/json | Select-Object -ExpandProperty result + [uint32]$result +} + +function Is-GethSyncing +{ + $body = ConvertTo-Json -Compress @{ + id = '1' + method = 'eth_syncing' + jsonrpc = '2.0' + } + + Invoke-RestMethod -Uri http://localhost:8545 -Method Post -UseBasicParsing -Body $body -ContentType application/json | Select-Object -ExpandProperty result +} + +function Get-PrysmState +{ + Invoke-RestMethod -Uri http://localhost:3500/eth/v1/node/syncing -ContentType application/json | Select-Object -ExpandProperty data +} + +function Get-GethCurrentSyncBlock +{ + [uint32]$currentBlock = (Is-GethSyncing).currentBlock + $currentBlock +} + +function Get-GethHighestSyncBlock +{ + [uint32]$highestBlock = (Is-GethSyncing).highestBlock + $highestBlock +} + #Create DataDir(s) if ( -not ( Get-Item -Path $mainChainDataDir -ErrorAction SilentlyContinue ) ) { @@ -358,12 +399,12 @@ if ( $NodeType -eq "50K" ) "" Start-Sleep 10 -if ($ethAddress -notmatch '^0x[a-fA-F0-9]{40}$') -{ - Write-Host (Get-TimeStamp) "ERROR: Invalid ETH Address Loaded.. Is GETH already running?" -ForegroundColor Red - Start-Sleep 30 - Exit -} + if ($ethAddress -notmatch '^0x[a-fA-F0-9]{40}$') + { + Write-Host (Get-TimeStamp) "ERROR: Invalid ETH Address Loaded.. Is GETH already running?" -ForegroundColor Red + Start-Sleep 30 + Exit + } #Launching GETH $API = $gethAPIPort @@ -376,55 +417,47 @@ if ($ethAddress -notmatch '^0x[a-fA-F0-9]{40}$') Start-Sleep 3 if ( $StartNode.HasExited -eq $true ) { - Write-Host (Get-TimeStamp) "ERROR: Something went wrong. Please contact support in Discord" -ForegroundColor Red + Write-Host (Get-TimeStamp) "ERROR: GETH not found. Please contact support in Discord" -ForegroundColor Red Start-Sleep 30 Exit } } - <# - $gethPeerCountBody = ConvertTo-Json -Compress @{ - jsonrpc = "2.0" - method = "net_peerCount" - id = "1" - } - [uint32]$gethPeerCount = Invoke-RestMethod -Uri "http://127.0.0.1:$API" -Method Post -Body $gethPeerCountBody -ContentType application/json | Select-Object -ExpandProperty result - While ( $gethPeerCount -lt 1 ) + While ( (Get-GethPeers) -lt 1 ) { - Write-Host (Get-TimeStamp) "Waiting for Peers..." -ForegroundColor Yellow - Start-Sleep 2 - [uint32]$gethPeerCount = Invoke-RestMethod -Uri "http://127.0.0.1:$API" -Method Post -Body $gethPeerCountBody -ContentType application/json | Select-Object -ExpandProperty result - } - - $gethSyncStateBody = ConvertTo-Json -Compress @{ - jsonrpc = "2.0" - method = "eth_syncing" - id = "1" - } - - $syncStatus = Invoke-RestMethod -Uri "http://127.0.0.1:$API" -Method Post -Body $gethSyncStateBody -ContentType application/json -ErrorAction SilentlyContinue | Select-Object -ExpandProperty result - While ( $syncStatus -eq $false -or $syncStatus.currentBlock -eq $null ) - { - Write-Host (Get-TimeStamp) "Waiting for Blockchain Synchronization to begin" -ForegroundColor Yellow - $syncStatus = Invoke-RestMethod -Uri "http://127.0.0.1:$API" -Method Post -Body $gethSyncStateBody -ContentType application/json -ErrorAction SilentlyContinue | Select-Object -ExpandProperty result - Start-Sleep 2 + Write-Host (Get-TimeStamp) "Waiting for Peers..." -ForegroundColor Yellow + Start-Sleep 3 + if ( $StartNode.HasExited -eq $true ) + { + Write-Host (Get-TimeStamp) "ERROR: Something went wrong. Please contact support in Discord" -ForegroundColor Red + Start-Sleep 30 + Exit + } } - [uint32]$currentBlock = Invoke-RestMethod -Uri "http://127.0.0.1:$API" -Method Post -Body $gethSyncStateBody -ContentType application/json | Select-Object -ExpandProperty result | Select-Object -ExpandProperty currentBlock - [uint32]$highestBlock = Invoke-RestMethod -Uri "http://127.0.0.1:$API" -Method Post -Body $gethSyncStateBody -ContentType application/json | Select-Object -ExpandProperty result | Select-Object -ExpandProperty highestBlock - - While ( ( $highestBlock ) -gt ( $currentBlock ) ) + While (((Get-PrysmState).is_optimistic -eq $true )-or ( (Get-PrysmState).is_syncing -eq $true )) { - [uint32]$currentBlock = Invoke-RestMethod -Uri "http://127.0.0.1:$API" -Method Post -Body $gethSyncStateBody -ContentType application/json | Select-Object -ExpandProperty result | Select-Object -ExpandProperty currentBlock - [uint32]$highestBlock = Invoke-RestMethod -Uri "http://127.0.0.1:$API" -Method Post -Body $gethSyncStateBody -ContentType application/json | Select-Object -ExpandProperty result | Select-Object -ExpandProperty highestBlock - $syncProgress = $highestBlock - $currentBlock - "" - Write-Host (Get-TimeStamp) "The Local Height is $currentBlock" -ForegroundColor Yellow - Write-Host (Get-TimeStamp) "The Current Tip is $highestBlock" -ForegroundColor Yellow - Write-Host (Get-TimeStamp) "$syncProgress Blocks Require Indexing..." -ForegroundColor Yellow - Start-Sleep 10 + Write-Host (Get-TimeStamp) "Waiting for GETH to sync..." -ForegroundColor Yellow + Start-Sleep 3 + if ( Is-GethSyncing -ne $false ) + { + $missingBlocks = (Get-GethHighestSyncBlock) - (Get-GethCurrentSyncBlock) + While ( ($missingBlocks) -gt 1 ) + { + "" + Write-Host (Get-TimeStamp) "The Synced Height is" (Get-GethCurrentSyncBlock) -ForegroundColor Yellow + Write-Host (Get-TimeStamp) "The Current Tip is" (Get-GethHighestSyncBlock) -ForegroundColor Yellow + Write-Host (Get-TimeStamp) "$missingBlocks Blocks Require Syncing..." -ForegroundColor Yellow + Start-Sleep 10 + if ( ! ( Get-Process -Name beacon-chain* )) + { + Write-Host (Get-TimeStamp) "ERROR: Prysm not found. Please contact support in Discord" -ForegroundColor Red + Start-Sleep 30 + Exit + } + } + } } - #> #Move to CirrusPegD Set-Location -Path $cloneDir/src/Stratis.CirrusPegD @@ -766,8 +799,8 @@ Exit # SIG # Begin signature block # MIIO+gYJKoZIhvcNAQcCoIIO6zCCDucCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR -# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUejsUZ0JLxas+Jqi18Wly5q0+ -# JuugggxCMIIFfjCCBGagAwIBAgIQCrk836uc/wPyOiuycqPb5zANBgkqhkiG9w0B +# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUrObR9s8OxtM113HzHFHzWizX +# 7w2gggxCMIIFfjCCBGagAwIBAgIQCrk836uc/wPyOiuycqPb5zANBgkqhkiG9w0B # AQsFADBsMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBFViBDb2Rl # IFNpZ25pbmcgQ0EgKFNIQTIpMB4XDTIxMDQyMjAwMDAwMFoXDTI0MDcxOTIzNTk1 @@ -837,11 +870,11 @@ Exit # ZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgRVYgQ29kZSBTaWduaW5nIENBIChT # SEEyKQIQCrk836uc/wPyOiuycqPb5zAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIB # DDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEE -# AYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQU0uX/VCXDvyJn -# BQQNVZ7Sa/3rHyEwDQYJKoZIhvcNAQEBBQAEggEAREhkDWsFFW5NTq6O8Tu7XOrj -# zVyt4YmbvHrOu4YtPRNsbs0Ch+OMa0oB2I5JSN5Ftez4YfjVyCt8xui+0hLpoZE5 -# HABgRZDZVpO5RLjB6j5DFx5nbWT1YZ32dM5DrATgYny3lf1/EIQUGlW6AKtxO8R4 -# PF64f7iq6v9Kv70e0HNc/hAFspRraOgwtJnMcVN4EsEMv8BqkKXoqUFJ/eycpMQJ -# yGpodM6/FleM8t/clyx3roZVE81QZcYphMbG6YvCn/8IfD44ibEHBq7wyGIST2ZP -# 9qoH+rk7qb16DiI0LT88CufOGxWPgEEB/Ic4oqiQC0sDG14SiWJ1mK3FuKbn1w== -# SIG # End signature block \ No newline at end of file +# AYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUtkdZtCkwChYL +# lfthA+rvfs02/fQwDQYJKoZIhvcNAQEBBQAEggEAOc212C7goOPHqsBfnSW7+h3j +# +vWWdF+Ik/kS/1icZSrYFBYiqsyF7CZodi/885xN+Vt80D6was9elZz55qbYY8Fy +# VeG/yHAHqodg3Rt2KDlYkk0xTReTrS2PSBGG3aaSqV4fj/xj3CMJ4bKlekJ0xnZU +# v66D8SlyCeyn+pR6dNahGF9IikfasJM4W3jgvd14unoilvKqtKFaCxPdl4c/pixV +# IaTRTfFgBwp1ibxY0Kdq/5TQ91oByYICJ4Kwwu/G0n2VP1ZLf7QJaJfzmjdxV+Jw +# qlaydHIC7Gd4iXJ/TcqpZki5VWHOXGef51cdauLD+nn+Xt12LYhi8Pxa5AncBg== +# SIG # End signature block diff --git a/Scripts/Masternode-Launch-Script-RC.ps1 b/Scripts/Masternode-Launch-Script-RC.ps1 new file mode 100644 index 0000000000..8df12dd4a2 --- /dev/null +++ b/Scripts/Masternode-Launch-Script-RC.ps1 @@ -0,0 +1,229 @@ +Write-Host STRAX Masternode Launch Script - Release Candidate Testing -ForegroundColor Cyan +"" +"" +Start-Sleep 5 + +Clear-Host +$mainChainAPIPort = '17103' +$sideChainAPIPort = '37223' +$sidechainMasternodesRepo = "https://github.com/stratisproject/StratisFullNode.git" +$sidechainMasternodeBranch = "release/1.4.0.7" +$stratisMasternodeDashboardRepo = "https://github.com/stratisproject/StratisMasternodeDashboard" + +Write-Host Testing: $sidechainMasternodeBranch -ForegroundColor Cyan +Start-Sleep 10 +"" +"" + +#Create Functions +function Shutdown-MainchainNode +{ + Write-Host "Shutting down Mainchain Node..." -ForegroundColor Yellow + $Headers = @{} + $Headers.Add("Accept","application/json") + Invoke-WebRequest -Uri http://localhost:$mainChainAPIPort/api/Node/shutdown -Method Post -ContentType application/json-patch+json -Headers $Headers -Body "true" -ErrorAction SilentlyContinue | Out-Null + + While ( Test-Connection -TargetName 127.0.0.1 -TCPPort $mainChainAPIPort -ErrorAction SilentlyContinue ) + { + Write-Host "Waiting for node to stop..." -ForegroundColor Yellow + Start-Sleep 5 + } + + Write-Host "SUCCESS: Mainchain Node shutdown" -ForegroundColor Green + Write-Host "" +} + +function Shutdown-SidechainNode +{ + Write-Host "Shutting down Sidechain Node..." -ForegroundColor Yellow + $Headers = @{} + $Headers.Add("Accept","application/json") + Invoke-WebRequest -Uri http://localhost:$sideChainAPIPort/api/Node/shutdown -Method Post -ContentType application/json-patch+json -Headers $Headers -Body "true" -ErrorAction SilentlyContinue | Out-Null + + While ( Test-Connection -TargetName 127.0.0.1 -TCPPort $sideChainAPIPort -ErrorAction SilentlyContinue ) + { + Write-Host "Waiting for node to stop..." -ForegroundColor Yellow + Start-Sleep 5 + } + + Write-Host "SUCCESS: Sidechain Node shutdown" -ForegroundColor Green + Write-Host "" +} + +function Get-TimeStamp +{ + return "[{0:dd/MM/yy} {0:HH:mm:ss}]" -f (Get-Date) +} + +#Check for pre-requisites +$dotnetVersion = dotnet --list-sdks +if ( -not ($dotnetVersion -like "6.0.*") ) +{ + Write-Host "ERROR: .NET Core 6.0 SDK or above not found, please download and install .NET Core SDK 6.0" -ForegroundColor Red + Start-Sleep 30 + Exit +} + +$gitVersion = git --version +if ( -not ($gitVersion -ne $null) ) +{ + Write-Host "ERROR: git not found, please download and install git" -ForegroundColor Red + Start-Sleep 30 + Exit +} + +$pwshVersion = $PSVersionTable.PSVersion.ToString() +if ( -not ($pwshVersion -gt "7.1") ) +{ + Write-Host "ERROR: PowerShell Core not found, please launch this script using 'PWSH' or download and install PowerShell 7 or greater" -ForegroundColor Red + Start-Sleep 30 + Exit +} + +#Set Required Environment Variables +if ($IsWindows) +{ + $mainChainDataDir = "$env:APPDATA\StratisNode\strax\StraxMain" + $sideChainDataDir = "$env:APPDATA\StratisNode\cirrus\CirrusMain" + $cloneDir = "$HOME\Desktop\STRAX-SidechainMasternodes-RC" + $stratisMasternodeDashboardCloneDir = $cloneDir.Replace('SidechainMasternodes','StratisMasternodeDashboard') +} + Else + { + Write-Host "ERROR: Windows OS was not detected." -ForegroundColor Red + Start-Sleep 10 + Exit + } + +#Check for an existing running node +Write-Host (Get-TimeStamp) "Checking for running Mainchain Node" -ForegroundColor Cyan +if ( Test-Connection -TargetName 127.0.0.1 -TCPPort $mainChainAPIPort ) +{ + Write-Host (Get-TimeStamp) "WARNING: A node is already running, will perform a graceful shutdown" -ForegroundColor DarkYellow + "" + Shutdown-MainchainNode +} + +Write-Host (Get-TimeStamp) "Checking for running Sidechain Node" -ForegroundColor Cyan +if ( Test-Connection -TargetName 127.0.0.1 -TCPPort $sideChainAPIPort ) +{ + Write-Host (Get-TimeStamp) "WARNING: A node is already running, will perform a graceful shutdown" -ForegroundColor DarkYellow + "" + Shutdown-SidechainNode +} + +#Code Update +if ( Test-Path -Path $cloneDir -ErrorAction SilentlyContinue ) +{ + Remove-Item -Path $cloneDir -Force -Recurse -ErrorAction SilentlyContinue +} + +Write-Host (Get-TimeStamp) INFO: "Checking for updates.." -ForegroundColor Yellow +if ( -not ( Test-Path -Path $CloneDir -ErrorAction SilentlyContinue) ) +{ + Write-Host (Get-TimeStamp) INFO: "Cloning SidechainMasternodes Branch" -ForegroundColor Cyan + Start-Process git.exe -ArgumentList "clone --recurse-submodules $sidechainMasternodesRepo -b $sidechainMasternodeBranch $cloneDir" -Wait +} + Else + { + Set-Location $cloneDir + Start-Process git.exe -ArgumentList "pull" -Wait + } + +if ( -not ( Test-Path -Path $stratisMasternodeDashboardCloneDir -ErrorAction SilentlyContinue ) ) +{ + Write-Host (Get-TimeStamp) INFO: "Cloning Stratis Masternode Dashboard" -ForegroundColor Cyan + Start-Process git.exe -ArgumentList "clone $stratisMasternodeDashboardRepo $stratisMasternodeDashboardCloneDir" -Wait +} + Else + { + Set-Location $stratisMasternodeDashboardCloneDir + Start-Process git.exe -ArgumentList "pull" -Wait + } + +#Call Launch Script +Set-Location $cloneDir\Scripts\ +& '.\LaunchSidechainMasternode.ps1' + +# SIG # Begin signature block +# MIIO+gYJKoZIhvcNAQcCoIIO6zCCDucCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB +# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR +# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUpzrZHURx4LtRPfn0ualvYWAE +# UEigggxCMIIFfjCCBGagAwIBAgIQCrk836uc/wPyOiuycqPb5zANBgkqhkiG9w0B +# AQsFADBsMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD +# VQQLExB3d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBFViBDb2Rl +# IFNpZ25pbmcgQ0EgKFNIQTIpMB4XDTIxMDQyMjAwMDAwMFoXDTI0MDcxOTIzNTk1 +# OVowgZ0xHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYBBAGC +# NzwCAQMTAkdCMREwDwYDVQQFEwgxMDU1MDMzMzELMAkGA1UEBhMCR0IxDzANBgNV +# BAcTBkxvbmRvbjEaMBgGA1UEChMRU3RyYXRpcyBHcm91cCBMdGQxGjAYBgNVBAMT +# EVN0cmF0aXMgR3JvdXAgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +# AQEAkn7b1/xIuE2TqJe+loS4l6g7UOKBpivRPKt1wIoaSj0sc1cbnlFSzDOcAnCS +# WVsHtX99Yk4mW9cCiWXuP0EcURF5pgu9TWiALZRVXefD0w3Luio/Ej4uQ741+Tf6 +# hCrgYn9Ui/a8VZB+dOF2I3Ixq0Y+dZQz61Ovp7FfRviBJBkN2cCY1YJEcAr1Um3Y +# EmxpKEAb3OfY9AXZCT22mCnvMwpPK80mY6e1T/928wrwHfEU+0IVl/blhEYvxtNf +# wgZVVHQ4wvmomW20iA+KyOc3EXbhJhOCP+4hrF6A6eUcrxyJd0wRFJkBd7B6LzKZ +# OyIfjIaHmCDZIaCjbolyOLVl8wIDAQABo4IB6DCCAeQwHwYDVR0jBBgwFoAUj+h+ +# 8G0yagAFI8dwl2o6kP9r6tQwHQYDVR0OBBYEFK/Xc5t+2Ql1Dlo2AuMKBW1RIL71 +# MCYGA1UdEQQfMB2gGwYIKwYBBQUHCAOgDzANDAtHQi0xMDU1MDMzMzAOBgNVHQ8B +# Af8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwewYDVR0fBHQwcjA3oDWgM4Yx +# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0VWQ29kZVNpZ25pbmdTSEEyLWcxLmNy +# bDA3oDWgM4YxaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0VWQ29kZVNpZ25pbmdT +# SEEyLWcxLmNybDBKBgNVHSAEQzBBMDYGCWCGSAGG/WwDAjApMCcGCCsGAQUFBwIB +# FhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwBwYFZ4EMAQMwfgYIKwYBBQUH +# AQEEcjBwMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wSAYI +# KwYBBQUHMAKGPGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEVW +# Q29kZVNpZ25pbmdDQS1TSEEyLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEB +# CwUAA4IBAQA6C/3OlxNLLPbXdaD6o5toGRufe+oRFPgCxkLuhHW10VM2tGJRLMgq +# dHwE8TRnefYibr+TyYzWd/XNN6DEks8T73GUNZxkwhWpAqMBiWDiSvPe3OcnX5J2 +# V6OKsynobw/F+hivNAOa98HhRPuh4dZp/Xswl+mZY+eKSLJ539pHpeelKobnYNIu +# PFh1iYbk5+80JzqppOSrKagZ0ahHriJRJrqPkTjv+oRmp2o5vEYlAiEhQoyfKXLN +# zNf99EU1qtsH5vVehUgluP9oHBABMYU+bAKXYeULWFRrSSkFowpp7mfAAbKZX4Hf +# QwCNt6Wh8JhqOdXuudIwNiAKUC6NokxRMIIGvDCCBaSgAwIBAgIQA/G04V86gvEU +# lniz19hHXDANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM +# RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSswKQYDVQQD +# EyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTEyMDQxODEy +# MDAwMFoXDTI3MDQxODEyMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERp +# Z2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMi +# RGlnaUNlcnQgRVYgQ29kZSBTaWduaW5nIENBIChTSEEyKTCCASIwDQYJKoZIhvcN +# AQEBBQADggEPADCCAQoCggEBAKdT+g+ytRPxZM+EgPyugDXRttfHoyysGiys8YSs +# OjUSOpKRulfkxMnzL6hIPLfWbtyXIrpReWGvQy8Nt5u0STGuRFg+pKGWp4dPI37D +# bGUkkFU+ocojfMVC6cR6YkWbfd5jdMueYyX4hJqarUVPrn0fyBPLdZvJ4eGK+AsM +# mPTKPtBFqnoepViTNjS+Ky4rMVhmtDIQn53wUqHv6D7TdvJAWtz6aj0bS612sIxc +# 7ja6g+owqEze8QsqWEGIrgCJqwPRFoIgInbrXlQ4EmLh0nAk2+0fcNJkCYAt4rad +# zh/yuyHzbNvYsxl7ilCf7+w2Clyat0rTCKA5ef3dvz06CSUCAwEAAaOCA1gwggNU +# MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoG +# CCsGAQUFBwMDMH8GCCsGAQUFBwEBBHMwcTAkBggrBgEFBQcwAYYYaHR0cDovL29j +# c3AuZGlnaWNlcnQuY29tMEkGCCsGAQUFBzAChj1odHRwOi8vY2FjZXJ0cy5kaWdp +# Y2VydC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3J0MIGPBgNV +# HR8EgYcwgYQwQKA+oDyGOmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy +# dEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwQKA+oDyGOmh0dHA6Ly9jcmw0LmRp +# Z2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwggHE +# BgNVHSAEggG7MIIBtzCCAbMGCWCGSAGG/WwDAjCCAaQwOgYIKwYBBQUHAgEWLmh0 +# dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0wggFk +# BggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBz +# ACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBz +# ACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBD +# AGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBp +# AG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBo +# ACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBl +# ACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAg +# AHIAZQBmAGUAcgBlAG4AYwBlAC4wHQYDVR0OBBYEFI/ofvBtMmoABSPHcJdqOpD/ +# a+rUMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEB +# CwUAA4IBAQAZM0oMgTM32602yeTJOru1Gy56ouL0Q0IXnr9OoU3hsdvpgd2fAfLk +# iNXp/gn9IcHsXYDS8NbBQ8L+dyvb+deRM85s1bIZO+Yu1smTT4hAjs3h9X7xD8ZZ +# VnLo62pBvRzVRtV8ScpmOBXBv+CRcHeH3MmNMckMKaIz7Y3ih82JjT8b/9XgGpeL +# fNpt+6jGsjpma3sBs83YpjTsEgGrlVilxFNXqGDm5wISoLkjZKJNu3yBJWQhvs/u +# QhhDl7ulNwavTf8mpU1hS+xGQbhlzrh5ngiWC4GMijuPx5mMoypumG1eYcaWt4q5 +# YS2TuOsOBEPX9f6m8GLUmWqlwcHwZJSAMYICIjCCAh4CAQEwgYAwbDELMAkGA1UE +# BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj +# ZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgRVYgQ29kZSBTaWduaW5nIENBIChT +# SEEyKQIQCrk836uc/wPyOiuycqPb5zAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIB +# DDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEE +# AYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUAxIlQa3dMYB7 +# +VkkQtrmwT6arYAwDQYJKoZIhvcNAQEBBQAEggEAgRJPaiWSi/TUGvbDwGhj8IWh +# fHy7pqiTJ1K9NXMYXOXsoQ2gS2LwxqYJAslm0Sx3VNjlJKyRBN1YeYBi2SGZF6VN +# 4MgEs9/80pyzmxikpYh+1QQ+A/O6VxXqKTqGXTD8y1cV6f5EXvw770OIA3TbYnNt +# HGb7fxPrlSFRp+GrYcmIfvxOuA7w2vg8M/MFG20HPkcVfzJaOTZWkbc5/CpunNKU +# s3uMx9dLhe4102GhsnQAbk7fs/sqOS1MToSZQ3uScxqJ+ftyBBFzEfdpFtOxpNug +# DfXCbGTDZpGNVK3N6/zl5Ild7+MXxCeO2/7+b4Xz+uY8UAwBEx78hehPMUJh2w== +# SIG # End signature block diff --git a/Scripts/Masternode-Launch-Script.ps1 b/Scripts/Masternode-Launch-Script.ps1 new file mode 100644 index 0000000000..89c981990b --- /dev/null +++ b/Scripts/Masternode-Launch-Script.ps1 @@ -0,0 +1,173 @@ +Write-Host STRAX Masternode Launch Script - Version 2 -ForegroundColor Cyan +"" +"" +Start-Sleep 5 + +Clear-Host +$mainChainAPIPort = '17103' +$sideChainAPIPort = '37223' +$sidechainMasternodesRepo = "https://github.com/stratisproject/StratisFullNode.git" +$sidechainMasternodeBranch = "SidechainMasternode" +$stratisMasternodeDashboardRepo = "https://github.com/stratisproject/StratisMasternodeDashboard" + +#Create Functions +function Get-TimeStamp +{ + return "[{0:dd/MM/yy} {0:HH:mm:ss}]" -f (Get-Date) +} + +#Check for pre-requisites +$dotnetVersion = dotnet --list-sdks +if ( -not ($dotnetVersion -like "6.0.*") ) +{ + Write-Host "ERROR: .NET Core 6.0 SDK or above not found, please download and install .NET Core SDK 6.0" -ForegroundColor Red + Start-Sleep 30 + Exit +} + +$gitVersion = git --version +if ( -not ($gitVersion -ne $null) ) +{ + Write-Host "ERROR: git not found, please download and install git" -ForegroundColor Red + Start-Sleep 30 + Exit +} + +$pwshVersion = $PSVersionTable.PSVersion.ToString() +if ( -not ($pwshVersion -gt "7.1") ) +{ + Write-Host "ERROR: PowerShell Core not found, please launch this script using 'PWSH' or download and install PowerShell 7 or greater" -ForegroundColor Red + Start-Sleep 30 + Exit +} + +#Set Required Environment Variables +if ($IsWindows) +{ + $mainChainDataDir = "$env:APPDATA\StratisNode\strax\StraxMain" + $sideChainDataDir = "$env:APPDATA\StratisNode\cirrus\CirrusMain" + $cloneDir = "$HOME\Desktop\STRAX-SidechainMasternodes" + $stratisMasternodeDashboardCloneDir = $cloneDir.Replace('SidechainMasternodes','StratisMasternodeDashboard') +} + Else + { + Write-Host "ERROR: Windows OS was not detected." -ForegroundColor Red + Start-Sleep 10 + Exit + } + +#Code Update +if ( -not ( Test-Path -Path $cloneDir\src\Stratis.StraxD -ErrorAction SilentlyContinue ) ) +{ + Remove-Item -Path $cloneDir -Force -Recurse -ErrorAction SilentlyContinue +} + +Write-Host (Get-TimeStamp) INFO: "Checking for updates.." -ForegroundColor Yellow +if ( -not ( Test-Path -Path $CloneDir -ErrorAction SilentlyContinue) ) +{ + Write-Host (Get-TimeStamp) INFO: "Cloning SidechainMasternodes Branch" -ForegroundColor Cyan + Start-Process git.exe -ArgumentList "clone --recurse-submodules $sidechainMasternodesRepo -b $sidechainMasternodeBranch $cloneDir" -Wait +} + Else + { + Set-Location $cloneDir + Start-Process git.exe -ArgumentList "pull" -Wait + } + +if ( -not ( Test-Path -Path $stratisMasternodeDashboardCloneDir -ErrorAction SilentlyContinue ) ) +{ + Write-Host (Get-TimeStamp) INFO: "Cloning Stratis Masternode Dashboard" -ForegroundColor Cyan + Start-Process git.exe -ArgumentList "clone $stratisMasternodeDashboardRepo $stratisMasternodeDashboardCloneDir" -Wait +} + Else + { + Set-Location $stratisMasternodeDashboardCloneDir + Start-Process git.exe -ArgumentList "pull" -Wait + } + +#Call Launch Script +Set-Location $cloneDir\Scripts\ +& '.\LaunchSidechainMasternode.ps1' + +# SIG # Begin signature block +# MIIO+gYJKoZIhvcNAQcCoIIO6zCCDucCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB +# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR +# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUcq/jwj/6Awtj69NhzCSzoIm6 +# 2VigggxCMIIFfjCCBGagAwIBAgIQCrk836uc/wPyOiuycqPb5zANBgkqhkiG9w0B +# AQsFADBsMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD +# VQQLExB3d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBFViBDb2Rl +# IFNpZ25pbmcgQ0EgKFNIQTIpMB4XDTIxMDQyMjAwMDAwMFoXDTI0MDcxOTIzNTk1 +# OVowgZ0xHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYBBAGC +# NzwCAQMTAkdCMREwDwYDVQQFEwgxMDU1MDMzMzELMAkGA1UEBhMCR0IxDzANBgNV +# BAcTBkxvbmRvbjEaMBgGA1UEChMRU3RyYXRpcyBHcm91cCBMdGQxGjAYBgNVBAMT +# EVN0cmF0aXMgR3JvdXAgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +# AQEAkn7b1/xIuE2TqJe+loS4l6g7UOKBpivRPKt1wIoaSj0sc1cbnlFSzDOcAnCS +# WVsHtX99Yk4mW9cCiWXuP0EcURF5pgu9TWiALZRVXefD0w3Luio/Ej4uQ741+Tf6 +# hCrgYn9Ui/a8VZB+dOF2I3Ixq0Y+dZQz61Ovp7FfRviBJBkN2cCY1YJEcAr1Um3Y +# EmxpKEAb3OfY9AXZCT22mCnvMwpPK80mY6e1T/928wrwHfEU+0IVl/blhEYvxtNf +# wgZVVHQ4wvmomW20iA+KyOc3EXbhJhOCP+4hrF6A6eUcrxyJd0wRFJkBd7B6LzKZ +# OyIfjIaHmCDZIaCjbolyOLVl8wIDAQABo4IB6DCCAeQwHwYDVR0jBBgwFoAUj+h+ +# 8G0yagAFI8dwl2o6kP9r6tQwHQYDVR0OBBYEFK/Xc5t+2Ql1Dlo2AuMKBW1RIL71 +# MCYGA1UdEQQfMB2gGwYIKwYBBQUHCAOgDzANDAtHQi0xMDU1MDMzMzAOBgNVHQ8B +# Af8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwewYDVR0fBHQwcjA3oDWgM4Yx +# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0VWQ29kZVNpZ25pbmdTSEEyLWcxLmNy +# bDA3oDWgM4YxaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0VWQ29kZVNpZ25pbmdT +# SEEyLWcxLmNybDBKBgNVHSAEQzBBMDYGCWCGSAGG/WwDAjApMCcGCCsGAQUFBwIB +# FhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwBwYFZ4EMAQMwfgYIKwYBBQUH +# AQEEcjBwMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wSAYI +# KwYBBQUHMAKGPGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEVW +# Q29kZVNpZ25pbmdDQS1TSEEyLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEB +# CwUAA4IBAQA6C/3OlxNLLPbXdaD6o5toGRufe+oRFPgCxkLuhHW10VM2tGJRLMgq +# dHwE8TRnefYibr+TyYzWd/XNN6DEks8T73GUNZxkwhWpAqMBiWDiSvPe3OcnX5J2 +# V6OKsynobw/F+hivNAOa98HhRPuh4dZp/Xswl+mZY+eKSLJ539pHpeelKobnYNIu +# PFh1iYbk5+80JzqppOSrKagZ0ahHriJRJrqPkTjv+oRmp2o5vEYlAiEhQoyfKXLN +# zNf99EU1qtsH5vVehUgluP9oHBABMYU+bAKXYeULWFRrSSkFowpp7mfAAbKZX4Hf +# QwCNt6Wh8JhqOdXuudIwNiAKUC6NokxRMIIGvDCCBaSgAwIBAgIQA/G04V86gvEU +# lniz19hHXDANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM +# RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSswKQYDVQQD +# EyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTEyMDQxODEy +# MDAwMFoXDTI3MDQxODEyMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERp +# Z2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMi +# RGlnaUNlcnQgRVYgQ29kZSBTaWduaW5nIENBIChTSEEyKTCCASIwDQYJKoZIhvcN +# AQEBBQADggEPADCCAQoCggEBAKdT+g+ytRPxZM+EgPyugDXRttfHoyysGiys8YSs +# OjUSOpKRulfkxMnzL6hIPLfWbtyXIrpReWGvQy8Nt5u0STGuRFg+pKGWp4dPI37D +# bGUkkFU+ocojfMVC6cR6YkWbfd5jdMueYyX4hJqarUVPrn0fyBPLdZvJ4eGK+AsM +# mPTKPtBFqnoepViTNjS+Ky4rMVhmtDIQn53wUqHv6D7TdvJAWtz6aj0bS612sIxc +# 7ja6g+owqEze8QsqWEGIrgCJqwPRFoIgInbrXlQ4EmLh0nAk2+0fcNJkCYAt4rad +# zh/yuyHzbNvYsxl7ilCf7+w2Clyat0rTCKA5ef3dvz06CSUCAwEAAaOCA1gwggNU +# MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoG +# CCsGAQUFBwMDMH8GCCsGAQUFBwEBBHMwcTAkBggrBgEFBQcwAYYYaHR0cDovL29j +# c3AuZGlnaWNlcnQuY29tMEkGCCsGAQUFBzAChj1odHRwOi8vY2FjZXJ0cy5kaWdp +# Y2VydC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3J0MIGPBgNV +# HR8EgYcwgYQwQKA+oDyGOmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy +# dEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwQKA+oDyGOmh0dHA6Ly9jcmw0LmRp +# Z2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwggHE +# BgNVHSAEggG7MIIBtzCCAbMGCWCGSAGG/WwDAjCCAaQwOgYIKwYBBQUHAgEWLmh0 +# dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0wggFk +# BggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBz +# ACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBz +# ACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBD +# AGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBp +# AG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBo +# ACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBl +# ACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAg +# AHIAZQBmAGUAcgBlAG4AYwBlAC4wHQYDVR0OBBYEFI/ofvBtMmoABSPHcJdqOpD/ +# a+rUMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEB +# CwUAA4IBAQAZM0oMgTM32602yeTJOru1Gy56ouL0Q0IXnr9OoU3hsdvpgd2fAfLk +# iNXp/gn9IcHsXYDS8NbBQ8L+dyvb+deRM85s1bIZO+Yu1smTT4hAjs3h9X7xD8ZZ +# VnLo62pBvRzVRtV8ScpmOBXBv+CRcHeH3MmNMckMKaIz7Y3ih82JjT8b/9XgGpeL +# fNpt+6jGsjpma3sBs83YpjTsEgGrlVilxFNXqGDm5wISoLkjZKJNu3yBJWQhvs/u +# QhhDl7ulNwavTf8mpU1hS+xGQbhlzrh5ngiWC4GMijuPx5mMoypumG1eYcaWt4q5 +# YS2TuOsOBEPX9f6m8GLUmWqlwcHwZJSAMYICIjCCAh4CAQEwgYAwbDELMAkGA1UE +# BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj +# ZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgRVYgQ29kZSBTaWduaW5nIENBIChT +# SEEyKQIQCrk836uc/wPyOiuycqPb5zAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIB +# DDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEE +# AYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUPetli19yk6kZ +# pFFOeCjk1XuwAx4wDQYJKoZIhvcNAQEBBQAEggEAGZs4PMx8MRyIgZjQhYZ4FuVS +# +XAQx9PsLWezcsrHJMSubHCcbfzPRcuVqYmDPZV4IpqJc8iGtl8yvhWmJNfr3jhw +# lOrqgA2n4yxJTjINWNLitHmUzkQ/dHDIz0j2CF4qux9yM5uwqD6uVH/6G2utCN0g +# LwnzW/39kNZBS4GxGBk+RgsNveTprj4IwglUZqaDzy0EZ68St07cZHLCnwkJuytv +# 6/im00MU510CCpMagUZDIOllxYHTxNFsIOBxcHiVyLth+J1dvFwNcjqgfVG6DQpS +# IYDte17+GdoN7jCK0Q6QrgBsSqlbcyYq3nk4FEpsa2BA7wzosDnhZVOXqd7DxQ== +# SIG # End signature block diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b11ea6bc7d..6d95b31526 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,4 +2,7 @@ $(MSBuildThisFileDirectory)None.ruleset + + false + diff --git a/src/FederationSetup/FederationSetup.csproj b/src/FederationSetup/FederationSetup.csproj index 6f5cb69ec7..03fb098cd9 100644 --- a/src/FederationSetup/FederationSetup.csproj +++ b/src/FederationSetup/FederationSetup.csproj @@ -2,8 +2,8 @@ Exe - netcoreapp3.1 - 1.3.2.4 + net6.0 + 1.4.0.7 Stratis Group Ltd. diff --git a/src/FodyNlogAdapter/FodyNlogAdapter.csproj b/src/FodyNlogAdapter/FodyNlogAdapter.csproj index e5efc24fe9..75ed91c0fb 100644 --- a/src/FodyNlogAdapter/FodyNlogAdapter.csproj +++ b/src/FodyNlogAdapter/FodyNlogAdapter.csproj @@ -1,9 +1,9 @@  - netcoreapp3.1 + net6.0 FodyNlogAdapter - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. Stratis.Utils.FodyNlogAdapter diff --git a/src/NBitcoin.Tests/NBitcoin.Tests.csproj b/src/NBitcoin.Tests/NBitcoin.Tests.csproj index 4847eb23b0..7f1466e35c 100644 --- a/src/NBitcoin.Tests/NBitcoin.Tests.csproj +++ b/src/NBitcoin.Tests/NBitcoin.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/NBitcoin/BitcoinStream.cs b/src/NBitcoin/BitcoinStream.cs index 05a166bb57..f482659099 100644 --- a/src/NBitcoin/BitcoinStream.cs +++ b/src/NBitcoin/BitcoinStream.cs @@ -49,7 +49,7 @@ public static IDisposable Nothing // TODO: Make NetworkOptions required in the constructors of this class. public partial class BitcoinStream { - private int maxArraySize = 1024 * 1024; + private int maxArraySize = 1024 * 1024 * 4; public int MaxArraySize { get diff --git a/src/NBitcoin/BlockStake.cs b/src/NBitcoin/BlockStake.cs index cb56134e95..1521919b88 100644 --- a/src/NBitcoin/BlockStake.cs +++ b/src/NBitcoin/BlockStake.cs @@ -263,6 +263,14 @@ public override Transaction CreateTransaction(string hex) return new PosTransaction(hex); } + /// + public override Transaction CreateTransaction(string hex, ProtocolVersion protocolVersion) + { + var transaction = new PosTransaction(); + transaction.FromBytes(Encoders.Hex.DecodeData(hex), protocolVersion); + return transaction; + } + /// public override Transaction CreateTransaction(byte[] bytes) { diff --git a/src/NBitcoin/ConsensusFactory.cs b/src/NBitcoin/ConsensusFactory.cs index a2048d1a54..b9cd6d384a 100644 --- a/src/NBitcoin/ConsensusFactory.cs +++ b/src/NBitcoin/ConsensusFactory.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Reflection; using NBitcoin.DataEncoders; +using NBitcoin.Protocol; namespace NBitcoin { @@ -178,6 +179,17 @@ public virtual Transaction CreateTransaction(string hex) return transaction; } + /// + /// Create a instance from a hex string representation. + /// Allows the protocol version to be overridden from the node's default. + /// + public virtual Transaction CreateTransaction(string hex, ProtocolVersion protocolVersion) + { + var transaction = new Transaction(); + transaction.FromBytes(Encoders.Hex.DecodeData(hex), protocolVersion); + return transaction; + } + /// /// Create a instance from a byte array representation. /// diff --git a/src/NBitcoin/NBitcoin.csproj b/src/NBitcoin/NBitcoin.csproj index ae04577363..6d3e74d183 100644 --- a/src/NBitcoin/NBitcoin.csproj +++ b/src/NBitcoin/NBitcoin.csproj @@ -7,11 +7,11 @@ - 4.0.0.86 + 4.0.0.87 - netcoreapp3.1 + net6.0 NBitcoin NStratis NStratis diff --git a/src/NBitcoin/Network.cs b/src/NBitcoin/Network.cs index eb69a200d1..17603173e8 100644 --- a/src/NBitcoin/Network.cs +++ b/src/NBitcoin/Network.cs @@ -984,6 +984,11 @@ public Transaction CreateTransaction(string hex) return this.Consensus.ConsensusFactory.CreateTransaction(hex); } + public Transaction CreateTransaction(string hex, ProtocolVersion protocolVersion) + { + return this.Consensus.ConsensusFactory.CreateTransaction(hex, protocolVersion); + } + public Transaction CreateTransaction(byte[] bytes) { return this.Consensus.ConsensusFactory.CreateTransaction(bytes); diff --git a/src/NBitcoin/PartialTransactionScriptEvaluationContext.cs b/src/NBitcoin/PartialTransactionScriptEvaluationContext.cs new file mode 100644 index 0000000000..0b46b83724 --- /dev/null +++ b/src/NBitcoin/PartialTransactionScriptEvaluationContext.cs @@ -0,0 +1,75 @@ +using System.Linq; + +namespace NBitcoin +{ + public class PartialTransactionScriptEvaluationContext : ScriptEvaluationContext + { + public PartialTransactionScriptEvaluationContext(Network network) : base(network) + { + } + + public override (bool success, bool isError) DetermineSignatures(ref int i, bool fRequireMinimal, ref int nKeysCount, int pbegincodehash, Script s, int hashversion, ref int ikey, TransactionChecker checker) + { + int nSigsCount = new CScriptNum(this._stack.Top(-i), fRequireMinimal).getint(); + if (nSigsCount < 0 || nSigsCount > nKeysCount) + return (false, !SetError(ScriptError.SigCount)); + + int isig = ++i; + i += nSigsCount; + if (this._stack.Count < i) + return (false, !SetError(ScriptError.InvalidStackOperation)); + + // Subset of script starting at the most recent codeseparator + var scriptCode = new Script(s._Script.Skip(pbegincodehash).ToArray()); + // Drop the signatures, since there's no way for a signature to sign itself + for (int k = 0; k < nSigsCount; k++) + { + byte[] vchSig = this._stack.Top(-isig - k); + if (hashversion == (int)HashVersion.Original) + scriptCode.FindAndDelete(vchSig); + } + + bool fSuccess = true; + while (fSuccess && nSigsCount > 0) + { + byte[] vchSig = this._stack.Top(-isig); + + // If the signature at the particular index in the stack is empty, + // move onto the next one. + if(vchSig.Length == 0) + { + isig++; + continue; + } + + byte[] vchPubKey = this._stack.Top(-ikey); + + // Note how this makes the exact order of pubkey/signature evaluation + // distinguishable by CHECKMULTISIG NOT if the STRICTENC flag is set. + // See the script_(in)valid tests for details. + if (!CheckSignatureEncoding(vchSig) || !CheckPubKeyEncoding(vchPubKey, hashversion)) + { + // serror is set + return (false, true); + } + + bool fOk = CheckSig(vchSig, vchPubKey, scriptCode, checker, hashversion); + + if (fOk) + { + isig++; + nSigsCount--; + } + ikey++; + nKeysCount--; + + // If there are more signatures left than keys left, + // then too many signatures have failed + if (nSigsCount > nKeysCount) + fSuccess = false; + } + + return (fSuccess, false); + } + } +} diff --git a/src/NBitcoin/ScriptEvaluationContext.cs b/src/NBitcoin/ScriptEvaluationContext.cs index b473e8a34d..75937018eb 100644 --- a/src/NBitcoin/ScriptEvaluationContext.cs +++ b/src/NBitcoin/ScriptEvaluationContext.cs @@ -156,11 +156,12 @@ public uint256 Hash internal set; } } + public class ScriptEvaluationContext { public Network Network { get; } - private class CScriptNum + protected class CScriptNum { private const long nMaxNumSize = 4; /** @@ -176,13 +177,14 @@ public CScriptNum(long n) { this.m_value = n; } + private long m_value; public CScriptNum(byte[] vch, bool fRequireMinimal) : this(vch, fRequireMinimal, 4) { - } + public CScriptNum(byte[] vch, bool fRequireMinimal, long nMaxNumSize) { if(vch.Length > nMaxNumSize) @@ -400,7 +402,7 @@ private static long set_vch(byte[] vch) } } - private ContextStack _stack = new ContextStack(); + protected ContextStack _stack = new ContextStack(); public ContextStack Stack { @@ -1461,55 +1463,9 @@ private bool EvalScript(Script s, TransactionChecker checker, int hashversion) if(this._stack.Count < i) return SetError(ScriptError.InvalidStackOperation); - int nSigsCount = new CScriptNum(this._stack.Top(-i), fRequireMinimal).getint(); - if(nSigsCount < 0 || nSigsCount > nKeysCount) - return SetError(ScriptError.SigCount); - - int isig = ++i; - i += nSigsCount; - if(this._stack.Count < i) - return SetError(ScriptError.InvalidStackOperation); - - // Subset of script starting at the most recent codeseparator - var scriptCode = new Script(s._Script.Skip(pbegincodehash).ToArray()); - // Drop the signatures, since there's no way for a signature to sign itself - for(int k = 0; k < nSigsCount; k++) - { - byte[] vchSig = this._stack.Top(-isig - k); - if(hashversion == (int)HashVersion.Original) - scriptCode.FindAndDelete(vchSig); - } - - bool fSuccess = true; - while(fSuccess && nSigsCount > 0) - { - byte[] vchSig = this._stack.Top(-isig); - byte[] vchPubKey = this._stack.Top(-ikey); - - // Note how this makes the exact order of pubkey/signature evaluation - // distinguishable by CHECKMULTISIG NOT if the STRICTENC flag is set. - // See the script_(in)valid tests for details. - if(!CheckSignatureEncoding(vchSig) || !CheckPubKeyEncoding(vchPubKey, hashversion)) - { - // serror is set - return false; - } - - bool fOk = CheckSig(vchSig, vchPubKey, scriptCode, checker, hashversion); - - if(fOk) - { - isig++; - nSigsCount--; - } - ikey++; - nKeysCount--; - - // If there are more signatures left than keys left, - // then too many signatures have failed - if(nSigsCount > nKeysCount) - fSuccess = false; - } + (bool fSuccess, bool isError) = DetermineSignatures(ref i, fRequireMinimal, ref nKeysCount, pbegincodehash, s, hashversion, ref ikey, checker); + if (isError) + return false; // Clean up stack of actual arguments while(i-- > 1) @@ -1569,6 +1525,62 @@ private bool EvalScript(Script s, TransactionChecker checker, int hashversion) return SetSuccess(ScriptError.OK); } + public virtual (bool success, bool isError) DetermineSignatures(ref int i, bool fRequireMinimal, ref int nKeysCount,int pbegincodehash, Script s, int hashversion, ref int ikey, TransactionChecker checker) + { + int nSigsCount = new CScriptNum(this._stack.Top(-i), fRequireMinimal).getint(); + if (nSigsCount < 0 || nSigsCount > nKeysCount) + return (false, !SetError(ScriptError.SigCount)); + + int isig = ++i; + i += nSigsCount; + if (this._stack.Count < i) + return (false, !SetError(ScriptError.InvalidStackOperation)); + + // Subset of script starting at the most recent codeseparator + var scriptCode = new Script(s._Script.Skip(pbegincodehash).ToArray()); + + // Drop the signatures, since there's no way for a signature to sign itself + for (int k = 0; k < nSigsCount; k++) + { + byte[] vchSig = this._stack.Top(-isig - k); + if (hashversion == (int)HashVersion.Original) + scriptCode.FindAndDelete(vchSig); + } + + bool fSuccess = true; + while (fSuccess && nSigsCount > 0) + { + byte[] vchSig = this._stack.Top(-isig); + byte[] vchPubKey = this._stack.Top(-ikey); + + // Note how this makes the exact order of pubkey/signature evaluation + // distinguishable by CHECKMULTISIG NOT if the STRICTENC flag is set. + // See the script_(in)valid tests for details. + if (!CheckSignatureEncoding(vchSig) || !CheckPubKeyEncoding(vchPubKey, hashversion)) + { + // serror is set + return (false, true); + } + + bool fOk = CheckSig(vchSig, vchPubKey, scriptCode, checker, hashversion); + + if (fOk) + { + isig++; + nSigsCount--; + } + ikey++; + nKeysCount--; + + // If there are more signatures left than keys left, + // then too many signatures have failed + if (nSigsCount > nKeysCount) + fSuccess = false; + } + + return (fSuccess, false); + } + private bool CheckSequence(CScriptNum nSequence, TransactionChecker checker) { Transaction txTo = checker.Transaction; @@ -1663,7 +1675,7 @@ private bool SetSuccess(ScriptError scriptError) return true; } - private bool SetError(ScriptError scriptError) + protected bool SetError(ScriptError scriptError) { this.Error = scriptError; return false; @@ -1726,7 +1738,7 @@ internal bool CheckSignatureEncoding(byte[] vchSig) return true; } - private bool CheckPubKeyEncoding(byte[] vchPubKey, int sigversion) + protected bool CheckPubKeyEncoding(byte[] vchPubKey, int sigversion) { if((this.ScriptVerify & ScriptVerify.StrictEnc) != 0 && !IsCompressedOrUncompressedPubKey(vchPubKey)) { @@ -2013,11 +2025,11 @@ public IEnumerable SignedHashes } } - public bool CheckSig(TransactionSignature signature, PubKey pubKey, Script scriptPubKey, IndexedTxIn txIn) { return CheckSig(signature, pubKey, scriptPubKey, txIn.Transaction, txIn.Index); } + public bool CheckSig(TransactionSignature signature, PubKey pubKey, Script scriptPubKey, Transaction txTo, uint nIn) { return CheckSig(signature.ToBytes(), pubKey.ToBytes(), scriptPubKey, txTo, (int)nIn); @@ -2033,7 +2045,7 @@ public bool CheckSig(byte[] vchSig, byte[] vchPubKey, Script scriptCode, Transac return CheckSig(vchSig, vchPubKey, scriptCode, new TransactionChecker(txTo, nIn), 0); } - private bool CheckSig(byte[] vchSig, byte[] vchPubKey, Script scriptCode, TransactionChecker checker, int sigversion) + protected bool CheckSig(byte[] vchSig, byte[] vchPubKey, Script scriptCode, TransactionChecker checker, int sigversion) { PubKey pubkey = null; try diff --git a/src/Stratis.Benchmark/Stratis.Benchmark.csproj b/src/Stratis.Benchmark/Stratis.Benchmark.csproj index a9fd7d2b55..1e581d943c 100644 --- a/src/Stratis.Benchmark/Stratis.Benchmark.csproj +++ b/src/Stratis.Benchmark/Stratis.Benchmark.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 diff --git a/src/Stratis.Bitcoin.Api.Tests/Stratis.Bitcoin.Api.Tests.csproj b/src/Stratis.Bitcoin.Api.Tests/Stratis.Bitcoin.Api.Tests.csproj index 4a33a9b80a..62f2d0a6b3 100644 --- a/src/Stratis.Bitcoin.Api.Tests/Stratis.Bitcoin.Api.Tests.csproj +++ b/src/Stratis.Bitcoin.Api.Tests/Stratis.Bitcoin.Api.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Api.Tests Stratis.Bitcoin.Api.Tests true diff --git a/src/Stratis.Bitcoin.Cli/Stratis.Bitcoin.Cli.csproj b/src/Stratis.Bitcoin.Cli/Stratis.Bitcoin.Cli.csproj index e8360ce60e..74eeb55cb1 100644 --- a/src/Stratis.Bitcoin.Cli/Stratis.Bitcoin.Cli.csproj +++ b/src/Stratis.Bitcoin.Cli/Stratis.Bitcoin.Cli.csproj @@ -2,8 +2,8 @@ Exe - netcoreapp3.1 - 1.3.2.4 + net6.0 + 1.4.0.7 Stratis Group Ltd. Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.Api/NodeController.cs b/src/Stratis.Bitcoin.Features.Api/NodeController.cs index 17019e21c6..cbd28eb925 100644 --- a/src/Stratis.Bitcoin.Features.Api/NodeController.cs +++ b/src/Stratis.Bitcoin.Features.Api/NodeController.cs @@ -158,7 +158,8 @@ public IActionResult Status([FromQuery] bool publish) CoinTicker = this.network.CoinTicker, State = this.fullNode.State.ToString(), BestPeerHeight = this.chainState.BestPeerTip?.Height, - InIbd = this.initialBlockDownloadState.IsInitialBlockDownload() + InIbd = this.initialBlockDownloadState.IsInitialBlockDownload(), + NodeStarted = this.fullNode.StartTime }; try diff --git a/src/Stratis.Bitcoin.Features.Api/Stratis.Bitcoin.Features.Api.csproj b/src/Stratis.Bitcoin.Features.Api/Stratis.Bitcoin.Features.Api.csproj index fabe69559d..77526bd585 100644 --- a/src/Stratis.Bitcoin.Features.Api/Stratis.Bitcoin.Features.Api.csproj +++ b/src/Stratis.Bitcoin.Features.Api/Stratis.Bitcoin.Features.Api.csproj @@ -1,16 +1,17 @@  - netcoreapp3.1 + net6.0 true Stratis.Bitcoin.Features.Api Library Stratis.Features.Api - 1.3.2.4 + 1.4.0.7 False library + false true Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.BlockStore.Tests/Stratis.Bitcoin.Features.BlockStore.Tests.csproj b/src/Stratis.Bitcoin.Features.BlockStore.Tests/Stratis.Bitcoin.Features.BlockStore.Tests.csproj index 4c42fc3ecb..7665980c33 100644 --- a/src/Stratis.Bitcoin.Features.BlockStore.Tests/Stratis.Bitcoin.Features.BlockStore.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.BlockStore.Tests/Stratis.Bitcoin.Features.BlockStore.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.BlockStore.Tests Stratis.Bitcoin.Features.BlockStore.Tests true diff --git a/src/Stratis.Bitcoin.Features.BlockStore/AddressIndexing/AddressIndexer.cs b/src/Stratis.Bitcoin.Features.BlockStore/AddressIndexing/AddressIndexer.cs index bd05214170..7a5b2e23ed 100644 --- a/src/Stratis.Bitcoin.Features.BlockStore/AddressIndexing/AddressIndexer.cs +++ b/src/Stratis.Bitcoin.Features.BlockStore/AddressIndexing/AddressIndexer.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using LiteDB; using Microsoft.Extensions.Logging; +using Mono.Posix; using NBitcoin; using Stratis.Bitcoin.AsyncWork; using Stratis.Bitcoin.Builder.Feature; @@ -16,9 +17,11 @@ using Stratis.Bitcoin.Configuration.Logging; using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Controllers.Models; +using Stratis.Bitcoin.EventBus.CoreEvents; using Stratis.Bitcoin.Features.BlockStore.Models; using Stratis.Bitcoin.Interfaces; using Stratis.Bitcoin.Primitives; +using Stratis.Bitcoin.Signals; using Stratis.Bitcoin.Utilities; using FileMode = LiteDB.FileMode; using Script = NBitcoin.Script; @@ -115,6 +118,8 @@ public class AddressIndexer : IAddressIndexer private readonly IUtxoIndexer utxoIndexer; + private readonly ISignals signals; + private Task indexingTask; private DateTime lastFlushTime; @@ -142,8 +147,8 @@ public class AddressIndexer : IAddressIndexer public IFullNodeFeature InitializingFeature { get; set; } - public AddressIndexer(StoreSettings storeSettings, DataFolder dataFolder, Network network, INodeStats nodeStats, - IConsensusManager consensusManager, IAsyncProvider asyncProvider, ChainIndexer chainIndexer, IDateTimeProvider dateTimeProvider, IUtxoIndexer utxoIndexer) + public AddressIndexer(StoreSettings storeSettings, DataFolder dataFolder, Network network, INodeStats nodeStats, + IConsensusManager consensusManager, IAsyncProvider asyncProvider, ChainIndexer chainIndexer, IDateTimeProvider dateTimeProvider, IUtxoIndexer utxoIndexer, ISignals signals) { this.storeSettings = storeSettings; this.network = network; @@ -154,6 +159,7 @@ public AddressIndexer(StoreSettings storeSettings, DataFolder dataFolder, Networ this.dateTimeProvider = dateTimeProvider; this.utxoIndexer = utxoIndexer; this.scriptAddressReader = new ScriptAddressReader(); + this.signals = signals; this.lockObject = new object(); this.flushChangesInterval = TimeSpan.FromMinutes(2); @@ -404,6 +410,8 @@ private void AddInlineStats(StringBuilder benchLog) " AddressCache%: " + this.addressIndexRepository.GetLoadPercentage().ToString().PadRight(8) + "OutPointCache%: " + this.outpointsRepository.GetLoadPercentage().ToString().PadRight(8) + $"Ms/block: {Math.Round(this.averageTimePerBlock.Average, 2)}"); + + this.signals.Publish(new AddressIndexerStatusEvent() { Tip = this.IndexerTip.Height }); } /// Processes a block that was added or removed from the consensus chain. diff --git a/src/Stratis.Bitcoin.Features.BlockStore/BlockStoreBehavior.cs b/src/Stratis.Bitcoin.Features.BlockStore/BlockStoreBehavior.cs index 56efaecfea..97b9e3404b 100644 --- a/src/Stratis.Bitcoin.Features.BlockStore/BlockStoreBehavior.cs +++ b/src/Stratis.Bitcoin.Features.BlockStore/BlockStoreBehavior.cs @@ -139,18 +139,26 @@ private async Task ProcessGetDataAsync(INetworkPeer peer, GetDataPayload getData // TODO: bring logic from core foreach (InventoryVector item in getDataPayload.Inventory.Where(inv => inv.Type.HasFlag(InventoryType.MSG_BLOCK))) { + // We could just check once on entry into this method, but it is possible for the peer's connected status to change + // between block transmission attempts. + if (!peer.IsConnected) + continue; + ChainedHeaderBlock chainedHeaderBlock = this.consensusManager.GetBlockData(item.Hash); if (chainedHeaderBlock?.Block != null) { this.logger.LogDebug("Sending block '{0}' to peer '{1}'.", chainedHeaderBlock.ChainedHeader, peer.RemoteSocketEndpoint); - //TODO strip block of witness if node does not support await peer.SendMessageAsync(new BlockPayload(chainedHeaderBlock.Block.WithOptions(this.ChainIndexer.Network.Consensus.ConsensusFactory, peer.SupportedTransactionOptions))).ConfigureAwait(false); } else { this.logger.LogDebug("Block with hash '{0}' requested from peer '{1}' was not found in store.", item.Hash, peer.RemoteSocketEndpoint); + + // https://btcinformation.org/en/developer-reference#notfound + // https://github.com/bitcoin/bitcoin/pull/2192 + await peer.SendMessageAsync(new NotFoundPayload(InventoryType.MSG_BLOCK, item.Hash)).ConfigureAwait(false); } } } diff --git a/src/Stratis.Bitcoin.Features.BlockStore/Controllers/BlockStoreController.cs b/src/Stratis.Bitcoin.Features.BlockStore/Controllers/BlockStoreController.cs index 1eeda5e795..bd9cb04ab4 100644 --- a/src/Stratis.Bitcoin.Features.BlockStore/Controllers/BlockStoreController.cs +++ b/src/Stratis.Bitcoin.Features.BlockStore/Controllers/BlockStoreController.cs @@ -278,6 +278,7 @@ public IActionResult VerboseAddressesBalancesData([FromBody] string addresses) { try { + // The 'IsNullOrWhiteSpace' is required due to POST passing an empty string instead of null. string[] addressesArray = string.IsNullOrWhiteSpace(addresses) ? new string[] { } : addresses.Split(','); this.logger.LogDebug("Asking data for {0} addresses.", addressesArray.Length); diff --git a/src/Stratis.Bitcoin.Features.BlockStore/Stratis.Bitcoin.Features.BlockStore.csproj b/src/Stratis.Bitcoin.Features.BlockStore/Stratis.Bitcoin.Features.BlockStore.csproj index 5866ec0f50..779fb72276 100644 --- a/src/Stratis.Bitcoin.Features.BlockStore/Stratis.Bitcoin.Features.BlockStore.csproj +++ b/src/Stratis.Bitcoin.Features.BlockStore/Stratis.Bitcoin.Features.BlockStore.csproj @@ -3,7 +3,7 @@ Stratis Bitcoin Features BlockStore Stratis.Bitcoin.Features.BlockStore - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.BlockStore Stratis.Features.BlockStore false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.ColdStaking.Tests/ColdStakingControllerTest.cs b/src/Stratis.Bitcoin.Features.ColdStaking.Tests/ColdStakingControllerTest.cs index 8709fa2ab7..b949e4bb1b 100644 --- a/src/Stratis.Bitcoin.Features.ColdStaking.Tests/ColdStakingControllerTest.cs +++ b/src/Stratis.Bitcoin.Features.ColdStaking.Tests/ColdStakingControllerTest.cs @@ -1082,9 +1082,10 @@ public void ColdStakingWithdrawalToColdWalletAccountThrowsWalletException() /// /// Confirms that trying to withdraw money from a non-existent cold staking account will raise an error. + /// ColdStakingWithdrawalFromNonExistingColdWalletAccountThrowsWalletException /// [Fact] - public void ColdStakingWithdrawalFromNonExistingColdWalletAccountThrowsWalletException() + public void ColdStakingWithdrawalFromNonExistingColdWallet() { this.Initialize(); this.CreateMempoolManager(); diff --git a/src/Stratis.Bitcoin.Features.ColdStaking.Tests/Stratis.Bitcoin.Features.ColdStaking.Tests.csproj b/src/Stratis.Bitcoin.Features.ColdStaking.Tests/Stratis.Bitcoin.Features.ColdStaking.Tests.csproj index 4d81a25e08..a8835ba46f 100644 --- a/src/Stratis.Bitcoin.Features.ColdStaking.Tests/Stratis.Bitcoin.Features.ColdStaking.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.ColdStaking.Tests/Stratis.Bitcoin.Features.ColdStaking.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.ColdStaking.Tests Stratis.Bitcoin.Features.ColdStaking.Tests true diff --git a/src/Stratis.Bitcoin.Features.ColdStaking/Stratis.Bitcoin.Features.ColdStaking.csproj b/src/Stratis.Bitcoin.Features.ColdStaking/Stratis.Bitcoin.Features.ColdStaking.csproj index 4b2e9eb8a8..a6e412c8be 100644 --- a/src/Stratis.Bitcoin.Features.ColdStaking/Stratis.Bitcoin.Features.ColdStaking.csproj +++ b/src/Stratis.Bitcoin.Features.ColdStaking/Stratis.Bitcoin.Features.ColdStaking.csproj @@ -1,13 +1,13 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.ColdStaking Stratis.Features.ColdStaking false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.Consensus.Tests/Stratis.Bitcoin.Features.Consensus.Tests.csproj b/src/Stratis.Bitcoin.Features.Consensus.Tests/Stratis.Bitcoin.Features.Consensus.Tests.csproj index 27e631775c..4734dbeb38 100644 --- a/src/Stratis.Bitcoin.Features.Consensus.Tests/Stratis.Bitcoin.Features.Consensus.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.Consensus.Tests/Stratis.Bitcoin.Features.Consensus.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.Consensus.Tests Stratis.Bitcoin.Features.Consensus.Tests true diff --git a/src/Stratis.Bitcoin.Features.Consensus/Stratis.Bitcoin.Features.Consensus.csproj b/src/Stratis.Bitcoin.Features.Consensus/Stratis.Bitcoin.Features.Consensus.csproj index ff4c21d1f1..70c5bdd849 100644 --- a/src/Stratis.Bitcoin.Features.Consensus/Stratis.Bitcoin.Features.Consensus.csproj +++ b/src/Stratis.Bitcoin.Features.Consensus/Stratis.Bitcoin.Features.Consensus.csproj @@ -3,7 +3,7 @@ Stratis Bitcoin Features Consensus Stratis.Bitcoin.Features.Consensus - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.Consensus Stratis.Features.Consensus false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.Dns.Tests/Stratis.Bitcoin.Features.Dns.Tests.csproj b/src/Stratis.Bitcoin.Features.Dns.Tests/Stratis.Bitcoin.Features.Dns.Tests.csproj index 5b4496ec33..81b2a11427 100644 --- a/src/Stratis.Bitcoin.Features.Dns.Tests/Stratis.Bitcoin.Features.Dns.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.Dns.Tests/Stratis.Bitcoin.Features.Dns.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.Dns.Tests Stratis.Bitcoin.Features.Dns.Tests true diff --git a/src/Stratis.Bitcoin.Features.Dns/Stratis.Bitcoin.Features.Dns.csproj b/src/Stratis.Bitcoin.Features.Dns/Stratis.Bitcoin.Features.Dns.csproj index 3f6a486e26..ee2ea71c3f 100644 --- a/src/Stratis.Bitcoin.Features.Dns/Stratis.Bitcoin.Features.Dns.csproj +++ b/src/Stratis.Bitcoin.Features.Dns/Stratis.Bitcoin.Features.Dns.csproj @@ -3,7 +3,7 @@ Stratis Bitcoin Features Dns Stratis.Bitcoin.Features.Dns - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.Dns Stratis.Features.Dns false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.ExternalAPI/Stratis.Bitcoin.Features.ExternalApi.csproj b/src/Stratis.Bitcoin.Features.ExternalAPI/Stratis.Bitcoin.Features.ExternalApi.csproj index 80a62141cd..ccd1636c5f 100644 --- a/src/Stratis.Bitcoin.Features.ExternalAPI/Stratis.Bitcoin.Features.ExternalApi.csproj +++ b/src/Stratis.Bitcoin.Features.ExternalAPI/Stratis.Bitcoin.Features.ExternalApi.csproj @@ -1,8 +1,8 @@  - netcoreapp3.1 - 1.3.2.4 + net6.0 + 1.4.0.7 Stratis Group Ltd. Stratis.Features.ExternalAPI Stratis.Features.ExternalAPI diff --git a/src/Stratis.Bitcoin.Features.Interop/CirrusContractClient.cs b/src/Stratis.Bitcoin.Features.Interop/CirrusContractClient.cs index 7fae8e45b2..f607e6f91b 100644 --- a/src/Stratis.Bitcoin.Features.Interop/CirrusContractClient.cs +++ b/src/Stratis.Bitcoin.Features.Interop/CirrusContractClient.cs @@ -6,8 +6,10 @@ using System.Threading.Tasks; using Flurl; using Flurl.Http; +using Microsoft.Extensions.Logging; using NBitcoin; using Newtonsoft.Json; +using Stratis.Bitcoin.Configuration.Logging; using Stratis.Bitcoin.Controllers.Models; using Stratis.Bitcoin.Features.Interop.ETHClient; using Stratis.Bitcoin.Features.Interop.Settings; @@ -35,6 +37,16 @@ public interface ICirrusContractClient /// The target SRC20 contract must obviously support the IMintable interface. Task MintAsync(string contractAddress, string destinationAddress, BigInteger amount); + /// + /// Submits a multisig transaction with its encoded parameters set to invoke the Mint method on a target SRC721 contract. + /// + /// The SRC20 contract address that tokens should be minted for. + /// The Cirrus address that the SRC20 tokens should be minted and assigned to. + /// The tokenId of the SRC721 token to be minted. + /// The URI of the SRC721 token to be minted. + /// The transactionId of the mint request submitted to the Cirrus multisig wallet contract. + Task MintNftAsync(string contractAddress, string destinationAddress, BigInteger tokenId, string uri); + /// /// Retrieves the receipt for a given smart contract invocation. /// @@ -66,6 +78,7 @@ public interface ICirrusContractClient /// Retrieves the number of confirmations a given multisig transactionId has. This is retrieved by invoking the Confirmations method of the multisig contract. /// /// The integer multisig contract transaction identifier to retrieve the number of multisig confirmations for. + /// The block height at which to get the confirmation count. /// The number of confirmations. /// This does not relate to the number of elapsed blocks. Task GetMultisigConfirmationCountAsync(BigInteger transactionId, ulong blockHeight); @@ -75,19 +88,27 @@ public interface ICirrusContractClient /// /// If succesfull, the transaction hash of the confirmation transaction, else null and the error message. Task<(string TransactionHash, string Message)> ConfirmTransactionAsync(BigInteger transactionId); + + Task GetKeyValueStoreAsync(string address, string key, ulong blockHeight); + + Task SetKeyValueStoreAsync(string key, string value); } /// public class CirrusContractClient : ICirrusContractClient { private const int GetReceiptWaitTimeSeconds = 180; + public const string KeyValueGetMethodName = "Get"; + public const string KeyValueSetMethodName = "Set"; public const string MultisigConfirmMethodName = "Confirm"; public const string MultisigSubmitMethodName = "Submit"; public const string SRC20MintMethodName = "Mint"; + public const string SRC721MintMethodName = "Mint"; private readonly CirrusInteropSettings cirrusInteropSettings; private readonly ChainIndexer chainIndexer; private readonly Serializer serializer; + private readonly ILogger logger; /// /// The constructor. @@ -99,35 +120,15 @@ public CirrusContractClient(InteropSettings interopSettings, ChainIndexer chainI this.cirrusInteropSettings = interopSettings.GetSettings(); this.chainIndexer = chainIndexer; this.serializer = new Serializer(new ContractPrimitiveSerializerV2(this.chainIndexer.Network)); + + this.logger = LogManager.GetCurrentClassLogger(); } - /// - public async Task MintAsync(string contractAddress, string destinationAddress, BigInteger amount) + private async Task MultisigContractCallInternalAsync(string contractAddress, string methodName, string methodDataHex) { BuildCallContractTransactionResponse response; try { - Address mintRecipient = destinationAddress.ToAddress(this.chainIndexer.Network); - - // Pack the parameters of the Mint method invocation into the format used by the multisig contract. - byte[] accountBytes = this.serializer.Serialize(mintRecipient); - byte[] accountBytesPadded = new byte[accountBytes.Length + 1]; - accountBytesPadded[0] = 9; // 9 = Address - Array.Copy(accountBytes, 0, accountBytesPadded, 1, accountBytes.Length); - - byte[] amountBytes = this.serializer.Serialize(new UInt256(amount.ToByteArray())); - byte[] amountBytesPadded = new byte[amountBytes.Length + 1]; - amountBytesPadded[0] = 12; // 12 = UInt256 - Array.Copy(amountBytes, 0, amountBytesPadded, 1, amountBytes.Length); - - byte[] output = this.serializer.Serialize(new byte[][] - { - accountBytesPadded, - amountBytesPadded - }); - - string mintDataHex = BitConverter.ToString(output).Replace("-", ""); - var request = new BuildCallContractTransactionRequest { WalletName = this.cirrusInteropSettings.CirrusWalletCredentials.WalletName, @@ -142,15 +143,17 @@ public async Task MintAsync(string contractAddre Sender = this.cirrusInteropSettings.CirrusSmartContractActiveAddress, Parameters = new string[] { - // Destination - this is the SRC20 contract that the mint will be invoked against, *not* the Cirrus address the minted tokens will be sent to + // Destination - in the case of a mint this is the SRC20/SRC721 contract that the mint will be invoked against, *not* the Cirrus address the minted tokens will be sent to "9#" + contractAddress, // MethodName - "4#" + SRC20MintMethodName, + "4#" + methodName, // Data - this is an analogue of the ABI-encoded data used in Ethereum contract calls - "10#" + mintDataHex + "10#" + methodDataHex } }; + this.logger.LogDebug($"{nameof(contractAddress)}:{contractAddress} {nameof(methodName)}:{methodName} {nameof(methodDataHex)}:{methodDataHex}"); + using (CancellationTokenSource cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(180))) { response = await this.cirrusInteropSettings.CirrusClientUrl @@ -221,6 +224,90 @@ public async Task MintAsync(string contractAddre } } + private byte[] GetMintData(Address mintRecipient, BigInteger amount) + { + // Pack the parameters of the Mint method invocation into the format used by the multisig contract. + byte[] accountBytes = this.serializer.Serialize(mintRecipient); + byte[] accountBytesPadded = CreatePaddedParameterArray(accountBytes, 9); // 9 = Address + + byte[] amountBytes = this.serializer.Serialize(new UInt256(amount.ToByteArray())); + byte[] amountBytesPadded = CreatePaddedParameterArray(amountBytes, 12); // 12 = UInt256 + + return this.serializer.Serialize(new byte[][] + { + accountBytesPadded, + amountBytesPadded + }); + } + + /// + public async Task MintAsync(string contractAddress, string destinationAddress, BigInteger amount) + { + try + { + Address mintRecipient = destinationAddress.ToAddress(this.chainIndexer.Network); + + byte[] mintData = GetMintData(mintRecipient, amount); + + string mintDataHex = BitConverter.ToString(mintData).Replace("-", ""); + + return await MultisigContractCallInternalAsync(contractAddress, SRC20MintMethodName, mintDataHex).ConfigureAwait(false); + } + catch (Exception ex) + { + return new MultisigTransactionIdentifiers + { + Message = $"Exception occurred trying to build and send the mint transaction: {ex}", + TransactionHash = "", + TransactionId = -1 + }; + } + } + + private byte[] GetMintNftData(Address mintRecipient, BigInteger tokenId, string uri) + { + // Pack the parameters of the SRC721 Mint method invocation into the format used by the multisig contract. + byte[] accountBytes = this.serializer.Serialize(mintRecipient); + byte[] accountBytesPadded = CreatePaddedParameterArray(accountBytes, 9); // 9 = Address + + byte[] tokenIdBytes = this.serializer.Serialize(new UInt256(tokenId.ToByteArray())); + byte[] tokenIdBytesPadded = CreatePaddedParameterArray(tokenIdBytes, 12); // 12 = UInt256 + + byte[] uriBytes = this.serializer.Serialize(uri); + byte[] uriBytesPadded = CreatePaddedParameterArray(uriBytes, 4); // 4 = String + + return this.serializer.Serialize(new byte[][] + { + accountBytesPadded, + tokenIdBytesPadded, + uriBytesPadded + }); + } + + /// + public async Task MintNftAsync(string contractAddress, string destinationAddress, BigInteger tokenId, string uri) + { + try + { + Address mintRecipient = destinationAddress.ToAddress(this.chainIndexer.Network); + + byte[] mintData = GetMintNftData(mintRecipient, tokenId, uri); + + string mintDataHex = BitConverter.ToString(mintData).Replace("-", ""); + + return await MultisigContractCallInternalAsync(contractAddress, SRC721MintMethodName, mintDataHex).ConfigureAwait(false); + } + catch (Exception ex) + { + return new MultisigTransactionIdentifiers + { + Message = $"Exception occurred trying to build and send the mint transaction: {ex}", + TransactionHash = "", + TransactionId = -1 + }; + } + } + /// public async Task GetReceiptAsync(string txHash) { @@ -430,11 +517,19 @@ public async Task GetMultisigConfirmationCountAsync(BigInteger transactionI } }; - response = await this.cirrusInteropSettings.CirrusClientUrl - .AppendPathSegment("api/smartcontracts/build-and-send-call") - .PostJsonAsync(request) - .ReceiveJson() - .ConfigureAwait(false); + using (CancellationTokenSource cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(180))) + { + response = await this.cirrusInteropSettings.CirrusClientUrl + .AppendPathSegment("api/smartcontracts/build-and-send-call") + .PostJsonAsync(request, cancellation.Token) + .ReceiveJson() + .ConfigureAwait(false); + + if (!response.Success) + { + return (null, $"Error confirming transfer '{response.TransactionId}': Possible transaction build and call timeout."); + } + } } catch (Exception ex) { @@ -460,6 +555,92 @@ public async Task GetMultisigConfirmationCountAsync(BigInteger transactionI return (null, $"Exception occurred trying to retrieve the receipt: {ex}"); } } + + /// + public async Task GetKeyValueStoreAsync(string address, string key, ulong blockHeight) + { + var request = new LocalCallContractRequest + { + BlockHeight = blockHeight, + Amount = "0", + ContractAddress = this.cirrusInteropSettings.CirrusKeyValueStoreContractAddress, + GasLimit = 250_000, + GasPrice = 100, + MethodName = KeyValueGetMethodName, + Parameters = new[] + { + "9#" + address, + "4#" + key + }, + Sender = this.cirrusInteropSettings.CirrusSmartContractActiveAddress + }; + + try + { + LocalExecutionResponse result = await this.cirrusInteropSettings.CirrusClientUrl + .AppendPathSegment("api/smartcontracts/local-call") + .AllowAnyHttpStatus() + .PostJsonAsync(request) + .ReceiveJson() + .ConfigureAwait(false); + + if (result.Return == null) + return null; + + return (string)result.Return; + } + catch (Exception) + { + throw; + } + } + + /// + public async Task SetKeyValueStoreAsync(string key, string value) + { + try + { + byte[] methodCallData = KeyValueData(key, value); + + string methodCallDataHex = BitConverter.ToString(methodCallData).Replace("-", ""); + + return await MultisigContractCallInternalAsync(this.cirrusInteropSettings.CirrusMultisigContractAddress, KeyValueSetMethodName, methodCallDataHex).ConfigureAwait(false); + } + catch (Exception ex) + { + return new MultisigTransactionIdentifiers + { + Message = $"Exception occurred trying to build and send the KVS Set transaction: {ex}", + TransactionHash = "", + TransactionId = -1 + }; + } + } + + private byte[] KeyValueData(string key, string value) + { + // Pack the parameters of the KVS Set method invocation into the format used by the multisig contract. + byte[] keyBytes = this.serializer.Serialize(key); + byte[] keyBytesPadded = CreatePaddedParameterArray(keyBytes, 4); // 4 = String + + byte[] valueBytes = this.serializer.Serialize(value); + byte[] valueBytesPadded = CreatePaddedParameterArray(valueBytes, 4); // 4 = String + + return this.serializer.Serialize(new byte[][] + { + keyBytesPadded, + valueBytesPadded + }); + } + + private byte[] CreatePaddedParameterArray(byte[] paramBytes, int paramType) + { + byte[] paramBytesPadded = new byte[paramBytes.Length + 1]; + paramBytesPadded[0] = (byte)paramType; + Array.Copy(paramBytes, 0, paramBytesPadded, 1, paramBytes.Length); + + return paramBytesPadded; + } } public class ConsensusTipModel diff --git a/src/Stratis.Bitcoin.Features.Interop/Controllers/InteropController.cs b/src/Stratis.Bitcoin.Features.Interop/Controllers/InteropController.cs index ba864790fa..9176ae4cb3 100644 --- a/src/Stratis.Bitcoin.Features.Interop/Controllers/InteropController.cs +++ b/src/Stratis.Bitcoin.Features.Interop/Controllers/InteropController.cs @@ -10,6 +10,7 @@ using NBitcoin; using NBitcoin.DataEncoders; using Stratis.Bitcoin.Configuration.Logging; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Features.Interop.ETHClient; using Stratis.Bitcoin.Features.Interop.Models; using Stratis.Bitcoin.Features.Interop.Settings; @@ -42,6 +43,7 @@ public sealed class InteropController : Controller private readonly ILogger logger; private readonly IReplenishmentKeyValueStore replenishmentKeyValueStore; private readonly Network network; + private readonly IExternalApiPoller externalApiPoller; public InteropController( ICallDataSerializer callDataSerializer, @@ -55,7 +57,8 @@ public InteropController( IFederationManager federationManager, InteropSettings interopSettings, InteropPoller interopPoller, - IReplenishmentKeyValueStore replenishmentKeyValueStore) + IReplenishmentKeyValueStore replenishmentKeyValueStore, + IExternalApiPoller externalApiPoller) { this.callDataSerializer = callDataSerializer; this.chainIndexer = chainIndexer; @@ -70,6 +73,7 @@ public InteropController( this.logger = LogManager.GetCurrentClassLogger(); this.network = network; this.replenishmentKeyValueStore = replenishmentKeyValueStore; + this.externalApiPoller = externalApiPoller; } [Route("initializeinterflux")] @@ -152,7 +156,7 @@ public IActionResult InteropState() [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.InternalServerError)] - public IActionResult InteropStatusBurnRequests([FromBody] string requestId) + public IActionResult InteropStatusGetRequest([FromBody] string requestId) { try { @@ -260,6 +264,8 @@ public IActionResult InteropStatusVotes() var receivedVotes = new Dictionary>(); + response.TransactionIdVotes = this.conversionRequestCoordinationService.GetTransactionIdStatus(); + foreach ((string requestId, HashSet pubKeys) in this.conversionRequestCoordinationService.GetStatus()) { var pubKeyList = new List(); @@ -390,7 +396,7 @@ public async Task RemoveOwnerAsync(DestinationChain destinationCh /// /// The chain the multisig wallet contract is deployed to. /// The multisig wallet transactionId (this is an integer, not an on-chain transaction hash). - /// The gas price to use for submitting the confirmation. + /// The gas price to use for submitting the confirmation (non-Cirrus chains only). Passing 0 will use the to look up the value. /// The on-chain transaction hash of the contract call transaction. [Route("confirmtransaction")] [HttpGet] @@ -401,12 +407,25 @@ public async Task ConfirmTransactionAsync(DestinationChain destin { try { + if (destinationChain == DestinationChain.CIRRUS) + { + var cirrusClient = new CirrusContractClient(this.interopSettings, this.chainIndexer); + + (string TransactionHash, string Message) result = await cirrusClient.ConfirmTransactionAsync(transactionId).ConfigureAwait(false); + + return this.Json(result.TransactionHash); + } + if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) return BadRequest($"{destinationChain} not enabled or supported!"); IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); - // TODO: Maybe for convenience the gas price could come from the external API poller + if (gasPrice == 0) + { + gasPrice = this.externalApiPoller.GetGasPrice(); + } + return this.Json(await client.ConfirmTransactionAsync(transactionId, gasPrice).ConfigureAwait(false)); } catch (Exception e) @@ -602,6 +621,69 @@ public async Task Erc20BalanceAsync(DestinationChain destinationC } } + /// + /// Retrieves the current owner of a given tokenId on a given ERC721 contract. + /// + /// The chain the ERC721 contract is deployed to. + /// The address of the contract on the given chain. + /// The tokenId to retrieve the owner for. + /// The URI string. + [Route("erc721owner")] + [HttpGet] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task Erc721OwnerAsync(DestinationChain destinationChain, string contractAddress, int tokenId) + { + try + { + if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) + return BadRequest($"{destinationChain} not enabled or supported!"); + + IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); + + return Ok((await client.GetErc721TokenOwnerAsync(contractAddress, tokenId).ConfigureAwait(false)).ToString()); + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + + return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString()); + } + } + + /// + /// Retrieves the current URI of a given tokenId on a given ERC721 contract. + /// + /// This may not match the value of the corresponding token on the Cirrus chain (if any). + /// The chain the ERC721 contract is deployed to. + /// The address of the contract on the given chain. + /// The tokenId to retrieve the URI for. + /// The URI string. + [Route("erc721uri")] + [HttpGet] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task Erc721UriAsync(DestinationChain destinationChain, string contractAddress, int tokenId) + { + try + { + if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) + return BadRequest($"{destinationChain} not enabled or supported!"); + + IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); + + return Ok((await client.GetErc721TokenUriAsync(contractAddress, tokenId).ConfigureAwait(false)).ToString()); + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + + return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString()); + } + } + [Route("requests/delete")] [HttpDelete] [ProducesResponseType((int)HttpStatusCode.OK)] diff --git a/src/Stratis.Bitcoin.Features.Interop/ETHClient/ContractSource/CirrusNFT.sol b/src/Stratis.Bitcoin.Features.Interop/ETHClient/ContractSource/CirrusNFT.sol new file mode 100644 index 0000000000..6d120f3dc7 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.Interop/ETHClient/ContractSource/CirrusNFT.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.1; + +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.5.0/contracts/access/Ownable.sol"; +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.5.0/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.5.0/contracts/utils/Counters.sol"; + +contract CirrusMintableNFT is ERC721URIStorage, Ownable { + using Counters for Counters.Counter; + Counters.Counter private _tokenIds; + + constructor() ERC721("CirrusMintableNFT", "NFT") { + } + + function mint(address recipient, uint256 tokenId, string memory tokenURI) public onlyOwner { + _safeMint(recipient, tokenId); + _setTokenURI(tokenId, tokenURI); + } + + function burn(uint256 tokenId) public + { + _burn(tokenId); + } +} diff --git a/src/Stratis.Bitcoin.Features.Interop/ETHClient/ETHClient.cs b/src/Stratis.Bitcoin.Features.Interop/ETHClient/ETHClient.cs index 5db8724632..87baac270a 100644 --- a/src/Stratis.Bitcoin.Features.Interop/ETHClient/ETHClient.cs +++ b/src/Stratis.Bitcoin.Features.Interop/ETHClient/ETHClient.cs @@ -125,6 +125,10 @@ public interface IETHClient Task GetErc20BalanceAsync(string addressToQuery, string contractAddress); + Task GetErc721TokenOwnerAsync(string contractAddress, BigInteger tokenId); + + Task GetErc721TokenUriAsync(string contractAddress, BigInteger tokenId); + /// /// Retrieves a string from the Key Value Store contract. /// @@ -196,6 +200,19 @@ public interface IETHClient /// The new threshold for confirmations for a multisig transaction to be executed. /// The hex data of the encoded parameters. string EncodeChangeRequirementParams(BigInteger requirement); + + /// + /// Returns the encoded form of transaction data that calls the safeTransferFrom(address, address, uint256) method on an ERC721-compliant contract. + /// This is exactly the contents of the 'data' field in a normal transaction. + /// This encoded data is required for submitting a transaction to the multisig contract. + /// + /// The address to transfer the token from (must be the token's current owner). + /// The address to transfer the token to. + /// The identifier of the token to be transferred. + /// The hex data of the encoded parameters. + string EncodeNftTransferParams(string from, string to, BigInteger tokenId); + + string EncodeNftMintParams(string recipient, BigInteger tokenId, string tokenUri); } public class ETHClient : IETHClient @@ -334,7 +351,7 @@ public async Task GetBlockAsync(BigInteger blockNumber) foreach (FilterLogVO log in logs) { - // By looking for the emitted event instead of looking for actual function calls we cater for a much wider variety of ERC20 implementations. + // By looking for the emitted event instead of looking for actual function calls we cater for a much wider variety of ERC20/721 implementations. EventLog eventLog = log?.Log?.DecodeEvent(); // TODO: We could probably optimise this even further by only trying to decode the event as the expected type, not both when the first attempt fails. @@ -364,7 +381,10 @@ public async Task GetBlockAsync(BigInteger blockNumber) TransferType = TransferType.Transfer, From = eventLog.Event.From, To = eventLog.Event.To, - Value = eventLog.Event.Value + Value = eventLog.Event.Value, + + // This field is not used for ERC20. Explicitly setting it to null here as a placeholder. + Uri = null })); continue; @@ -380,14 +400,19 @@ public async Task GetBlockAsync(BigInteger blockNumber) continue; } - if (nftEventLog.Event.TokenId == BigInteger.Zero) + // We presume that an NFT could conceivably have tokenId = 0. + + if (nftEventLog.Event.TokenId < BigInteger.Zero) { - // Ignoring zero-valued transfer. continue; } - if (nftEventLog.Event.TokenId < BigInteger.Zero) + string uri = await this.GetErc721TokenUriAsync(tx.To, nftEventLog.Event.TokenId).ConfigureAwait(false); + + if (uri == null) { + this.logger.Warn($"Unable to retrieve NFT URI from transaction '{tx.TransactionHash}' for tokenId '{nftEventLog.Event.TokenId}'"); + continue; } @@ -397,7 +422,8 @@ public async Task GetBlockAsync(BigInteger blockNumber) TransferType = TransferType.Transfer, From = nftEventLog.Event.From, To = nftEventLog.Event.To, - Value = nftEventLog.Event.TokenId + Value = nftEventLog.Event.TokenId, + Uri = uri })); } } @@ -502,8 +528,17 @@ public async Task GetWStraxBalanceAsync(string addressToQuery) public async Task GetErc20BalanceAsync(string addressToQuery, string contractAddress) { - // TODO: Make a generic ERC20 contract interface for the necessary methods, rather than sharing this - return await WrappedStrax.GetErc20BalanceAsync(this.web3, contractAddress, addressToQuery).ConfigureAwait(false); + return await Erc20Interface.GetBalanceAsync(this.web3, contractAddress, addressToQuery).ConfigureAwait(false); + } + + public async Task GetErc721TokenOwnerAsync(string contractAddress, BigInteger tokenId) + { + return await NftInterface.GetTokenOwnerAsync(this.web3, contractAddress, tokenId).ConfigureAwait(false); + } + + public async Task GetErc721TokenUriAsync(string contractAddress, BigInteger tokenId) + { + return await NftInterface.GetTokenUriAsync(this.web3, contractAddress, tokenId).ConfigureAwait(false); } /// @@ -589,5 +624,27 @@ public string EncodeChangeRequirementParams(BigInteger requirement) return result; } + + /// + public string EncodeNftTransferParams(string from, string to, BigInteger tokenId) + { + const string SafeTransferFromMethod = "42842e0e"; + + var abiEncode = new ABIEncode(); + string result = SafeTransferFromMethod + abiEncode.GetABIEncoded(new ABIValue("address", from), new ABIValue("address", to), new ABIValue("uint256", tokenId)).ToHex(); + + return result; + } + + /// + public string EncodeNftMintParams(string recipient, BigInteger tokenId, string tokenUri) + { + const string NftMintMethod = "d3fc9864"; + + var abiEncode = new ABIEncode(); + string result = NftMintMethod + abiEncode.GetABIEncoded(new ABIValue("address", recipient), new ABIValue("uint256", tokenId), new ABIValue("string", tokenUri)).ToHex(); + + return result; + } } } diff --git a/src/Stratis.Bitcoin.Features.Interop/ETHClient/Erc20Interface.cs b/src/Stratis.Bitcoin.Features.Interop/ETHClient/Erc20Interface.cs new file mode 100644 index 0000000000..4fd3588bc7 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.Interop/ETHClient/Erc20Interface.cs @@ -0,0 +1,23 @@ +using System.Numerics; +using System.Threading.Tasks; +using Nethereum.Contracts.ContractHandlers; +using Nethereum.Web3; + +namespace Stratis.Bitcoin.Features.Interop.ETHClient +{ + public class Erc20Interface + { + public static async Task GetBalanceAsync(Web3 web3, string contractAddress, string addressToQuery) + { + var balanceOfFunctionMessage = new BalanceOfFunction() + { + Owner = addressToQuery + }; + + IContractQueryHandler balanceHandler = web3.Eth.GetContractQueryHandler(); + BigInteger balance = await balanceHandler.QueryAsync(contractAddress, balanceOfFunctionMessage); + + return balance; + } + } +} diff --git a/src/Stratis.Bitcoin.Features.Interop/ETHClient/NftInterface.cs b/src/Stratis.Bitcoin.Features.Interop/ETHClient/NftInterface.cs index d6543ac8d1..33960165d2 100644 --- a/src/Stratis.Bitcoin.Features.Interop/ETHClient/NftInterface.cs +++ b/src/Stratis.Bitcoin.Features.Interop/ETHClient/NftInterface.cs @@ -1,5 +1,10 @@ using System.Numerics; +using System.Threading.Tasks; using Nethereum.ABI.FunctionEncoding.Attributes; +using Nethereum.Contracts; +using Nethereum.Contracts.ContractHandlers; +using Nethereum.RPC.Eth.DTOs; +using Nethereum.Web3; namespace Stratis.Bitcoin.Features.Interop.ETHClient { @@ -28,4 +33,537 @@ public class NftTransferEventDTO : IEventDTO [Parameter("uint256", "_tokenId", 3, true)] public BigInteger TokenId { get; set; } } + + [Function("tokenURI", "string")] + public class TokenUriFunction : FunctionMessage + { + [Parameter("uint256", "tokenId", 1)] + public BigInteger TokenId { get; set; } + } + + [Function("ownerOf", "address")] + public class NftOwnerFunction : FunctionMessage + { + [Parameter("uint256", "tokenId", 1)] + public BigInteger TokenId { get; set; } + } + + [Function("safeTransferFrom")] + public class NftTransferFunction : FunctionMessage + { + [Parameter("address", "from", 1)] + public string From { get; set; } + + [Parameter("address", "to", 2)] + public string To { get; set; } + + [Parameter("uint256", "tokenId", 3)] + public BigInteger TokenId { get; set; } + } + + [Function("mint")] + public class NftMintFunction : FunctionMessage + { + [Parameter("address", "recipient", 1)] + public string Recipient { get; set; } + + [Parameter("uint256", "tokenId", 2)] + public BigInteger TokenId { get; set; } + + [Parameter("string", "tokenUri", 3)] + public string TokenUri { get; set; } + } + + public class NftInterface + { + public static async Task GetTokenOwnerAsync(Web3 web3, string contractAddress, BigInteger tokenId) + { + var nftOwnerFunctionMessage = new NftOwnerFunction() + { + TokenId = tokenId + }; + + IContractQueryHandler ownerHandler = web3.Eth.GetContractQueryHandler(); + string owner = await ownerHandler.QueryAsync(contractAddress, nftOwnerFunctionMessage).ConfigureAwait(false); + + return owner; + } + + /// + /// If the NFT contract supports the ERC721Metadata extension, it should expose a 'tokenURI(uint256 tokenId)' method that + /// can be interrogated to retrieve the token-specific URI. + /// + /// The URI for the given tokenId. + public static async Task GetTokenUriAsync(Web3 web3, string contractAddress, BigInteger tokenId) + { + var tokenUriFunctionMessage = new TokenUriFunction() + { + TokenId = tokenId + }; + + IContractQueryHandler balanceHandler = web3.Eth.GetContractQueryHandler(); + string uri = await balanceHandler.QueryAsync(contractAddress, tokenUriFunctionMessage).ConfigureAwait(false); + + return uri; + } + + public static async Task SafeTransferAsync(Web3 web3, string contractAddress, string recipient, BigInteger tokenId) + { + IContractTransactionHandler transferHandler = web3.Eth.GetContractTransactionHandler(); + + var transfer = new NftTransferFunction() + { + From = "", + To = recipient, + TokenId = tokenId + }; + + TransactionReceipt transactionTransferReceipt = await transferHandler.SendRequestAndWaitForReceiptAsync(contractAddress, transfer); + + return transactionTransferReceipt.TransactionHash; + } + + public static async Task MintAsync(Web3 web3, string contractAddress, string recipient, BigInteger tokenId, string tokenUri) + { + IContractTransactionHandler mintHandler = web3.Eth.GetContractTransactionHandler(); + + var mint = new NftMintFunction() + { + Recipient = recipient, + TokenId = tokenId, + TokenUri = tokenUri + }; + + TransactionReceipt transactionMintReceipt = await mintHandler.SendRequestAndWaitForReceiptAsync(contractAddress, mint); + + return transactionMintReceipt.TransactionHash; + } + } + + /* Extensions to the ERC721 standard used by InterFlux: + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "tokenURI", + "type": "string" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + + "42966c68": "burn(uint256)", + "d3fc9864": "mint(address,uint256,string)", + */ + + // 60806040523480156200001157600080fd5b50604080518082018252600b81526a135a5b9d18589b1953919560aa1b60208083019182528351808501909452600384526213919560ea1b90840152815191929162000060916000916200007f565b508051620000769060019060208401906200007f565b50505062000161565b8280546200008d9062000125565b90600052602060002090601f016020900481019282620000b15760008555620000fc565b82601f10620000cc57805160ff1916838001178555620000fc565b82800160010185558215620000fc579182015b82811115620000fc578251825591602001919060010190620000df565b506200010a9291506200010e565b5090565b5b808211156200010a57600081556001016200010f565b600181811c908216806200013a57607f821691505b6020821081036200015b57634e487b7160e01b600052602260045260246000fd5b50919050565b6116e680620001716000396000f3fe608060405234801561001057600080fd5b50600436106100ea5760003560e01c806370a082311161008c578063b88d4fde11610066578063b88d4fde146101e1578063c87b56dd146101f4578063d0def52114610207578063e985e9c51461021a57600080fd5b806370a08231146101a557806395d89b41146101c6578063a22cb465146101ce57600080fd5b8063095ea7b3116100c8578063095ea7b31461015757806323b872dd1461016c57806342842e0e1461017f5780636352211e1461019257600080fd5b806301ffc9a7146100ef57806306fdde0314610117578063081812fc1461012c575b600080fd5b6101026100fd366004611181565b610256565b60405190151581526020015b60405180910390f35b61011f6102a8565b60405161010e91906111f6565b61013f61013a366004611209565b61033a565b6040516001600160a01b03909116815260200161010e565b61016a61016536600461123e565b6103c7565b005b61016a61017a366004611268565b6104dc565b61016a61018d366004611268565b61050d565b61013f6101a0366004611209565b610528565b6101b86101b33660046112a4565b61059f565b60405190815260200161010e565b61011f610626565b61016a6101dc3660046112bf565b610635565b61016a6101ef366004611387565b610644565b61011f610202366004611209565b61067c565b6101b8610215366004611403565b6107f2565b610102610228366004611465565b6001600160a01b03918216600090815260056020908152604080832093909416825291909152205460ff1690565b60006001600160e01b031982166380ac58cd60e01b148061028757506001600160e01b03198216635b5e139f60e01b145b806102a257506301ffc9a760e01b6001600160e01b03198316145b92915050565b6060600080546102b790611498565b80601f01602080910402602001604051908101604052809291908181526020018280546102e390611498565b80156103305780601f1061030557610100808354040283529160200191610330565b820191906000526020600020905b81548152906001019060200180831161031357829003601f168201915b5050505050905090565b60006103458261082a565b6103ab5760405162461bcd60e51b815260206004820152602c60248201527f4552433732313a20617070726f76656420717565727920666f72206e6f6e657860448201526b34b9ba32b73a103a37b5b2b760a11b60648201526084015b60405180910390fd5b506000908152600460205260409020546001600160a01b031690565b60006103d282610528565b9050806001600160a01b0316836001600160a01b03160361043f5760405162461bcd60e51b815260206004820152602160248201527f4552433732313a20617070726f76616c20746f2063757272656e74206f776e656044820152603960f91b60648201526084016103a2565b336001600160a01b038216148061045b575061045b8133610228565b6104cd5760405162461bcd60e51b815260206004820152603860248201527f4552433732313a20617070726f76652063616c6c6572206973206e6f74206f7760448201527f6e6572206e6f7220617070726f76656420666f7220616c6c000000000000000060648201526084016103a2565b6104d78383610847565b505050565b6104e633826108b5565b6105025760405162461bcd60e51b81526004016103a2906114d2565b6104d783838361099b565b6104d783838360405180602001604052806000815250610644565b6000818152600260205260408120546001600160a01b0316806102a25760405162461bcd60e51b815260206004820152602960248201527f4552433732313a206f776e657220717565727920666f72206e6f6e657869737460448201526832b73a103a37b5b2b760b91b60648201526084016103a2565b60006001600160a01b03821661060a5760405162461bcd60e51b815260206004820152602a60248201527f4552433732313a2062616c616e636520717565727920666f7220746865207a65604482015269726f206164647265737360b01b60648201526084016103a2565b506001600160a01b031660009081526003602052604090205490565b6060600180546102b790611498565b610640338383610b37565b5050565b61064e33836108b5565b61066a5760405162461bcd60e51b81526004016103a2906114d2565b61067684848484610c05565b50505050565b60606106878261082a565b6106ed5760405162461bcd60e51b815260206004820152603160248201527f45524337323155524953746f726167653a2055524920717565727920666f72206044820152703737b732bc34b9ba32b73a103a37b5b2b760791b60648201526084016103a2565b6000828152600660205260408120805461070690611498565b80601f016020809104026020016040519081016040528092919081815260200182805461073290611498565b801561077f5780601f106107545761010080835404028352916020019161077f565b820191906000526020600020905b81548152906001019060200180831161076257829003601f168201915b50505050509050600061079d60408051602081019091526000815290565b905080516000036107af575092915050565b8151156107e15780826040516020016107c9929190611523565b60405160208183030381529060405292505050919050565b6107ea84610c38565b949350505050565b6000610802600780546001019055565b600061080d60075490565b90506108198482610d0f565b6108238184610e42565b9392505050565b6000908152600260205260409020546001600160a01b0316151590565b600081815260046020526040902080546001600160a01b0319166001600160a01b038416908117909155819061087c82610528565b6001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45050565b60006108c08261082a565b6109215760405162461bcd60e51b815260206004820152602c60248201527f4552433732313a206f70657261746f7220717565727920666f72206e6f6e657860448201526b34b9ba32b73a103a37b5b2b760a11b60648201526084016103a2565b600061092c83610528565b9050806001600160a01b0316846001600160a01b031614806109675750836001600160a01b031661095c8461033a565b6001600160a01b0316145b806107ea57506001600160a01b0380821660009081526005602090815260408083209388168352929052205460ff166107ea565b826001600160a01b03166109ae82610528565b6001600160a01b031614610a125760405162461bcd60e51b815260206004820152602560248201527f4552433732313a207472616e736665722066726f6d20696e636f72726563742060448201526437bbb732b960d91b60648201526084016103a2565b6001600160a01b038216610a745760405162461bcd60e51b8152602060048201526024808201527f4552433732313a207472616e7366657220746f20746865207a65726f206164646044820152637265737360e01b60648201526084016103a2565b610a7f600082610847565b6001600160a01b0383166000908152600360205260408120805460019290610aa8908490611568565b90915550506001600160a01b0382166000908152600360205260408120805460019290610ad690849061157f565b909155505060008181526002602052604080822080546001600160a01b0319166001600160a01b0386811691821790925591518493918716917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef91a4505050565b816001600160a01b0316836001600160a01b031603610b985760405162461bcd60e51b815260206004820152601960248201527f4552433732313a20617070726f766520746f2063616c6c65720000000000000060448201526064016103a2565b6001600160a01b03838116600081815260056020908152604080832094871680845294825291829020805460ff191686151590811790915591519182527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31910160405180910390a3505050565b610c1084848461099b565b610c1c84848484610ecd565b6106765760405162461bcd60e51b81526004016103a290611597565b6060610c438261082a565b610ca75760405162461bcd60e51b815260206004820152602f60248201527f4552433732314d657461646174613a2055524920717565727920666f72206e6f60448201526e3732bc34b9ba32b73a103a37b5b2b760891b60648201526084016103a2565b6000610cbe60408051602081019091526000815290565b90506000815111610cde5760405180602001604052806000815250610823565b80610ce884610fce565b604051602001610cf9929190611523565b6040516020818303038152906040529392505050565b6001600160a01b038216610d655760405162461bcd60e51b815260206004820181905260248201527f4552433732313a206d696e7420746f20746865207a65726f206164647265737360448201526064016103a2565b610d6e8161082a565b15610dbb5760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e7465640000000060448201526064016103a2565b6001600160a01b0382166000908152600360205260408120805460019290610de490849061157f565b909155505060008181526002602052604080822080546001600160a01b0319166001600160a01b03861690811790915590518392907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908290a45050565b610e4b8261082a565b610eae5760405162461bcd60e51b815260206004820152602e60248201527f45524337323155524953746f726167653a2055524920736574206f66206e6f6e60448201526d32bc34b9ba32b73a103a37b5b2b760911b60648201526084016103a2565b600082815260066020908152604090912082516104d7928401906110cf565b60006001600160a01b0384163b15610fc357604051630a85bd0160e11b81526001600160a01b0385169063150b7a0290610f119033908990889088906004016115e9565b6020604051808303816000875af1925050508015610f4c575060408051601f3d908101601f19168201909252610f4991810190611626565b60015b610fa9573d808015610f7a576040519150601f19603f3d011682016040523d82523d6000602084013e610f7f565b606091505b508051600003610fa15760405162461bcd60e51b81526004016103a290611597565b805181602001fd5b6001600160e01b031916630a85bd0160e11b1490506107ea565b506001949350505050565b606081600003610ff55750506040805180820190915260018152600360fc1b602082015290565b8160005b811561101f578061100981611643565b91506110189050600a83611672565b9150610ff9565b60008167ffffffffffffffff81111561103a5761103a6112fb565b6040519080825280601f01601f191660200182016040528015611064576020820181803683370190505b5090505b84156107ea57611079600183611568565b9150611086600a86611686565b61109190603061157f565b60f81b8183815181106110a6576110a661169a565b60200101906001600160f81b031916908160001a9053506110c8600a86611672565b9450611068565b8280546110db90611498565b90600052602060002090601f0160209004810192826110fd5760008555611143565b82601f1061111657805160ff1916838001178555611143565b82800160010185558215611143579182015b82811115611143578251825591602001919060010190611128565b5061114f929150611153565b5090565b5b8082111561114f5760008155600101611154565b6001600160e01b03198116811461117e57600080fd5b50565b60006020828403121561119357600080fd5b813561082381611168565b60005b838110156111b95781810151838201526020016111a1565b838111156106765750506000910152565b600081518084526111e281602086016020860161119e565b601f01601f19169290920160200192915050565b60208152600061082360208301846111ca565b60006020828403121561121b57600080fd5b5035919050565b80356001600160a01b038116811461123957600080fd5b919050565b6000806040838503121561125157600080fd5b61125a83611222565b946020939093013593505050565b60008060006060848603121561127d57600080fd5b61128684611222565b925061129460208501611222565b9150604084013590509250925092565b6000602082840312156112b657600080fd5b61082382611222565b600080604083850312156112d257600080fd5b6112db83611222565b9150602083013580151581146112f057600080fd5b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b600067ffffffffffffffff8084111561132c5761132c6112fb565b604051601f8501601f19908116603f01168101908282118183101715611354576113546112fb565b8160405280935085815286868601111561136d57600080fd5b858560208301376000602087830101525050509392505050565b6000806000806080858703121561139d57600080fd5b6113a685611222565b93506113b460208601611222565b925060408501359150606085013567ffffffffffffffff8111156113d757600080fd5b8501601f810187136113e857600080fd5b6113f787823560208401611311565b91505092959194509250565b6000806040838503121561141657600080fd5b61141f83611222565b9150602083013567ffffffffffffffff81111561143b57600080fd5b8301601f8101851361144c57600080fd5b61145b85823560208401611311565b9150509250929050565b6000806040838503121561147857600080fd5b61148183611222565b915061148f60208401611222565b90509250929050565b600181811c908216806114ac57607f821691505b6020821081036114cc57634e487b7160e01b600052602260045260246000fd5b50919050565b60208082526031908201527f4552433732313a207472616e736665722063616c6c6572206973206e6f74206f6040820152701ddb995c881b9bdc88185c1c1c9bdd9959607a1b606082015260800190565b6000835161153581846020880161119e565b83519083019061154981836020880161119e565b01949350505050565b634e487b7160e01b600052601160045260246000fd5b60008282101561157a5761157a611552565b500390565b6000821982111561159257611592611552565b500190565b60208082526032908201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560408201527131b2b4bb32b91034b6b83632b6b2b73a32b960711b606082015260800190565b6001600160a01b038581168252841660208201526040810183905260806060820181905260009061161c908301846111ca565b9695505050505050565b60006020828403121561163857600080fd5b815161082381611168565b60006001820161165557611655611552565b5060010190565b634e487b7160e01b600052601260045260246000fd5b6000826116815761168161165c565b500490565b6000826116955761169561165c565b500690565b634e487b7160e01b600052603260045260246000fdfea264697066735822122042368c8df2a26d5c5210fe3b8ac9c93c5443667e38f4937353a267d93e0dfea164736f6c634300080d0033 + + /* + { + "095ea7b3": "approve(address,uint256)", + "70a08231": "balanceOf(address)", + "081812fc": "getApproved(uint256)", + "e985e9c5": "isApprovedForAll(address,address)", + "d0def521": "mint(address,string)", + "06fdde03": "name()", + "6352211e": "ownerOf(uint256)", + "42842e0e": "safeTransferFrom(address,address,uint256)", + "b88d4fde": "safeTransferFrom(address,address,uint256,bytes)", + "a22cb465": "setApprovalForAll(address,bool)", + "01ffc9a7": "supportsInterface(bytes4)", + "95d89b41": "symbol()", + "c87b56dd": "tokenURI(uint256)", + "23b872dd": "transferFrom(address,address,uint256)" + } + */ + + /* + [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "string", + "name": "tokenURI", + "type": "string" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] + */ } diff --git a/src/Stratis.Bitcoin.Features.Interop/ETHClient/TransferDetails.cs b/src/Stratis.Bitcoin.Features.Interop/ETHClient/TransferDetails.cs index d9e0419597..e3b2ba4c95 100644 --- a/src/Stratis.Bitcoin.Features.Interop/ETHClient/TransferDetails.cs +++ b/src/Stratis.Bitcoin.Features.Interop/ETHClient/TransferDetails.cs @@ -30,5 +30,11 @@ public class TransferDetails /// For an ERC721 transfer, this is the token identifier. /// public BigInteger Value { get; set; } + + /// + /// For an ERC721 transfer, this is the URI of the token metadata. + /// This field is not used for ERC20 transfers. + /// + public string Uri { get; set; } } } diff --git a/src/Stratis.Bitcoin.Features.Interop/InteropBehavior.cs b/src/Stratis.Bitcoin.Features.Interop/InteropBehavior.cs index f3b58b8e78..e321d36051 100644 --- a/src/Stratis.Bitcoin.Features.Interop/InteropBehavior.cs +++ b/src/Stratis.Bitcoin.Features.Interop/InteropBehavior.cs @@ -124,7 +124,7 @@ private async Task ProcessMessageAsync(INetworkPeer peer, IncomingMessage messag private async Task ProcessConversionRequestPayloadAsync(INetworkPeer peer, ConversionRequestPayload payload) { - this.logger.LogDebug($"Conversion request payload request for id '{payload.RequestId}' received from '{peer.PeerEndPoint.Address}':'{peer.RemoteSocketEndpoint.Address}' proposing transaction ID '{payload.TransactionId}', (IsTransfer: {payload.IsTransfer}, IsReplenishment: {payload.IsReplenishment})."); + this.logger.LogDebug($"Conversion request payload request for id '{payload.RequestId}' received from '{peer.PeerEndPoint.Address}':'{peer.RemoteSocketEndpoint.Address}' proposing transaction ID '{payload.TransactionId}', (IsTransfer: {payload.IsTransfer}, IsReplenishment: {payload.IsReplenishment}, IsKeyValue: {payload.IsKeyValue})."); if (payload.TransactionId == BigInteger.MinusOne) return; @@ -168,7 +168,7 @@ private async Task ProcessConversionRequestPayloadAsync(INetworkPeer peer, Conve await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); string signature = this.federationManager.CurrentFederationKey.SignMessage(payload.RequestId + payload.TransactionId); - await this.AttachedPeer.SendMessageAsync(ConversionRequestPayload.Reply(payload.RequestId, payload.TransactionId, signature, payload.DestinationChain, payload.IsTransfer, payload.IsReplenishment)).ConfigureAwait(false); + await this.AttachedPeer.SendMessageAsync(ConversionRequestPayload.Reply(payload.RequestId, payload.TransactionId, signature, payload.DestinationChain, payload.IsTransfer, payload.IsReplenishment, payload.IsKeyValue)).ConfigureAwait(false); } } diff --git a/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs b/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs index 11a5ba8f8d..0f829781ac 100644 --- a/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs +++ b/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs @@ -33,8 +33,6 @@ using Stratis.Features.FederatedPeg.SourceChain; using Stratis.Features.FederatedPeg.TargetChain; using Stratis.Interop.Contracts; -using Stratis.SmartContracts; -using Stratis.SmartContracts.CLR; namespace Stratis.Bitcoin.Features.Interop { @@ -237,18 +235,18 @@ public async Task InitializeAsync() // Call this explicitly for Cirrus as it does not fall into the ethClientProvider group. await LoadLastPolledBlockForBurnAndTransferRequestsAsync(DestinationChain.CIRRUS).ConfigureAwait(false); - // Initialize loop that polls for all burn requests. + // Initialize loop that polls for all burn requests. this.pollBlockForBurnsAndTransfersLoop = this.asyncProvider.CreateAndRunAsyncLoop("PollBlockForBurnsAndTransfersAsync", async (cancellation) => { if (this.initialBlockDownloadState.IsInitialBlockDownload()) return; - // In the event that the last polled block was set back a considerable distance from the tip, we need to first catch up faster. - // If we are already in the acceptable range, the usual logic will apply. - await EnsureLastPolledBlockIsSyncedWithChainAsync().ConfigureAwait(false); - try { + // In the event that the last polled block was set back a considerable distance from the tip, we need to first catch up faster. + // If we are already in the acceptable range, the usual logic will apply. + await EnsureLastPolledBlockIsSyncedWithChainAsync().ConfigureAwait(false); + foreach (KeyValuePair supportedChain in this.ethClientProvider.GetAllSupportedChains()) { CheckForBlockHeightOverrides(supportedChain.Key); @@ -272,7 +270,7 @@ public async Task InitializeAsync() repeatEvery: TimeSpans.TenSeconds, startAfter: TimeSpans.Minute); - // Initialize loop that polls for all transfer (SRC20 to ERC20) requests. + // Initialize loop that polls for all transfer (SRC20 to ERC20, and SRC721 to ERC721) requests. this.pollCirrusForTransfersLoop = this.asyncProvider.CreateAndRunAsyncLoop("PollCirrusForBurnsAsync", async (cancellation) => { if (this.initialBlockDownloadState.IsInitialBlockDownload()) @@ -429,11 +427,12 @@ private async Task PollCirrusForTransfersAsync() } // TODO: Need to build these sets across all supported chains - HashSet watchedSrc20Contracts = this.interopSettings.GetSettingsByChain(DestinationChain.ETH).WatchedErc20Contracts.Values.ToHashSet(); - HashSet watchedSrc721Contracts = this.interopSettings.GetSettingsByChain(DestinationChain.ETH).WatchedErc721Contracts.Values.ToHashSet(); + // Note that the values here are Cirrus contract addresses. + HashSet watchedErc20Contracts = this.interopSettings.GetSettingsByChain(DestinationChain.ETH).WatchedErc20Contracts.Values.ToHashSet(); + HashSet watchedErc721Contracts = this.interopSettings.GetSettingsByChain(DestinationChain.ETH).WatchedErc721Contracts.Values.ToHashSet(); - var zeroAddressRaw = new uint160(Address.Zero.ToBytes()); - string zeroAddress = zeroAddressRaw.ToBase58Address(this.network); + // The watched SRC721 contracts have an inverted key/value. So these are also Cirrus contract addresses. + HashSet watchedSrc721Contracts = this.interopSettings.GetSettingsByChain(DestinationChain.ETH).WatchedSrc721Contracts.Keys.ToHashSet(); foreach (NBitcoin.Transaction transaction in block.Transactions.Where(t => t.IsSmartContractExecTransaction())) { @@ -452,79 +451,104 @@ private async Task PollCirrusForTransfersAsync() { if (this.conversionRequestRepository.Get(receipt.TransactionHash) != null) { - this.logger.Info($"SRC20 transfer transaction '{receipt.TransactionHash}' already exists, ignoring."); + this.logger.Info($"Transfer transaction '{receipt.TransactionHash}' already exists, ignoring."); continue; } } // Filter out calls to contracts that we aren't monitoring. - // Note: The contract call for the burn is made against the SRC20/721 contract, but the burn event log is emitted with a To address of zero. - if (!watchedSrc20Contracts.Contains(receipt.To) && !watchedSrc721Contracts.Contains(receipt.To)) + // Note: The contract call for the burn/transfer is made against the SRC20/721 contract, but the burn/transfer event log is emitted with a To address of the actual recipient, i.e. zero in the case of a burn. + if (!watchedErc20Contracts.Contains(receipt.To) && !watchedErc721Contracts.Contains(receipt.To) && !watchedSrc721Contracts.Contains(receipt.To)) continue; + // We don't easily know if this was an SRC20 or SRC721 burn/transfer, thus we use the watched hashsets as a lookup. + // Initially assume that it is an ERC20 request, i.e. the SRC20 token is being burned to initiate the process. + ContractType contractType = ContractType.ERC20; + ConversionRequestType conversionRequestType = ConversionRequestType.Burn; + + // If the Cirrus contract address relates to a watched ERC721 contract then this must be regarded as an ERC721 request (SRC721 burn). + if (watchedErc721Contracts.Contains(receipt.To)) + contractType = ContractType.ERC721; + + // This is a special case, and will be a mint if the transfer is being made to the federation multisig contract. + if (watchedSrc721Contracts.Contains(receipt.To)) + { + contractType = ContractType.ERC721; + conversionRequestType = ConversionRequestType.Mint; + } + this.logger.Info($"Found transaction {receipt.TransactionHash} from {receipt.From} with a receipt that affects watched contract {receipt.To}."); - // We don't easily know if this was an SRC20 or SRC721 burn, unless we use the watched hashsets as a lookup. // In any case we have to validate all the fields in the relevant receipt logs. foreach (CirrusLogResponse log in receipt.Logs) { - TransferDetails src20burn = ExtractBurnFromBurnMetadataLog(log); + TransferDetails srcDetails = conversionRequestType == ConversionRequestType.Burn ? ExtractBurnFromBurnMetadataLog(log, contractType) : ExtractTransferFromTransferLog(log, contractType); - if (src20burn != null && src20burn.ContractType == ContractType.ERC20) + if (srcDetails == null) { - this.logger.Info($"Found a valid SRC20->ERC20 transfer transaction with metadata: {src20burn.To}."); + continue; + } - // Create the conversion request object. - var request = new ConversionRequest() - { - RequestId = receipt.TransactionHash, - RequestType = ConversionRequestType.Burn, - Amount = ConvertBigIntegerToUint256(src20burn.Value), - BlockHeight = applicableHeight, - DestinationAddress = src20burn.To, - DestinationChain = DestinationChain.ETH, - }; - - // Save it. - lock (this.repositoryLock) - { - this.conversionRequestRepository.Save(request); - } + this.logger.Info($"Found a valid {(contractType == ContractType.ERC721 ? "SRC721->ERC721" : "SRC20->ERC20")} {(conversionRequestType == ConversionRequestType.Burn ? "burn" : "transfer")} transaction with metadata: {srcDetails.To}."); - // First determine if the transaction contains a fee paying the multisig, if not, fail the transfer. - TxOut feeProvidedFromTransaction = RetrieveConversionRequestFeeOutput(transaction, request, receipt); - if (feeProvidedFromTransaction == null) - continue; + // Create the conversion request object. + var request = new ConversionRequest() + { + RequestId = receipt.TransactionHash, + RequestType = ConversionRequestType.Burn, + Amount = ConvertBigIntegerToUint256(srcDetails.Value), + BlockHeight = applicableHeight, + DestinationAddress = srcDetails.To, + DestinationChain = DestinationChain.ETH, + }; + + // Save it. + lock (this.repositoryLock) + { + this.conversionRequestRepository.Save(request); + } - // Determine and agree on a fee via all the multisig nodes. - // ERC20 transfers out of the multisig wallet have the same cost structure as a wSTRAX conversion, so we can use the same fee estimation logic. - InteropConversionRequestFee feeDeterminedByMultiSig = await this.conversionRequestFeeService.AgreeFeeForConversionRequestAsync(receipt.TransactionHash, (int)receipt.BlockNumber).ConfigureAwait(false); + // First determine if the transaction contains a fee paying the multisig, if not, fail the transfer. + TxOut feeProvidedFromTransaction = RetrieveConversionRequestFeeOutput(transaction, request, receipt); + if (feeProvidedFromTransaction == null) + continue; - // If a dynamic fee could not be determined, ignore the fee for now. - // Subsequent work in progress will allow us to reprocess "missed" multisig fees. - if (feeDeterminedByMultiSig == null || (feeDeterminedByMultiSig != null && feeDeterminedByMultiSig.State != InteropFeeState.AgreeanceConcluded)) - this.logger.Warn($"A dynamic fee for SRC20->ERC20 request '{receipt.TransactionHash}' could not be determined, ignoring fee until reprocessing at some later stage."); - else - ProcessConversionRequestFee(feeProvidedFromTransaction, feeDeterminedByMultiSig, receipt, applicableHeight, block); + // Determine and agree on a fee via all the multisig nodes. + // ERC20 transfers out of the multisig wallet have the same cost structure as a wSTRAX conversion, so we can use the same fee estimation logic. + InteropConversionRequestFee feeDeterminedByMultiSig = await this.conversionRequestFeeService.AgreeFeeForConversionRequestAsync(receipt.TransactionHash, (int)receipt.BlockNumber).ConfigureAwait(false); - KeyValuePair contractMapping = this.interopSettings.GetSettingsByChain(DestinationChain.ETH).WatchedErc20Contracts.First(c => c.Value == receipt.To); - SupportedContractAddress token = SupportedContractAddresses.ForNetwork(this.network.NetworkType).FirstOrDefault(t => t.NativeNetworkAddress.ToLowerInvariant() == contractMapping.Key.ToLowerInvariant()); - var tokenString = token == null ? contractMapping.Key : $"{token.TokenName}-{contractMapping.Key}"; + // If a dynamic fee could not be determined, ignore the fee for now. + // Subsequent work in progress will allow us to reprocess "missed" multisig fees. + if (feeDeterminedByMultiSig == null || (feeDeterminedByMultiSig != null && feeDeterminedByMultiSig.State != InteropFeeState.AgreeanceConcluded)) + this.logger.Warn($"A dynamic fee for SRC20->ERC20 request '{receipt.TransactionHash}' could not be determined, ignoring fee until reprocessing at some later stage."); + else + ProcessConversionRequestFee(feeProvidedFromTransaction, feeDeterminedByMultiSig, receipt, applicableHeight, block); - this.logger.Info($"A transfer request from CRS to '{tokenString}' will be processed."); + KeyValuePair contractMapping = this.interopSettings.GetSettingsByChain(DestinationChain.ETH).WatchedErc20Contracts.First(c => c.Value == receipt.To); + SupportedContractAddress token = SupportedContractAddresses.ForNetwork(this.network.NetworkType).FirstOrDefault(t => t.NativeNetworkAddress.ToLowerInvariant() == contractMapping.Key.ToLowerInvariant()); + var tokenString = token == null ? contractMapping.Key : $"{token.TokenName}-{contractMapping.Key}"; - request.Processed = false; - request.RequestStatus = ConversionRequestStatus.Unprocessed; - request.TokenContract = contractMapping.Key; + this.logger.Info($"A transfer request from CRS to '{tokenString}' will be processed."); - lock (this.repositoryLock) - { - this.conversionRequestRepository.Save(request); - } + request.Processed = false; + request.RequestStatus = ConversionRequestStatus.Unprocessed; + request.TokenContract = contractMapping.Key; + + lock (this.repositoryLock) + { + this.conversionRequestRepository.Save(request); } - // TODO: Awaiting an InterFluxNonFungibleToken contract that has a 'burn with metadata' method - // TransferDetails src721burn = ExtractBurnFromTransferLog(log, zeroAddress); + this.logger.Info($"A transfer request from CRS to '{tokenString}' will be processed."); + + request.Processed = false; + request.RequestStatus = ConversionRequestStatus.Unprocessed; + request.TokenContract = contractMapping.Key; + + lock (this.repositoryLock) + { + this.conversionRequestRepository.Save(request); + } } } catch (Exception e) @@ -616,11 +640,12 @@ private void ProcessConversionRequestFee(TxOut feeProvidedFromTransaction, Inter } /// - /// SRC20 InterFluxStandardToken burns. + /// SRC20 InterFluxStandardToken burns and SRC721 InterFluxNonFungibleToken burns. /// /// The log data from a Cirrus smart contract receipt. - /// A instance if this was a valid SRC721 burn, else null. - private TransferDetails ExtractBurnFromBurnMetadataLog(CirrusLogResponse log) + /// The type of contract this receipt is for. + /// A instance if this was a valid SRC20/721 burn, else null. + private TransferDetails ExtractBurnFromBurnMetadataLog(CirrusLogResponse log, ContractType contractType) { // We presume that anything emitting this event must be a burn and thus the 'To' address is neither included in the event nor validated here. if (log.Log.Event != "BurnMetadata") @@ -647,7 +672,7 @@ private TransferDetails ExtractBurnFromBurnMetadataLog(CirrusLogResponse log) var transfer = new TransferDetails() { - ContractType = ContractType.ERC20, + ContractType = contractType, From = fromAddress, To = metadataString, TransferType = TransferType.Burn, @@ -657,17 +682,12 @@ private TransferDetails ExtractBurnFromBurnMetadataLog(CirrusLogResponse log) return transfer; } - /// - /// For SRC721 NonFungibleToken burns. - /// /// The log data from a Cirrus smart contract receipt. - /// The base58 representation of . - /// A instance if this was a valid SRC721 burn, else null. - private TransferDetails ExtractBurnFromTransferLog(CirrusLogResponse log, string zeroAddress) + /// The type of contract this receipt is for. + /// A instance if this was a valid SRC20/721 transfer, else null. + private TransferDetails ExtractTransferFromTransferLog(CirrusLogResponse log, ContractType contractType) { - throw new NotImplementedException("This will not work yet, the NFT contract should instead be adapted to emit a BurnMetadata log as well"); - - if (log.Log.Event != "TransferLog") + if (log.Log.Event != "Transfer") return null; if (!log.Log.Data.TryGetValue("from", out object from)) @@ -680,25 +700,21 @@ private TransferDetails ExtractBurnFromTransferLog(CirrusLogResponse log, string string toAddress = (string)to; - // If it's not being transferred to the zero address it isn't a burn, and can thus be ignored. - if (toAddress != zeroAddress) - return null; - - if (!log.Log.Data.TryGetValue("tokenId", out object tokenId)) + if (!log.Log.Data.TryGetValue("amount", out object amount)) return null; - string tokenIdString = (string)tokenId; + string amountString = (string)amount; - if (!int.TryParse(tokenIdString, out int tokenIdInt)) + if (!BigInteger.TryParse(amountString, out BigInteger transferAmount)) return null; var transfer = new TransferDetails() { - ContractType = ContractType.ERC721, + ContractType = contractType, From = fromAddress, To = toAddress, - TransferType = TransferType.Burn, - Value = tokenIdInt + TransferType = TransferType.Transfer, + Value = transferAmount }; return transfer; @@ -835,16 +851,17 @@ private void ProcessWStraxBurn(string blockHash, string transactionHash, BurnFun } /// - /// Processes Transfer contract call events made against one of the ERC20 token contracts being monitored by the poller. + /// Processes Transfer contract call events made against one of the ERC20/ERC721 token contracts being monitored by the poller. /// /// Only transfers affecting the federation multisig wallet contract are processed. /// The hash of the block the transfer transaction appeared in. /// The hash of the transaction that the transfer method call appeared in. /// The address of the ERC20 contract that the transfer was actioned against. /// The metadata of the transfer method call. + /// The chain-specific client. private async Task ProcessTransferAsync(string blockHash, string transactionHash, string transferContractAddress, TransferDetails transfer, KeyValuePair supportedChain) { - this.logger.Info("Conversion transfer transaction '{0}' received from polled block '{1}', sender {2}.", transactionHash, blockHash, transfer.From); + this.logger.Info("Conversion transfer transaction '{0}' of type '{1}' received from polled block '{2}', sender {3}.", transactionHash, transfer.ContractType, blockHash, transfer.From); lock (this.repositoryLock) { @@ -885,12 +902,20 @@ private async Task ProcessTransferAsync(string blockHash, string transactionHash return; } - if (!this.interopSettings.GetSettingsByChain(supportedChain.Key).WatchedErc20Contracts.TryGetValue(transferContractAddress, out string destinationContract)) + string destinationContract = null; + + if (transfer.ContractType == ContractType.ERC20 && !this.interopSettings.GetSettingsByChain(supportedChain.Key).WatchedErc20Contracts.TryGetValue(transferContractAddress, out destinationContract)) { this.logger.Error("Unknown ERC20 contract address '{0}'; unable to map it to an SRC20 contract", transferContractAddress); return; } + if (transfer.ContractType == ContractType.ERC721 && !this.interopSettings.GetSettingsByChain(supportedChain.Key).WatchedErc721Contracts.TryGetValue(transferContractAddress, out destinationContract)) + { + this.logger.Error("Unknown ERC721 contract address '{0}'; unable to map it to an SRC721 contract", transferContractAddress); + return; + } + this.logger.Info($"Conversion transfer transaction '{transactionHash}' with transfer contract address {transferContractAddress} has destination contract destination address {destinationContract}."); lock (this.repositoryLock) @@ -905,7 +930,8 @@ private async Task ProcessTransferAsync(string blockHash, string transactionHash BlockHeight = (int)CalculateProcessingHeight(), DestinationAddress = destinationAddress, DestinationChain = DestinationChain.CIRRUS, - TokenContract = destinationContract // Need to record the mapping of which Cirrus token contract the minting request should be actioned against. + TokenContract = destinationContract, // Need to record the mapping of which Cirrus token contract the minting request should be actioned against. + TokenUri = transfer.Uri // If applicable (i.e. for an xRC721 conversion), the URI of the NFT being minted. }); } } @@ -923,7 +949,7 @@ private ulong ConvertWeiToSatoshi(BigInteger wei) /// /// Iterates through all unprocessed mint requests in the repository. /// - /// This processes all WSTRAX as well as ERC20 to SRC20 (USDT, WTBC etc) minting requests. + /// This processes all WSTRAX as well as ERC20 to SRC20 (USDT, WTBC etc) and ERC721 to SRC721 minting requests. /// /// If this node is regarded as the designated originator of the multisig transaction, it will submit the transfer transaction data to /// the multisig wallet contract on the Ethereum chain. This data consists of a method call to the transfer() method on the wrapped STRAX contract, @@ -969,41 +995,71 @@ private async Task ProcessMintRequestsAsync() // continue; //} - bool isTransfer = false; + // Default to ERC20. This value only gets used if this is not a wSTRAX transfer anyway. + ContractType contractType = ContractType.ERC20; + + if (this.interopSettings.ETHSettings.WatchedErc721Contracts.ContainsValue(request.TokenContract)) + { + contractType = ContractType.ERC721; + } + + // We need to determine exactly what kind of conversion request this is in order to call the appropriate portions of the state machine. + // If the destination chain is Cirrus then the request is one of the following: + // a. ERC20 -> SRC20 mint + // b. ERC721 -> SRC721 mint + // Note that 'returning' a Cirrus-originating NFT to the Cirrus chain is performed by burning the minted NFT on the Ethereum chain, not by transferring the Ethereum copy to the multisig wallet. + bool mintOnCirrus = false; if (!string.IsNullOrWhiteSpace(request.TokenContract) && request.DestinationChain == DestinationChain.CIRRUS) { - // This is an ERC20 -> SRC20 minting request, and therefore needs to be handled differently to wSTRAX. - this.logger.Info("Processing ERC20 to SRC20 transfer request {0}.", request.RequestId); - isTransfer = true; + // This is an ERCx -> SRCx minting request, and therefore needs to be handled differently to wSTRAX. + this.logger.Info("Processing {0} transfer request {1}.", (contractType == ContractType.ERC20 ? "ERC20 to SRC20" : "ERC721 to SRC721"), request.RequestId); + mintOnCirrus = true; } + // If the destination chain is not Cirrus, then the request could be one of the following: + // a. wSTRAX minting transaction + // b. SRC721 -> ERC721 mint + // Note that SRC20 -> ERC20 mints are currently not supported. + bool originator = DetermineConversionRequestOriginator(request.BlockHeight, out IFederationMember designatedMember); IETHClient clientForDestChain = this.ethClientProvider.GetClientForChain(request.DestinationChain); - if (!isTransfer) - await PerformReplenishmentAsync(clientForDestChain, request, originator); + if (!mintOnCirrus) + await PerformReplenishmentAsync(clientForDestChain, request, originator).ConfigureAwait(false); this.logger.Info("Processing mint request {0} on {1} chain.", request.RequestId, request.DestinationChain); - // The state machine gets shared between wSTRAX minting transactions, and ERC20/SRC20 transfers. - // TODO: Refactor this to use the InteropPollerStateMachine class for wSTRAX minting transactions, as it will make the logic a lot more concise + // The state machine gets shared between wSTRAX minting transactions, and ERCx/SRCx transfers. // The main difference between the two is that we do not have an IEthClient for Cirrus, and have to make contract calls via the HTTP API. // TODO: Perhaps the transactionId coordination should actually be done within the multisig contract. This will however increase gas costs for each mint. Maybe a Cirrus contract instead? switch (request.RequestStatus) { case ConversionRequestStatus.Unprocessed: { - stateMachine.Unprocessed(request, originator, designatedMember); + stateMachine.Unprocessed(request, originator, designatedMember, mintOnCirrus, contractType); break; } case ConversionRequestStatus.OriginatorNotSubmitted: { - if (isTransfer) + if (mintOnCirrus) { // TODO: Make a Cirrus version of SubmitTransactionAsync that can handle more generic operations than just minting - MultisigTransactionIdentifiers identifiers = await this.cirrusClient.MintAsync(request.TokenContract, request.DestinationAddress, new BigInteger(request.Amount.ToBytes())).ConfigureAwait(false); + MultisigTransactionIdentifiers identifiers = null; + + if (contractType == ContractType.ERC20) + identifiers = await this.cirrusClient.MintAsync(request.TokenContract, request.DestinationAddress, new BigInteger(request.Amount.ToBytes())).ConfigureAwait(false); + else + { + BigInteger tokenId = new BigInteger(request.Amount.ToBytes()); + + identifiers = await this.cirrusClient.MintNftAsync(request.TokenContract, request.DestinationAddress, tokenId, request.TokenUri).ConfigureAwait(false); + + // Update the tokenId mapping in the key value store contract. + //string key = $"{request.TokenContract}:{request.Amount}"; + //MultisigTransactionIdentifiers kvsIdentifiers = await this.cirrusClient.SetKeyValueStoreAsync(key, tokenId.ToString()).ConfigureAwait(false); + } if (identifiers.TransactionId == BigInteger.MinusOne) { @@ -1027,7 +1083,7 @@ private async Task ProcessMintRequestsAsync() BigInteger amountToSubmit = this.CoinsToWei(request.Amount.GetLow64()); string contractToSubmit = this.interopSettings.GetSettingsByChain(request.DestinationChain).WrappedStraxContractAddress; - await stateMachine.OriginatorNotSubmittedAsync(request, clientForDestChain, this.interopSettings, amountToSubmit, contractToSubmit).ConfigureAwait(false); + await stateMachine.OriginatorNotSubmittedAsync(request, clientForDestChain, this.interopSettings, amountToSubmit, contractToSubmit, mintOnCirrus, contractType).ConfigureAwait(false); } break; @@ -1035,7 +1091,7 @@ private async Task ProcessMintRequestsAsync() case ConversionRequestStatus.OriginatorSubmitting: { - await stateMachine.OriginatorSubmittingAsync(request, clientForDestChain, this.cirrusClient, this.SubmissionConfirmationThreshold, isTransfer).ConfigureAwait(false); + await stateMachine.OriginatorSubmittingAsync(request, clientForDestChain, this.cirrusClient, this.SubmissionConfirmationThreshold, mintOnCirrus, contractType).ConfigureAwait(false); break; } @@ -1048,7 +1104,7 @@ private async Task ProcessMintRequestsAsync() // The coordination mechanism safeguards against this, as any such spurious transaction will not receive acceptance votes. // TODO: The transactionId should be accompanied by the hash of the submission transaction on the Ethereum chain so that it can be verified - await stateMachine.OriginatorSubmittedAsync(request, this.interopSettings, isTransfer).ConfigureAwait(false); + await stateMachine.OriginatorSubmittedAsync(request, this.interopSettings, mintOnCirrus, false, contractType).ConfigureAwait(false); break; } @@ -1061,7 +1117,7 @@ private async Task ProcessMintRequestsAsync() { // The originator isn't responsible for anything further at this point, except for periodically checking the confirmation count. // The non-originators also need to monitor the confirmation count so that they know when to mark the transaction as processed locally. - BigInteger confirmationCount = isTransfer ? await this.cirrusClient.GetMultisigConfirmationCountAsync(transactionId3, (ulong)this.chainIndexer.Tip.Height).ConfigureAwait(false) : await clientForDestChain.GetMultisigConfirmationCountAsync(transactionId3).ConfigureAwait(false); + BigInteger confirmationCount = mintOnCirrus ? await this.cirrusClient.GetMultisigConfirmationCountAsync(transactionId3, (ulong)this.chainIndexer.Tip.Height).ConfigureAwait(false) : await clientForDestChain.GetMultisigConfirmationCountAsync(transactionId3).ConfigureAwait(false); if (confirmationCount >= this.interopSettings.GetSettingsByChain(request.DestinationChain).MultisigWalletQuorum) { @@ -1080,7 +1136,7 @@ private async Task ProcessMintRequestsAsync() // Even though the vote is finalised, other nodes may come and go. So we re-broadcast the finalised votes to all federation peers. // Nodes will simply ignore the messages if they are not relevant. - await this.BroadcastCoordinationVoteRequestAsync(request.RequestId, transactionId3, request.DestinationChain, isTransfer).ConfigureAwait(false); + await this.BroadcastCoordinationVoteRequestAsync(request.RequestId, transactionId3, request.DestinationChain, mintOnCirrus, false, false).ConfigureAwait(false); // No state transition here, we are waiting for sufficient confirmations. } @@ -1104,7 +1160,7 @@ private async Task ProcessMintRequestsAsync() this.logger.Info("Quorum reached for conversion transaction '{0}' with transactionId '{1}', submitting confirmation to contract.", request.RequestId, agreedUponId); string confirmationHash; - if (isTransfer) + if (mintOnCirrus) { (string TransactionHash, string Message) result = await this.cirrusClient.ConfirmTransactionAsync(agreedUponId).ConfigureAwait(false); @@ -1154,7 +1210,7 @@ private async Task ProcessMintRequestsAsync() this.conversionRequestCoordinationService.AddVote(request.RequestId, transactionId4, this.federationManager.CurrentFederationKey.PubKey); - await this.BroadcastCoordinationVoteRequestAsync(request.RequestId, transactionId4, request.DestinationChain, isTransfer).ConfigureAwait(false); + await this.BroadcastCoordinationVoteRequestAsync(request.RequestId, transactionId4, request.DestinationChain, mintOnCirrus, false, false).ConfigureAwait(false); } // No state transition here, as we are waiting for the candidate transactionId to progress to an agreed upon transactionId via a quorum. @@ -1180,13 +1236,14 @@ private async Task ProcessMintRequestsAsync() } /// - /// Iterates through all unprocessed SRC20 burn requests in the repository. + /// Iterates through all unprocessed SRC20/SRC721 burn requests in the repository. /// /// This includes SRC20 to ERC20 burns. /// /// If this node is regarded as the designated originator of the multisig transaction, it will submit the transfer transaction data to - /// the multisig wallet contract on the Ethereum chain. This data consists of a method call to the transfer() method on the ERC20 contract, - /// as well as the intended recipient address and amount of tokens to be transferred. + /// the multisig wallet contract on the Ethereum chain. This data consists of a method call to the transfer() method on the ERC20 contract + /// (or the safeTransferFrom() method in the case of an ERC721 contract), as well as the intended recipient address and amount/tokenId of + /// tokens to be transferred. /// private async Task ProcessBurnRequestsAsync() { @@ -1221,31 +1278,43 @@ private async Task ProcessBurnRequestsAsync() // continue; //} - bool originator = DetermineConversionRequestOriginator(request.BlockHeight, out IFederationMember designatedMember); + // Default to ERC20. This value only gets used if this is not a wSTRAX transfer anyway. + ContractType contractType = ContractType.ERC20; - IETHClient clientForDestChain = this.ethClientProvider.GetClientForChain(request.DestinationChain); + if (this.interopSettings.ETHSettings.WatchedErc721Contracts.ContainsValue(request.TokenContract)) + { + contractType = ContractType.ERC721; + } - this.logger.Info("Processing burn request '{0}' on {1} chain.", request.RequestId, request.DestinationChain); + bool originator = DetermineConversionRequestOriginator(request.BlockHeight, out IFederationMember designatedMember); - BigInteger balanceRemaining = await clientForDestChain.GetErc20BalanceAsync(this.interopSettings.GetSettingsByChain(request.DestinationChain).MultisigWalletAddress, request.TokenContract).ConfigureAwait(false); + IETHClient clientForDestChain = this.ethClientProvider.GetClientForChain(request.DestinationChain); - // The request amount is already denominated in 'wei' (or the Cirrus SRC20 equivalent) so we just need to change the underlying type. - BigInteger conversionAmountInWei = new BigInteger(request.Amount.ToBytes()); + this.logger.Info("Processing burn request '{0}' of type {1} on {2} chain.", request.RequestId, contractType, request.DestinationChain); - // Unlike the wSTRAX contract, the multisig cannot mint new tokens on the ERC20 contracts it is monitoring. - // So we retrieve the balance as a sanity check, but if it is insufficient then something has gone badly wrong and we have to abort processing. - if (conversionAmountInWei >= balanceRemaining) + if (contractType == ContractType.ERC20) { - this.logger.Error($"Multisig {nameof(balanceRemaining)}={balanceRemaining} is insufficient for {nameof(conversionAmountInWei)}={conversionAmountInWei}, failed to process transaction {request.RequestId}."); + BigInteger balanceRemaining = await clientForDestChain.GetErc20BalanceAsync(this.interopSettings.GetSettingsByChain(request.DestinationChain).MultisigWalletAddress, request.TokenContract).ConfigureAwait(false); - request.Processed = true; + // The request amount is already denominated in 'wei' (or the Cirrus SRC20 equivalent) so we just need to change the underlying type. + BigInteger conversionAmountInWei = new BigInteger(request.Amount.ToBytes()); - lock (this.repositoryLock) + // Unlike the wSTRAX contract, the multisig cannot mint new tokens on the ERC20 contracts it is monitoring. + // So we retrieve the balance as a sanity check, but if it is insufficient then something has gone badly wrong and we have to abort processing. + if (conversionAmountInWei >= balanceRemaining) { - this.conversionRequestRepository.Save(request); - } + this.logger.Error($"Multisig {nameof(balanceRemaining)}={balanceRemaining} is insufficient for {nameof(conversionAmountInWei)}={conversionAmountInWei}, failed to process transaction {request.RequestId}."); - continue; + request.RequestStatus = ConversionRequestStatus.Failed; + request.Processed = true; + + lock (this.repositoryLock) + { + this.conversionRequestRepository.Save(request); + } + + continue; + } } // TODO: Perhaps the transactionId coordination should actually be done within the multisig contract. This will however increase gas costs for each mint. Maybe a Cirrus contract instead? @@ -1253,7 +1322,7 @@ private async Task ProcessBurnRequestsAsync() { case ConversionRequestStatus.Unprocessed: { - stateMachine.Unprocessed(request, originator, designatedMember); + stateMachine.Unprocessed(request, originator, designatedMember, false, contractType); break; } @@ -1261,14 +1330,14 @@ private async Task ProcessBurnRequestsAsync() { BigInteger amountToSubmit = new BigInteger(request.Amount.ToBytes()); - await stateMachine.OriginatorNotSubmittedAsync(request, clientForDestChain, this.interopSettings, amountToSubmit, request.TokenContract).ConfigureAwait(false); + await stateMachine.OriginatorNotSubmittedAsync(request, clientForDestChain, this.interopSettings, amountToSubmit, request.TokenContract, false, contractType).ConfigureAwait(false); break; } case ConversionRequestStatus.OriginatorSubmitting: { - await stateMachine.OriginatorSubmittingAsync(request, clientForDestChain, this.cirrusClient, this.SubmissionConfirmationThreshold, false).ConfigureAwait(false); + await stateMachine.OriginatorSubmittingAsync(request, clientForDestChain, this.cirrusClient, this.SubmissionConfirmationThreshold, false, contractType).ConfigureAwait(false); break; } @@ -1281,14 +1350,14 @@ private async Task ProcessBurnRequestsAsync() // The coordination mechanism safeguards against this, as any such spurious transaction will not receive acceptance votes. // TODO: The transactionId should be accompanied by the hash of the submission transaction on the Ethereum chain so that it can be verified - await stateMachine.OriginatorSubmittedAsync(request, this.interopSettings, false).ConfigureAwait(false); + await stateMachine.OriginatorSubmittedAsync(request, this.interopSettings, false, false, contractType).ConfigureAwait(false); break; } case ConversionRequestStatus.VoteFinalised: { - await stateMachine.VoteFinalisedAsync(request, clientForDestChain, this.interopSettings).ConfigureAwait(false); + await stateMachine.VoteFinalisedAsync(request, clientForDestChain, this.interopSettings, contractType).ConfigureAwait(false); break; } @@ -1301,7 +1370,7 @@ private async Task ProcessBurnRequestsAsync() // This is done within the InteropBehavior automatically, we just check each poll loop if a transaction has enough votes yet. // Each node must only ever confirm a single transactionId for a given conversion transaction. - await stateMachine.NotOriginatorAsync(request, clientForDestChain, this.interopSettings).ConfigureAwait(false); + await stateMachine.NotOriginatorAsync(request, clientForDestChain, this.interopSettings, contractType).ConfigureAwait(false); break; } @@ -1447,7 +1516,7 @@ private async Task PerformReplenishmentAsync(IETHClient clientForDestinationChai // First check if an existing unprocessed replenishment exists. replenishmentTransaction = this.replenishmentKeyValueStore.FindUnprocessed(); - // Initiate a new replenisment. + // Initiate a new replenishment. if (replenishmentTransaction == null) { MultisigTransactionIdentifiers identifiers = await this.ethClientProvider.GetClientForChain(request.DestinationChain).SubmitTransactionAsync(this.interopSettings.GetSettingsByChain(request.DestinationChain).WrappedStraxContractAddress, 0, mintData, gasPrice).ConfigureAwait(false); @@ -1474,7 +1543,7 @@ private async Task PerformReplenishmentAsync(IETHClient clientForDestinationChai // Now we need to broadcast the mint transactionId to the other multisig nodes so that they can sign it off. // TODO: The other multisig nodes must be careful not to blindly trust that any given transactionId relates to a mint transaction. Need to validate the recipient - await this.BroadcastCoordinationVoteRequestAsync(mintRequestId, replenishmentTransaction.TransactionId, request.DestinationChain, false, true).ConfigureAwait(false); + await this.BroadcastCoordinationVoteRequestAsync(mintRequestId, replenishmentTransaction.TransactionId, request.DestinationChain, false, true, false).ConfigureAwait(false); } else this.logger.Info("Insufficient reserve balance remaining, waiting for originator to initiate mint transaction to replenish reserve."); @@ -1501,7 +1570,7 @@ private async Task PerformReplenishmentAsync(IETHClient clientForDestinationChai { this.logger.Debug("Originator broadcasting id {0}.", replenishmentTransaction.TransactionId); - await this.BroadcastCoordinationVoteRequestAsync(mintRequestId, replenishmentTransaction.TransactionId, request.DestinationChain, false, true).ConfigureAwait(false); + await this.BroadcastCoordinationVoteRequestAsync(mintRequestId, replenishmentTransaction.TransactionId, request.DestinationChain, false, true, false).ConfigureAwait(false); } else { @@ -1517,7 +1586,7 @@ private async Task PerformReplenishmentAsync(IETHClient clientForDestinationChai this.conversionRequestCoordinationService.AddVote(mintRequestId, ourTransactionId, this.federationManager.CurrentFederationKey.PubKey); // Broadcast our vote. - await this.BroadcastCoordinationVoteRequestAsync(mintRequestId, ourTransactionId, request.DestinationChain, false, true).ConfigureAwait(false); + await this.BroadcastCoordinationVoteRequestAsync(mintRequestId, ourTransactionId, request.DestinationChain, false, true, false).ConfigureAwait(false); } } @@ -1584,10 +1653,10 @@ private async Task PerformReplenishmentAsync(IETHClient clientForDestinationChai } } - private async Task BroadcastCoordinationVoteRequestAsync(string requestId, BigInteger transactionId, DestinationChain destinationChain, bool isTransfer, bool isReplenishment = false) + private async Task BroadcastCoordinationVoteRequestAsync(string requestId, BigInteger transactionId, DestinationChain destinationChain, bool isTransfer, bool isReplenishment, bool isKeyValue) { string signature = this.federationManager.CurrentFederationKey.SignMessage(requestId + ((int)transactionId)); - await this.federatedPegBroadcaster.BroadcastAsync(ConversionRequestPayload.Request(requestId, (int)transactionId, signature, destinationChain, isTransfer, isReplenishment)).ConfigureAwait(false); + await this.federatedPegBroadcaster.BroadcastAsync(ConversionRequestPayload.Request(requestId, (int)transactionId, signature, destinationChain, isTransfer, isReplenishment, isKeyValue)).ConfigureAwait(false); } private BigInteger CoinsToWei(ulong satoshi) @@ -1638,30 +1707,40 @@ private void AddComponentStats(StringBuilder benchLog) private (int Decimals, string DestinationText) RetrieveDecimalsAndTokenDestination(ConversionRequest request) { int decimals = 8; - string destinationText = request.DestinationChain.ToString(); - SupportedContractAddress token = null; if (request.RequestType == ConversionRequestType.Burn) { if (string.IsNullOrEmpty(request.TokenContract)) - destinationText = "wSTRAX->STRAX"; + return (decimals, "wSTRAX->STRAX"); else - token = SupportedContractAddresses.ForNetwork(this.network.NetworkType).FirstOrDefault(t => t.NativeNetworkAddress.ToLowerInvariant() == request.TokenContract.ToLowerInvariant()); + { + SupportedContractAddress token = SupportedContractAddresses.ForNetwork(this.network.NetworkType).FirstOrDefault(t => t.NativeNetworkAddress.ToLowerInvariant() == request.TokenContract.ToLowerInvariant()); + if (token != null) + return (token.Decimals, $"{token.TokenName}->{request.DestinationChain}"); + else + return (8, "Cirrus->Unknown"); + } } else { if (string.IsNullOrEmpty(request.TokenContract)) - destinationText = "STRAX->wSTRAX"; + return (decimals, "STRAX->wSTRAX"); else - token = SupportedContractAddresses.ForNetwork(this.network.NetworkType).FirstOrDefault(t => t.SRC20Address.ToLowerInvariant() == request.TokenContract.ToLowerInvariant()); + return DetermineTokenOrNftContract(request); } + } + + private (int Decimals, string DestinationText) DetermineTokenOrNftContract(ConversionRequest request) + { + SupportedContractAddress token = SupportedContractAddresses.ForNetwork(this.network.NetworkType).FirstOrDefault(t => t.SRC20Address.ToLowerInvariant() == request.TokenContract.ToLowerInvariant()); if (token != null) - { - decimals = token.Decimals; - destinationText = $"{token.TokenName}->{request.DestinationChain}"; - } + return (token.Decimals, $"{token.TokenName}->{request.DestinationChain}"); + + string nftContract = this.interopSettings.ETHSettings.WatchedErc721Contracts.Values.FirstOrDefault(n => n.ToLowerInvariant() == request.TokenContract.ToLowerInvariant()); + if (nftContract != null) + return (8, "NFT->Cirrus"); - return (decimals, destinationText); + return (8, "Unknown->Cirrus"); } private ulong CalculateProcessingHeight() @@ -1697,4 +1776,4 @@ public void Dispose() this.pollCirrusForTransfersLoop?.Dispose(); } } -} \ No newline at end of file +} diff --git a/src/Stratis.Bitcoin.Features.Interop/InteropPollerStateMachine.cs b/src/Stratis.Bitcoin.Features.Interop/InteropPollerStateMachine.cs index d190b31118..2d765745b5 100644 --- a/src/Stratis.Bitcoin.Features.Interop/InteropPollerStateMachine.cs +++ b/src/Stratis.Bitcoin.Features.Interop/InteropPollerStateMachine.cs @@ -33,9 +33,9 @@ public InteropPollerStateMachine(ILogger logger, IExternalApiPoller externalApiP this.federatedPegBroadcaster = federatedPegBroadcaster; } - public void Unprocessed(ConversionRequest request, bool originator, IFederationMember designatedMember) + public void Unprocessed(ConversionRequest request, bool originator, IFederationMember designatedMember, bool mintOnCirrus, ContractType contractType) { - string transactionType = request.RequestType == ConversionRequestType.Burn ? "SRC20->ERC20" : "CRS->WSTRAX"; + string transactionType = DetermineTransactionTypeForLog(request, mintOnCirrus, contractType); if (originator) { @@ -52,15 +52,35 @@ public void Unprocessed(ConversionRequest request, bool originator, IFederationM } } - public async Task OriginatorNotSubmittedAsync(ConversionRequest request, IETHClient clientForDestChain, InteropSettings interopSettings, BigInteger submissionAmount, string contractToSubmit) + public async Task OriginatorNotSubmittedAsync(ConversionRequest request, IETHClient clientForDestChain, InteropSettings interopSettings, BigInteger submissionAmount, string contractToSubmit, bool mintOnCirrus, ContractType contractType) { - string transactionType = request.RequestType == ConversionRequestType.Burn ? "SRC20->ERC20" : "CRS->WSTRAX"; + string transactionType = DetermineTransactionTypeForLog(request, mintOnCirrus, contractType); this.logger.Info($"Request '{request}'; '{transactionType}' conversion not yet submitted, checking which gas price to use."); - // First construct the necessary transfer() transaction data, utilising the ABI of a standard ERC20 contract. - // When this constructed transaction is actually executed, the transfer's source account will be the account executing the transaction i.e. the multisig contract address. - string abiData = clientForDestChain.EncodeTransferParams(request.DestinationAddress, submissionAmount); + string abiData = null; + + if (contractType == ContractType.ERC721) + { + if (request.RequestType == ConversionRequestType.Burn) + { + // A burn implies we are releasing the NFT from the multisig wallet back to the given destination address. + // First construct the necessary safeTransferFrom() transaction data, utilising the ABI of a standard ERC721 contract. + abiData = clientForDestChain.EncodeNftTransferParams(interopSettings.ETHSettings.MultisigWalletAddress, request.DestinationAddress, submissionAmount); + } + else + { + // If this is instead a mint request, a transfer into the multisig has been detected on the Cirrus chain and the corresponding NFT needs to be minted on the Ethereum chain. + abiData = clientForDestChain.EncodeNftMintParams(request.DestinationAddress, submissionAmount, request.TokenUri); + } + } + else + { + // First construct the necessary transfer() transaction data, utilising the ABI of a standard ERC20 contract. + abiData = clientForDestChain.EncodeTransferParams(request.DestinationAddress, submissionAmount); + } + + // When the constructed transaction is actually executed, the transfer's source account will be the account executing the transaction i.e. the multisig contract address. int gasPrice = this.externalApiPoller.GetGasPrice(); @@ -93,11 +113,11 @@ public async Task OriginatorNotSubmittedAsync(ConversionRequest request, IETHCli request.ExternalChainTxEventId = identifiers.TransactionId.ToString(); } - public async Task OriginatorSubmittingAsync(ConversionRequest request, IETHClient clientForDestChain, ICirrusContractClient cirrusClient, BigInteger submissionConfirmationThreshold, bool isTransfer) + public async Task OriginatorSubmittingAsync(ConversionRequest request, IETHClient clientForDestChain, ICirrusContractClient cirrusClient, BigInteger submissionConfirmationThreshold, bool mintOnCirrus, ContractType contractType) { - string transactionType = DetermineTransactionTypeForLog(request, isTransfer); + string transactionType = DetermineTransactionTypeForLog(request, mintOnCirrus, contractType); - (BigInteger confirmationCount, string blockHash) = isTransfer ? cirrusClient.GetConfirmations(request.ExternalChainBlockHeight) : await clientForDestChain.GetConfirmationsAsync(request.ExternalChainTxHash).ConfigureAwait(false); + (BigInteger confirmationCount, string blockHash) = mintOnCirrus ? cirrusClient.GetConfirmations(request.ExternalChainBlockHeight) : await clientForDestChain.GetConfirmationsAsync(request.ExternalChainTxHash).ConfigureAwait(false); this.logger.Info($"Originator confirming {transactionType} transaction id '{request.ExternalChainTxHash}' '({request.ExternalChainTxEventId})' before broadcasting; confirmations: {confirmationCount}; Block Hash {blockHash}."); @@ -111,15 +131,15 @@ public async Task OriginatorSubmittingAsync(ConversionRequest request, IETHClien request.RequestStatus = ConversionRequestStatus.OriginatorSubmitted; } - public async Task OriginatorSubmittedAsync(ConversionRequest request, InteropSettings interopSettings, bool isTransfer) + public async Task OriginatorSubmittedAsync(ConversionRequest request, InteropSettings interopSettings, bool mintOnCirrus, bool isKeyValue, ContractType contractType) { - string transactionType = DetermineTransactionTypeForLog(request, isTransfer); + string transactionType = DetermineTransactionTypeForLog(request, mintOnCirrus, contractType); BigInteger transactionId2 = this.conversionRequestCoordinationService.GetCandidateTransactionId(request.RequestId); if (transactionId2 != BigInteger.MinusOne) { - await this.BroadcastCoordinationVoteRequestAsync(request.RequestId, transactionId2, request.DestinationChain, isTransfer).ConfigureAwait(false); + await this.BroadcastCoordinationVoteRequestAsync(request.RequestId, transactionId2, request.DestinationChain, mintOnCirrus, isKeyValue).ConfigureAwait(false); BigInteger agreedTransactionId = this.conversionRequestCoordinationService.GetAgreedTransactionId(request.RequestId, interopSettings.GetSettingsByChain(request.DestinationChain).MultisigWalletQuorum); @@ -132,9 +152,9 @@ public async Task OriginatorSubmittedAsync(ConversionRequest request, InteropSet } } - public async Task VoteFinalisedAsync(ConversionRequest request, IETHClient clientForDestChain, InteropSettings interopSettings) + public async Task VoteFinalisedAsync(ConversionRequest request, IETHClient clientForDestChain, InteropSettings interopSettings, ContractType contractType) { - string transactionType = DetermineTransactionTypeForLog(request, !string.IsNullOrEmpty(request.TokenContract)); + string transactionType = DetermineTransactionTypeForLog(request, !string.IsNullOrEmpty(request.TokenContract), contractType); BigInteger transactionId3 = this.conversionRequestCoordinationService.GetAgreedTransactionId(request.RequestId, interopSettings.GetSettingsByChain(request.DestinationChain).MultisigWalletQuorum); @@ -161,16 +181,16 @@ public async Task VoteFinalisedAsync(ConversionRequest request, IETHClient clien // There are not enough confirmations yet. // Even though the vote is finalised, other nodes may come and go. So we re-broadcast the finalised votes to all federation peers. // Nodes will simply ignore the messages if they are not relevant. - await this.BroadcastCoordinationVoteRequestAsync(request.RequestId, transactionId3, request.DestinationChain, false).ConfigureAwait(false); + await this.BroadcastCoordinationVoteRequestAsync(request.RequestId, transactionId3, request.DestinationChain, false, false).ConfigureAwait(false); // No state transition here, we are waiting for sufficient confirmations. } } } - public async Task NotOriginatorAsync(ConversionRequest request, IETHClient clientForDestChain, InteropSettings interopSettings) + public async Task NotOriginatorAsync(ConversionRequest request, IETHClient clientForDestChain, InteropSettings interopSettings, ContractType contractType) { - string transactionType = DetermineTransactionTypeForLog(request, !string.IsNullOrEmpty(request.TokenContract)); + string transactionType = DetermineTransactionTypeForLog(request, !string.IsNullOrEmpty(request.TokenContract), contractType); BigInteger agreedUponId = this.conversionRequestCoordinationService.GetAgreedTransactionId(request.RequestId, interopSettings.GetSettingsByChain(request.DestinationChain).MultisigWalletQuorum); @@ -208,14 +228,14 @@ public async Task NotOriginatorAsync(ConversionRequest request, IETHClient clien this.conversionRequestCoordinationService.AddVote(request.RequestId, transactionId4, this.federationManager.CurrentFederationKey.PubKey); - await this.BroadcastCoordinationVoteRequestAsync(request.RequestId, transactionId4, request.DestinationChain, false).ConfigureAwait(false); + await this.BroadcastCoordinationVoteRequestAsync(request.RequestId, transactionId4, request.DestinationChain, false, false).ConfigureAwait(false); } // No state transition here, as we are waiting for the candidate transactionId to progress to an agreed upon transactionId via a quorum. } } - private string DetermineTransactionTypeForLog(ConversionRequest request, bool isTransfer) + private string DetermineTransactionTypeForLog(ConversionRequest request, bool mintOnCirrus, ContractType contractType) { string transactionType; @@ -223,19 +243,37 @@ private string DetermineTransactionTypeForLog(ConversionRequest request, bool is { transactionType = "CRS->WSTRAX"; - if (isTransfer) - transactionType = "ERC20->CRS"; + if (mintOnCirrus) + { + if (contractType == ContractType.ERC20) + { + transactionType = "ERC20->CRS"; + } + else + { + transactionType = "ERC721->CRS"; + } + } } else - transactionType = "SRC20->ERC20"; + { + if (contractType == ContractType.ERC20) + { + transactionType = "SRC20->ERC20"; + } + else + { + transactionType = "SRC721->ERC721"; + } + } return transactionType; } - private async Task BroadcastCoordinationVoteRequestAsync(string requestId, BigInteger transactionId, DestinationChain destinationChain, bool isTransfer) + private async Task BroadcastCoordinationVoteRequestAsync(string requestId, BigInteger transactionId, DestinationChain destinationChain, bool mintOnCirrus, bool isKeyValue) { string signature = this.federationManager.CurrentFederationKey.SignMessage(requestId + ((int)transactionId)); - await this.federatedPegBroadcaster.BroadcastAsync(ConversionRequestPayload.Request(requestId, (int)transactionId, signature, destinationChain, isTransfer, false)).ConfigureAwait(false); + await this.federatedPegBroadcaster.BroadcastAsync(ConversionRequestPayload.Request(requestId, (int)transactionId, signature, destinationChain, mintOnCirrus, false, isKeyValue)).ConfigureAwait(false); } } } diff --git a/src/Stratis.Bitcoin.Features.Interop/Models/InteropStatusResponseModel.cs b/src/Stratis.Bitcoin.Features.Interop/Models/InteropStatusResponseModel.cs index fe247874a6..51dc4b3594 100644 --- a/src/Stratis.Bitcoin.Features.Interop/Models/InteropStatusResponseModel.cs +++ b/src/Stratis.Bitcoin.Features.Interop/Models/InteropStatusResponseModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Numerics; using Newtonsoft.Json; namespace Stratis.Bitcoin.Features.Interop.Models @@ -7,5 +8,8 @@ public class InteropStatusResponseModel { [JsonProperty(PropertyName = "receivedVotes")] public Dictionary> ReceivedVotes { get; set; } + + [JsonProperty(PropertyName = "transactionIdVotes")] + public Dictionary> TransactionIdVotes { get; set; } } } diff --git a/src/Stratis.Bitcoin.Features.Interop/Payloads/ConversionRequestPayload.cs b/src/Stratis.Bitcoin.Features.Interop/Payloads/ConversionRequestPayload.cs index cee73089ba..394d702a83 100644 --- a/src/Stratis.Bitcoin.Features.Interop/Payloads/ConversionRequestPayload.cs +++ b/src/Stratis.Bitcoin.Features.Interop/Payloads/ConversionRequestPayload.cs @@ -14,6 +14,7 @@ public sealed class ConversionRequestPayload : Payload private bool isRequesting; private bool isTransfer; private bool isReplenishment; + private bool isKeyValue; public string RequestId { get { return this.requestId; } } @@ -41,12 +42,18 @@ public sealed class ConversionRequestPayload : Payload /// public bool IsReplenishment { get { return this.isReplenishment; } } + /// + /// True if the request in question is negotiating a multisig key value store transactionId. + /// False if not. + /// + public bool IsKeyValue { get { return this.isKeyValue; } } + /// Parameterless constructor needed for deserialization. public ConversionRequestPayload() { } - private ConversionRequestPayload(string requestId, int transactionId, string signature, DestinationChain destinationChain, bool isRequesting, bool isTransfer, bool isReplenishment) + private ConversionRequestPayload(string requestId, int transactionId, string signature, DestinationChain destinationChain, bool isRequesting, bool isTransfer, bool isReplenishment, bool isKeyValue) { this.requestId = requestId; this.transactionId = transactionId; @@ -55,6 +62,7 @@ private ConversionRequestPayload(string requestId, int transactionId, string sig this.isRequesting = isRequesting; this.isTransfer = isTransfer; this.isReplenishment = isReplenishment; + this.isKeyValue = isKeyValue; } /// @@ -67,16 +75,17 @@ public override void ReadWriteCore(BitcoinStream stream) stream.ReadWrite(ref this.isRequesting); stream.ReadWrite(ref this.isTransfer); stream.ReadWrite(ref this.isReplenishment); + stream.ReadWrite(ref this.isKeyValue); } - public static ConversionRequestPayload Request(string requestId, int transactionId, string signature, DestinationChain destinationChain, bool isTransfer, bool isReplenishment) + public static ConversionRequestPayload Request(string requestId, int transactionId, string signature, DestinationChain destinationChain, bool isTransfer, bool isReplenishment, bool isKeyValue) { - return new ConversionRequestPayload(requestId, transactionId, signature, destinationChain, true, isTransfer, isReplenishment); + return new ConversionRequestPayload(requestId, transactionId, signature, destinationChain, true, isTransfer, isReplenishment, isKeyValue); } - public static ConversionRequestPayload Reply(string requestId, int transactionId, string signature, DestinationChain destinationChain, bool isTransfer, bool isReplenishment) + public static ConversionRequestPayload Reply(string requestId, int transactionId, string signature, DestinationChain destinationChain, bool isTransfer, bool isReplenishment, bool isKeyValue) { - return new ConversionRequestPayload(requestId, transactionId, signature, destinationChain, false, isTransfer, isReplenishment); + return new ConversionRequestPayload(requestId, transactionId, signature, destinationChain, false, isTransfer, isReplenishment, isKeyValue); } /// diff --git a/src/Stratis.Bitcoin.Features.Interop/Settings/CirrusInteropSettings.cs b/src/Stratis.Bitcoin.Features.Interop/Settings/CirrusInteropSettings.cs index b88b99e2a5..d15bb9b6fb 100644 --- a/src/Stratis.Bitcoin.Features.Interop/Settings/CirrusInteropSettings.cs +++ b/src/Stratis.Bitcoin.Features.Interop/Settings/CirrusInteropSettings.cs @@ -9,6 +9,8 @@ public sealed class CirrusInteropSettings : ETHInteropSettings /// This is the URL of the Cirrus node's API, for actioning SRC20 contract calls. public string CirrusClientUrl { get; set; } + public string CirrusKeyValueStoreContractAddress { get; set; } + public string CirrusSmartContractActiveAddress { get; set; } public string CirrusMultisigContractAddress { get; set; } @@ -18,6 +20,7 @@ public sealed class CirrusInteropSettings : ETHInteropSettings public CirrusInteropSettings(NodeSettings nodeSettings) : base(nodeSettings) { this.CirrusClientUrl = nodeSettings.ConfigReader.GetOrDefault("cirrusclienturl", nodeSettings.Network.IsTest() ? "http://localhost:38223" : "http://localhost:37223"); + this.CirrusKeyValueStoreContractAddress = nodeSettings.ConfigReader.GetOrDefault("cirruskeyvaluestorecontractaddress", null); this.CirrusSmartContractActiveAddress = nodeSettings.ConfigReader.GetOrDefault("cirrussmartcontractactiveaddress", null); this.CirrusMultisigContractAddress = nodeSettings.ConfigReader.GetOrDefault("cirrusmultisigcontractaddress", null); } diff --git a/src/Stratis.Bitcoin.Features.Interop/Settings/InteropSettings.cs b/src/Stratis.Bitcoin.Features.Interop/Settings/InteropSettings.cs index 8f53c4ebaf..6b257bf353 100644 --- a/src/Stratis.Bitcoin.Features.Interop/Settings/InteropSettings.cs +++ b/src/Stratis.Bitcoin.Features.Interop/Settings/InteropSettings.cs @@ -104,6 +104,10 @@ public class ETHInteropSettings /// against the federation multisig wallet. These are mapped to their corresponding SRC721 contract. public Dictionary WatchedErc721Contracts { get; set; } + /// A collection of contract addresses for SRC721 tokens that should be monitored for minting events. + /// These are mapped to a suitable ERC721 contract that the federation has minting rights for. + public Dictionary WatchedSrc721Contracts { get; set; } + #region unused /// This is intended for future functionality and should therefore not be provided/set yet. @@ -130,6 +134,7 @@ public ETHInteropSettings(NodeSettings nodeSettings) this.InteropContractAddress = nodeSettings.ConfigReader.GetOrDefault(this.GetSettingsPrefix() + "interopcontractaddress", ""); this.WatchedErc20Contracts = new Dictionary(); this.WatchedErc721Contracts = new Dictionary(); + this.WatchedSrc721Contracts = new Dictionary(); string watchErc20Key = this.GetSettingsPrefix() + "watcherc20"; @@ -175,6 +180,28 @@ public ETHInteropSettings(NodeSettings nodeSettings) this.WatchedErc721Contracts[splitWatched[0]] = splitWatched[1]; } + string watchSrc721Key = this.GetSettingsPrefix() + "watchsrc721"; + + foreach (string watched in nodeSettings.ConfigReader.GetAll(watchSrc721Key)) + { + if (!watched.Contains(",")) + { + throw new Exception($"Value of -{watchSrc721Key} invalid, should be -{watchSrc721Key}=,: {watched}"); + } + + string[] splitWatched = watched.Split(","); + + if (splitWatched.Length != 2) + { + throw new Exception($"Value of -{watchSrc721Key} invalid, should be -{watchSrc721Key}=,: {watched}"); + } + + // Ensure that a valid Cirrus address was provided. + BitcoinAddress.Create(splitWatched[0], nodeSettings.Network); + + this.WatchedSrc721Contracts[splitWatched[0]] = splitWatched[1]; + } + this.MultisigWalletQuorum = nodeSettings.ConfigReader.GetOrDefault(MultisigWalletContractQuorumKey, 6); this.MultisigWalletAddress = nodeSettings.ConfigReader.GetOrDefault(multisigWalletContractAddressKey, ""); this.WrappedStraxContractAddress = nodeSettings.ConfigReader.GetOrDefault(wrappedStraxContractAddressKey, ""); diff --git a/src/Stratis.Bitcoin.Features.Interop/Stratis.Bitcoin.Features.Interop.csproj b/src/Stratis.Bitcoin.Features.Interop/Stratis.Bitcoin.Features.Interop.csproj index b8aaf9e32c..feca437ce6 100644 --- a/src/Stratis.Bitcoin.Features.Interop/Stratis.Bitcoin.Features.Interop.csproj +++ b/src/Stratis.Bitcoin.Features.Interop/Stratis.Bitcoin.Features.Interop.csproj @@ -1,8 +1,8 @@  - netcoreapp3.1 - 1.3.2.4 + net6.0 + 1.4.0.7 Stratis Group Ltd. Stratis.Features.Interop Stratis.Features.Interop diff --git a/src/Stratis.Bitcoin.Features.LightWallet/LightWalletSyncManager.cs b/src/Stratis.Bitcoin.Features.LightWallet/LightWalletSyncManager.cs index abd6d86884..89035233c5 100644 --- a/src/Stratis.Bitcoin.Features.LightWallet/LightWalletSyncManager.cs +++ b/src/Stratis.Bitcoin.Features.LightWallet/LightWalletSyncManager.cs @@ -93,11 +93,11 @@ public void Start() this.walletTip = this.chainIndexer.GetHeader(this.walletManager.WalletTipHash); } - this.transactionAddedSubscription = this.signals.Subscribe(this.OnTransactionAdded); + this.transactionAddedSubscription = this.signals.Subscribe(this.OnTransactionAdded); this.transactionRemovedSubscription = this.signals.Subscribe(this.OnTransactionRemoved); } - private void OnTransactionAdded(TransactionAddedToMemoryPool transactionAdded) + private void OnTransactionAdded(TransactionAddedToMemoryPoolEvent transactionAdded) { this.walletManager.ProcessTransaction(transactionAdded.AddedTransaction); } diff --git a/src/Stratis.Bitcoin.Features.LightWallet/Stratis.Bitcoin.Features.LightWallet.csproj b/src/Stratis.Bitcoin.Features.LightWallet/Stratis.Bitcoin.Features.LightWallet.csproj index 637d7147d2..f776bbb6b9 100644 --- a/src/Stratis.Bitcoin.Features.LightWallet/Stratis.Bitcoin.Features.LightWallet.csproj +++ b/src/Stratis.Bitcoin.Features.LightWallet/Stratis.Bitcoin.Features.LightWallet.csproj @@ -1,13 +1,13 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.LightWallet Stratis.Features.LightWallet false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.MemoryPool.Tests/Stratis.Bitcoin.Features.MemoryPool.Tests.csproj b/src/Stratis.Bitcoin.Features.MemoryPool.Tests/Stratis.Bitcoin.Features.MemoryPool.Tests.csproj index ea74f44fe1..e82f0cf39e 100644 --- a/src/Stratis.Bitcoin.Features.MemoryPool.Tests/Stratis.Bitcoin.Features.MemoryPool.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.MemoryPool.Tests/Stratis.Bitcoin.Features.MemoryPool.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.MemoryPool.Tests Stratis.Bitcoin.Features.MemoryPool.Tests true diff --git a/src/Stratis.Bitcoin.Features.MemoryPool/MempoolBehavior.cs b/src/Stratis.Bitcoin.Features.MemoryPool/MempoolBehavior.cs index c4290c3226..424a74317a 100644 --- a/src/Stratis.Bitcoin.Features.MemoryPool/MempoolBehavior.cs +++ b/src/Stratis.Bitcoin.Features.MemoryPool/MempoolBehavior.cs @@ -36,6 +36,14 @@ public class MempoolBehavior : NetworkPeerBehavior /// private const int InventoryBroadcastMax = 7 * InventoryBroadcastInterval; + /// + /// Prevents a single peer from sending us huge quantities of fake `inv` messages with nonexistent hashes. + /// This value is somewhat arbitrarily chosen, it is far greater than the maximum number of transactions that can be stored in any single block. + /// The number of unmined transactions in the mempool should ideally hover around the number in a single block or below, otherwise the mempool will eventually + /// fill completely. However, on some networks it is not uncommon for there to be surges in transaction volume, so we leave some leeway. + /// + private const int MaximumInventoryToTrackCount = 50000; + /// Memory pool validator for validating transactions. private readonly IMempoolValidator validator; @@ -73,7 +81,7 @@ public class MempoolBehavior : NetworkPeerBehavior /// Filter for inventory known. /// State that is local to the behavior. /// - private readonly HashSet filterInventoryKnown; + private readonly LruHashSet filterInventoryKnown; /// /// Locking object for memory pool behaviour. @@ -113,11 +121,12 @@ public MempoolBehavior( this.lockObject = new object(); this.inventoryTxToSend = new HashSet(); - this.filterInventoryKnown = new HashSet(); + this.filterInventoryKnown = new LruHashSet(MaximumInventoryToTrackCount); this.isPeerWhitelistedForRelay = false; this.isBlocksOnlyMode = false; } + // TODO: Not clear what this was intended for, maybe to periodically re-request mempool contents from peer nodes? /// Time of last memory pool request in unix time. public long LastMempoolReq { get; private set; } @@ -261,14 +270,14 @@ private async Task SendMempoolPayloadAsync(INetworkPeer peer, MempoolPayload mes } } - this.filterInventoryKnown.Add(hash); + this.filterInventoryKnown.AddOrUpdate(hash); transactionsToSend.Add(hash); this.logger.LogDebug("Added transaction ID '{0}' to inventory list.", hash); } } this.logger.LogDebug("Sending transaction inventory to peer '{0}'.", peer.RemoteSocketEndpoint); - await this.SendAsTxInventoryAsync(peer, transactionsToSend); + await this.SendAsTxInventoryAsync(peer, transactionsToSend).ConfigureAwait(false); this.LastMempoolReq = this.mempoolManager.DateTimeProvider.GetTime(); } @@ -303,7 +312,13 @@ private async Task ProcessInvAsync(INetworkPeer peer, InvPayload invPayload) { foreach (var inv in inventoryTxs) { - this.filterInventoryKnown.Add(inv.Hash); + // It is unlikely that the transaction will be in the to-send hashmap, but there is no harm attempting to remove it anyway. + this.inventoryTxToSend.Remove(inv.Hash); + + // At this point we have no idea whether the proffered hashes exist or are spurious. So we have to rely on the bounded LRU hashmap implementation + // to control the maximum number of extant hashes that we track for this peer. It isn't really worth trying to evict mined transactions from this + // hashmap. + this.filterInventoryKnown.AddOrUpdate(inv.Hash); } } @@ -344,19 +359,24 @@ private async Task ProcessGetDataAsync(INetworkPeer peer, GetDataPayload getData foreach (InventoryVector item in getDataPayload.Inventory.Where(inv => inv.Type.HasFlag(InventoryType.MSG_TX))) { - // TODO: check if we need to add support for "not found" + // We could just check once on entry into this method, but it is possible for the peer's connected status to change + // between inv transmission attempts. + if (!peer.IsConnected) + continue; TxMempoolInfo trxInfo = await this.mempoolManager.InfoAsync(item.Hash).ConfigureAwait(false); - if (trxInfo != null) + if (trxInfo == null) { - // TODO: strip block of witness if peer does not support - if (peer.IsConnected) - { - this.logger.LogDebug("Sending transaction '{0}' to peer '{1}'.", item.Hash, peer.RemoteSocketEndpoint); - await peer.SendMessageAsync(new TxPayload(trxInfo.Trx.WithOptions(peer.SupportedTransactionOptions, this.network.Consensus.ConsensusFactory))).ConfigureAwait(false); - } + // https://btcinformation.org/en/developer-reference#notfound + // https://github.com/bitcoin/bitcoin/pull/2192 + await peer.SendMessageAsync(new NotFoundPayload(InventoryType.MSG_TX, item.Hash)).ConfigureAwait(false); + + continue; } + + this.logger.LogDebug("Sending transaction '{0}' to peer '{1}'.", item.Hash, peer.RemoteSocketEndpoint); + await peer.SendMessageAsync(new TxPayload(trxInfo.Trx.WithOptions(peer.SupportedTransactionOptions, this.network.Consensus.ConsensusFactory))).ConfigureAwait(false); } } @@ -382,14 +402,14 @@ private async Task ProcessTxPayloadAsync(INetworkPeer peer, TxPayload transactio // add to local filter lock (this.lockObject) { - this.filterInventoryKnown.Add(trxHash); + this.filterInventoryKnown.AddOrUpdate(trxHash); } this.logger.LogDebug("Added transaction ID '{0}' to known inventory filter.", trxHash); var state = new MempoolValidationState(true); - if (!await this.orphans.AlreadyHaveAsync(trxHash) && await this.validator.AcceptToMemoryPool(state, trx)) + if (!await this.orphans.AlreadyHaveAsync(trxHash).ConfigureAwait(false) && await this.validator.AcceptToMemoryPool(state, trx).ConfigureAwait(false)) { - await this.validator.SanityCheck(); + await this.validator.SanityCheck().ConfigureAwait(false); this.RelayTransaction(trxHash); this.signals.Publish(new TransactionReceived(trx)); @@ -399,7 +419,7 @@ private async Task ProcessTxPayloadAsync(INetworkPeer peer, TxPayload transactio this.logger.LogInformation("Transaction ID '{0}' accepted to memory pool from peer '{1}' (poolsz {2} txn, {3} kb).", trxHash, peer.RemoteSocketEndpoint, mmsize, memdyn / 1000); - await this.orphans.ProcessesOrphansAsync(this, trx); + await this.orphans.ProcessesOrphansAsync(this, trx).ConfigureAwait(false); } else if (state.MissingInputs) { @@ -472,6 +492,7 @@ private void AddTransactionToSend(uint256 hash) { if (!this.filterInventoryKnown.Contains(hash)) { + // We do not need to bound this in the same way as the 'known' map, because transactions are only sent/relayed when they are considered valid, plus this map is constantly shrinking as txes are sent. this.inventoryTxToSend.Add(hash); } } @@ -572,7 +593,7 @@ public async Task SendTrickleAsync() foreach (uint256 hash in findInMempool) { // Not in the mempool anymore? don't bother sending it. - TxMempoolInfo txInfo = await this.mempoolManager.InfoAsync(hash); + TxMempoolInfo txInfo = await this.mempoolManager.InfoAsync(hash).ConfigureAwait(false); if (txInfo == null) { this.logger.LogDebug("Transaction ID '{0}' not added to inventory list, no longer in mempool.", hash); diff --git a/src/Stratis.Bitcoin.Features.MemoryPool/Rules/CheckCoinViewMempoolRule.cs b/src/Stratis.Bitcoin.Features.MemoryPool/Rules/CheckCoinViewMempoolRule.cs index 3cea22920a..ba3d4b6793 100644 --- a/src/Stratis.Bitcoin.Features.MemoryPool/Rules/CheckCoinViewMempoolRule.cs +++ b/src/Stratis.Bitcoin.Features.MemoryPool/Rules/CheckCoinViewMempoolRule.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using System; +using Microsoft.Extensions.Logging; using NBitcoin; using Stratis.Bitcoin.Features.MemoryPool.Interfaces; using Stratis.Bitcoin.Utilities; @@ -41,6 +42,13 @@ public override void CheckTransaction(MempoolValidationContext context) { context.State.MissingInputs = true; this.logger.LogTrace("(-)[FAIL_MISSING_INPUTS]"); + int height = 0; + try + { + height = context.View.GetTipHash()?.Height ?? 0; + } + catch (Exception) { } + this.logger.LogDebug($"Transaction '{context.TransactionHash}' has a missing input at height { height }: '{(txin.PrevOut.Hash.ToString() + "-" + txin.PrevOut.N)}'. The full transaction is '{context.Transaction.ToHex()}'."); context.State.Fail(MempoolErrors.MissingOrSpentInputs).Throw(); } } diff --git a/src/Stratis.Bitcoin.Features.MemoryPool/Stratis.Bitcoin.Features.MemoryPool.csproj b/src/Stratis.Bitcoin.Features.MemoryPool/Stratis.Bitcoin.Features.MemoryPool.csproj index f8831a1ded..46eb5e90a1 100644 --- a/src/Stratis.Bitcoin.Features.MemoryPool/Stratis.Bitcoin.Features.MemoryPool.csproj +++ b/src/Stratis.Bitcoin.Features.MemoryPool/Stratis.Bitcoin.Features.MemoryPool.csproj @@ -3,7 +3,7 @@ Stratis Bitcoin Features MemoryPool Stratis.Bitcoin.Features.MemoryPool - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.MemoryPool Stratis.Features.MemoryPool false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False library Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.MemoryPool/TransactionAddedToMemoryPool.cs b/src/Stratis.Bitcoin.Features.MemoryPool/TransactionAddedToMemoryPoolEvent.cs similarity index 54% rename from src/Stratis.Bitcoin.Features.MemoryPool/TransactionAddedToMemoryPool.cs rename to src/Stratis.Bitcoin.Features.MemoryPool/TransactionAddedToMemoryPoolEvent.cs index 7421b3c497..fcc911c432 100644 --- a/src/Stratis.Bitcoin.Features.MemoryPool/TransactionAddedToMemoryPool.cs +++ b/src/Stratis.Bitcoin.Features.MemoryPool/TransactionAddedToMemoryPoolEvent.cs @@ -1,5 +1,6 @@ using NBitcoin; using Stratis.Bitcoin.EventBus; +using Stratis.Bitcoin.Features.MemoryPool.Interfaces; namespace Stratis.Bitcoin.Features.MemoryPool { @@ -7,13 +8,15 @@ namespace Stratis.Bitcoin.Features.MemoryPool /// Event that is executed when a transaction is removed from the mempool. /// /// - public class TransactionAddedToMemoryPool : EventBase + public class TransactionAddedToMemoryPoolEvent : EventBase { public Transaction AddedTransaction { get; } - - public TransactionAddedToMemoryPool(Transaction addedTransaction) + public readonly long MemPoolSize; + + public TransactionAddedToMemoryPoolEvent(Transaction addedTransaction, long mempoolSize) { this.AddedTransaction = addedTransaction; - } + this.MemPoolSize = mempoolSize; + } } } \ No newline at end of file diff --git a/src/Stratis.Bitcoin.Features.MemoryPool/TxMemPool.cs b/src/Stratis.Bitcoin.Features.MemoryPool/TxMemPool.cs index c770a821b9..a73cf8987b 100644 --- a/src/Stratis.Bitcoin.Features.MemoryPool/TxMemPool.cs +++ b/src/Stratis.Bitcoin.Features.MemoryPool/TxMemPool.cs @@ -379,7 +379,7 @@ public bool AddUnchecked(uint256 hash, TxMempoolEntry entry, SetEntries setAnces if (this.signals != null) { - this.signals.Publish(new TransactionAddedToMemoryPool(entry.Transaction)); + this.signals.Publish(new TransactionAddedToMemoryPoolEvent(entry.Transaction, this.MapTx.Count)); } return true; diff --git a/src/Stratis.Bitcoin.Features.Miner.Tests/Stratis.Bitcoin.Features.Miner.Tests.csproj b/src/Stratis.Bitcoin.Features.Miner.Tests/Stratis.Bitcoin.Features.Miner.Tests.csproj index 654e224be8..36c13cbba0 100644 --- a/src/Stratis.Bitcoin.Features.Miner.Tests/Stratis.Bitcoin.Features.Miner.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.Miner.Tests/Stratis.Bitcoin.Features.Miner.Tests.csproj @@ -2,7 +2,7 @@ false - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.Miner.Tests Stratis.Bitcoin.Features.Miner.Tests true diff --git a/src/Stratis.Bitcoin.Features.Miner/Stratis.Bitcoin.Features.Miner.csproj b/src/Stratis.Bitcoin.Features.Miner/Stratis.Bitcoin.Features.Miner.csproj index 6291eb0e0a..0dbe278e8f 100644 --- a/src/Stratis.Bitcoin.Features.Miner/Stratis.Bitcoin.Features.Miner.csproj +++ b/src/Stratis.Bitcoin.Features.Miner/Stratis.Bitcoin.Features.Miner.csproj @@ -3,7 +3,7 @@ Stratis Bitcoin Features Miner Stratis.Bitcoin.Features.Miner - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.Miner Stratis.Features.Miner false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.Notifications.Tests/Stratis.Bitcoin.Features.Notifications.Tests.csproj b/src/Stratis.Bitcoin.Features.Notifications.Tests/Stratis.Bitcoin.Features.Notifications.Tests.csproj index 1cc4b1baee..9541e56058 100644 --- a/src/Stratis.Bitcoin.Features.Notifications.Tests/Stratis.Bitcoin.Features.Notifications.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.Notifications.Tests/Stratis.Bitcoin.Features.Notifications.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.Notifications.Tests Stratis.Bitcoin.Features.Notifications.Tests true diff --git a/src/Stratis.Bitcoin.Features.Notifications/Stratis.Bitcoin.Features.Notifications.csproj b/src/Stratis.Bitcoin.Features.Notifications/Stratis.Bitcoin.Features.Notifications.csproj index 55470c3d46..584ca05d7f 100644 --- a/src/Stratis.Bitcoin.Features.Notifications/Stratis.Bitcoin.Features.Notifications.csproj +++ b/src/Stratis.Bitcoin.Features.Notifications/Stratis.Bitcoin.Features.Notifications.csproj @@ -3,7 +3,7 @@ Stratis Bitcoin Features Notifications Stratis.Bitcoin.Features.Notifications - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.Notifications Stratis.Features.Notifications false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common.csproj b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common.csproj index ebd3b93084..bf6d932493 100644 --- a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common.csproj +++ b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.PoA.IntegrationTests.Common Stratis.Features.PoA.IntegrationTests.Common @@ -13,7 +13,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False diff --git a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoAMiner.cs b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoAMiner.cs index f265e63bce..7ebd2a3ee0 100644 --- a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoAMiner.cs +++ b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoAMiner.cs @@ -13,6 +13,7 @@ using Stratis.Bitcoin.Features.PoA.Voting; using Stratis.Bitcoin.Features.Wallet.Interfaces; using Stratis.Bitcoin.Interfaces; +using Stratis.Bitcoin.Signals; using Stratis.Bitcoin.Utilities; namespace Stratis.Bitcoin.Features.PoA.IntegrationTests.Common @@ -45,9 +46,28 @@ public TestPoAMiner( PoASettings poAMinerSettings, IAsyncProvider asyncProvider, IIdleFederationMembersKicker idleFederationMembersKicker, + ISignals signals, NodeSettings nodeSettings) - : base(consensusManager, dateTimeProvider, network, nodeLifetime, ibdState, blockDefinition, slotsManager, connectionManager, - poaHeaderValidator, federationManager, federationHistory, integrityValidator, walletManager, nodeStats, votingManager, poAMinerSettings, asyncProvider, idleFederationMembersKicker, nodeSettings) + : base(consensusManager, + dateTimeProvider, + network, + nodeLifetime, + ibdState, + blockDefinition, + slotsManager, + connectionManager, + poaHeaderValidator, + federationManager, + federationHistory, + integrityValidator, + walletManager, + nodeStats, + votingManager, + poAMinerSettings, + asyncProvider, + idleFederationMembersKicker, + signals, + nodeSettings) { this.timeProvider = dateTimeProvider as EditableTimeProvider; diff --git a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/EnableVoteKickingTests.cs b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/EnableVoteKickingTests.cs index 97f8074144..91505ef05f 100644 --- a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/EnableVoteKickingTests.cs +++ b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/EnableVoteKickingTests.cs @@ -32,7 +32,10 @@ public async Task EnableAutoKickAsync() targetSpacingSeconds: 60, votingEnabled: true, autoKickIdleMembers: false, - federationMemberMaxIdleTimeSeconds: oldOptions.FederationMemberMaxIdleTimeSeconds); + federationMemberMaxIdleTimeSeconds: oldOptions.FederationMemberMaxIdleTimeSeconds) + { + PollExpiryBlocks = 450 + }; CoreNode node1 = builder.CreatePoANode(votingNetwork1, votingNetwork1.FederationKey1).Start(); CoreNode node2 = builder.CreatePoANode(votingNetwork2, votingNetwork2.FederationKey2).Start(); @@ -54,7 +57,10 @@ public async Task EnableAutoKickAsync() targetSpacingSeconds: 60, votingEnabled: true, autoKickIdleMembers: true, - federationMemberMaxIdleTimeSeconds: idleTimeSeconds); + federationMemberMaxIdleTimeSeconds: idleTimeSeconds) + { + PollExpiryBlocks = 450 + }; // Restart node 1 to ensure that we have the new network consensus options which reflects // the autokicking enabled. diff --git a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/Stratis.Bitcoin.Features.PoA.IntegrationTests.csproj b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/Stratis.Bitcoin.Features.PoA.IntegrationTests.csproj index 2035026db1..da0290ee2d 100644 --- a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/Stratis.Bitcoin.Features.PoA.IntegrationTests.csproj +++ b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/Stratis.Bitcoin.Features.PoA.IntegrationTests.csproj @@ -2,13 +2,13 @@ Exe - netcoreapp3.1 - + net6.0 - - + + + all runtime; build; native; contentfiles; analyzers diff --git a/src/Stratis.Bitcoin.Features.PoA.Tests/Stratis.Bitcoin.Features.PoA.Tests.csproj b/src/Stratis.Bitcoin.Features.PoA.Tests/Stratis.Bitcoin.Features.PoA.Tests.csproj index 3eac5fc701..4320a0ce2e 100644 --- a/src/Stratis.Bitcoin.Features.PoA.Tests/Stratis.Bitcoin.Features.PoA.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.PoA.Tests/Stratis.Bitcoin.Features.PoA.Tests.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 diff --git a/src/Stratis.Bitcoin.Features.PoA/Events/MiningStatisticsEvent.cs b/src/Stratis.Bitcoin.Features.PoA/Events/MiningStatisticsEvent.cs new file mode 100644 index 0000000000..e6bb8a15f7 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.PoA/Events/MiningStatisticsEvent.cs @@ -0,0 +1,16 @@ +using Stratis.Bitcoin.EventBus; + +namespace Stratis.Bitcoin.Features.PoA.Events +{ + public class MiningStatisticsEvent : EventBase + { + public readonly MiningStatisticsModel MiningStatistics; + public readonly int FederationMemberSize; + + public MiningStatisticsEvent(MiningStatisticsModel miningStatistics, int federationMemberSize) + { + this.MiningStatistics = miningStatistics; + this.FederationMemberSize = federationMemberSize; + } + } +} diff --git a/src/Stratis.Bitcoin.Features.PoA/PoAConsensusOptions.cs b/src/Stratis.Bitcoin.Features.PoA/PoAConsensusOptions.cs index 5c26d099fd..2d93d0d1d6 100644 --- a/src/Stratis.Bitcoin.Features.PoA/PoAConsensusOptions.cs +++ b/src/Stratis.Bitcoin.Features.PoA/PoAConsensusOptions.cs @@ -58,6 +58,14 @@ public class PoAConsensusOptions : ConsensusOptions /// public int Release1300ActivationHeight { get; set; } + /// + /// The height at which Release 1.4.0.0 became active. + /// + /// This was primarily used for activating ScriptPubkey sorting for paying multisig recipients. + /// + /// + public int Release1400ActivationHeight { get; set; } + /// /// The height at which inituitive mining slots become active. /// Legacy mining slots are determined by mining_slot = block_height % number_of_federation_members. diff --git a/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs b/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs index 0f7e8e717c..0ddc4fd537 100644 --- a/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs +++ b/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs @@ -14,11 +14,13 @@ using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Consensus.Validators; using Stratis.Bitcoin.Features.Miner; +using Stratis.Bitcoin.Features.PoA.Events; using Stratis.Bitcoin.Features.PoA.Voting; using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.Features.Wallet.Interfaces; using Stratis.Bitcoin.Interfaces; using Stratis.Bitcoin.Mining; +using Stratis.Bitcoin.Signals; using Stratis.Bitcoin.Utilities; using TracerAttributes; @@ -104,6 +106,8 @@ public class PoAMiner : IPoAMiner private Script walletScriptPubKey; + private readonly ISignals signals; + public PoAMiner( IConsensusManager consensusManager, IDateTimeProvider dateTimeProvider, @@ -123,6 +127,7 @@ public PoAMiner( PoASettings poAMinerSettings, IAsyncProvider asyncProvider, IIdleFederationMembersKicker idleFederationMembersKicker, + ISignals signals, NodeSettings nodeSettings) { this.consensusManager = consensusManager; @@ -149,6 +154,7 @@ public PoAMiner( this.nodeSettings = nodeSettings; this.miningStatistics = new MiningStatisticsModel(); + this.signals = signals; nodeStats.RegisterStats(this.AddComponentStats, StatsType.Component, this.GetType().Name); } @@ -216,6 +222,8 @@ private void GatherMiningStatistics() this.miningStatisticsLog = log.ToString(); + this.signals?.Publish(new MiningStatisticsEvent(this.miningStatistics, 0)); + return; } @@ -227,6 +235,8 @@ private void GatherMiningStatistics() this.miningStatisticsLog = log.ToString(); + this.signals?.Publish(new MiningStatisticsEvent(this.miningStatistics, 0)); + return; } @@ -243,7 +253,7 @@ private void GatherMiningStatistics() // TODO: Make this a command line option. bool includeHeight = false; - log.AppendLine($"Mining information for the last { maxDepth } blocks."); + log.AppendLine($"Mining information for the last {maxDepth} blocks."); if (includeHeight) log.AppendLine("Note 'MISS' indicates a slot where a miner didn't produce a block."); else @@ -282,7 +292,7 @@ private void GatherMiningStatistics() if (includeHeight) { string strHeight = minedInThisSlot ? currentHeader.Height.ToString().PadLeft(7) : "---MISS"; - log.Append($"{strHeight}:{ pubKeyRepresentation } "); + log.Append($"{strHeight}:{pubKeyRepresentation} "); } else { @@ -298,10 +308,12 @@ private void GatherMiningStatistics() this.miningStatistics.MinerHits = hitCount; + this.signals?.Publish(new MiningStatisticsEvent(this.miningStatistics, maxDepth)); + log.Append("..."); log.AppendLine(); log.AppendLine($"Miner hits".PadRight(LoggingConfiguration.ColumnLength) + $": {hitCount} of {maxDepth}({(((float)hitCount / (float)maxDepth)).ToString("P2")})"); - log.AppendLine($"Miner idle time".PadRight(LoggingConfiguration.ColumnLength) + $": { TimeSpan.FromSeconds(this.network.ConsensusOptions.TargetSpacingSeconds * (maxDepth - hitCount)).ToString(@"hh\:mm\:ss")}"); + log.AppendLine($"Miner idle time".PadRight(LoggingConfiguration.ColumnLength) + $": {TimeSpan.FromSeconds(this.network.ConsensusOptions.TargetSpacingSeconds * (maxDepth - hitCount)).ToString(@"hh\:mm\:ss")}"); log.AppendLine(); this.miningStatisticsLog = log.ToString(); diff --git a/src/Stratis.Bitcoin.Features.PoA/Stratis.Bitcoin.Features.PoA.csproj b/src/Stratis.Bitcoin.Features.PoA/Stratis.Bitcoin.Features.PoA.csproj index a0b4ad3489..92e06526fd 100644 --- a/src/Stratis.Bitcoin.Features.PoA/Stratis.Bitcoin.Features.PoA.csproj +++ b/src/Stratis.Bitcoin.Features.PoA/Stratis.Bitcoin.Features.PoA.csproj @@ -3,7 +3,7 @@ Stratis Bitcoin Features PoA Stratis.Bitcoin.Features.PoA - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.PoA Stratis.Features.PoA false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.RPC.Tests/Stratis.Bitcoin.Features.RPC.Tests.csproj b/src/Stratis.Bitcoin.Features.RPC.Tests/Stratis.Bitcoin.Features.RPC.Tests.csproj index 67cac583ac..f244616756 100644 --- a/src/Stratis.Bitcoin.Features.RPC.Tests/Stratis.Bitcoin.Features.RPC.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.RPC.Tests/Stratis.Bitcoin.Features.RPC.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.RPC.Tests Stratis.Bitcoin.Features.RPC.Tests true diff --git a/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionModelBinder.cs b/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionModelBinder.cs new file mode 100644 index 0000000000..b5f5b515e9 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionModelBinder.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Stratis.Bitcoin.Features.RPC.Models; +using Stratis.Bitcoin.Utilities.JsonConverters; + +namespace Stratis.Bitcoin.Features.RPC.ModelBinders +{ + public class CreateRawTransactionInputBinder : IModelBinder, IModelBinderProvider + { + public Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext.ModelType != typeof(CreateRawTransactionInput[])) + { + return Task.CompletedTask; + } + + ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); + + string key = val.FirstValue; + + if (key == null) + { + return Task.CompletedTask; + } + + CreateRawTransactionInput[] inputs = Serializer.ToObject(key); + + bindingContext.Result = ModelBindingResult.Success(inputs); + + return Task.CompletedTask; + } + + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context.Metadata.ModelType == typeof(CreateRawTransactionInput[])) + return this; + + return null; + } + } +} diff --git a/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionOutputBinder.cs b/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionOutputBinder.cs new file mode 100644 index 0000000000..a6c357e2cf --- /dev/null +++ b/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionOutputBinder.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Newtonsoft.Json.Linq; +using Stratis.Bitcoin.Features.RPC.Models; + +public class CreateRawTransactionOutputBinder : IModelBinder, IModelBinderProvider +{ + public Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext.ModelType != typeof(CreateRawTransactionOutput[])) + { + return Task.CompletedTask; + } + + ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); + + string raw = val.FirstValue; + + if (raw == null) + { + return Task.CompletedTask; + } + + JArray outerArray = JArray.Parse(raw); + + var model = new List(); + + foreach (var item in outerArray.Children()) + { + foreach (JProperty property in item.Properties()) + { + string addressOrData = property.Name; + string value = property.Value.ToString(); + + model.Add(new CreateRawTransactionOutput() { Key = addressOrData, Value = value }); + } + } + + bindingContext.Result = ModelBindingResult.Success(model.ToArray()); + + return Task.CompletedTask; + } + + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context.Metadata.ModelType == typeof(CreateRawTransactionOutput[])) + return this; + + return null; + } +} diff --git a/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs b/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs new file mode 100644 index 0000000000..d0366e7828 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs @@ -0,0 +1,36 @@ +using NBitcoin; +using Newtonsoft.Json; + +namespace Stratis.Bitcoin.Features.RPC.Models +{ + /// + /// Used for storing the data that represents desired transaction inputs for the createrawtransaction RPC call. + /// + public class CreateRawTransactionInput + { + [JsonProperty(PropertyName = "txid")] + public uint256 TxId { get; set; } + + [JsonProperty(PropertyName = "vout")] + public uint VOut { get; set; } + + /// + /// If not provided, the sequence in the created transaction will be set to uint.MaxValue by default. + /// + [JsonProperty(PropertyName = "sequence")] + public uint? Sequence { get; set; } + } + + /// + /// Used for storing the key-value pairs that represent desired transaction outputs for the createrawtransaction RPC call. + /// + /// The key and value properties are populated by a custom model binder and therefore never appear with those names in the raw JSON. + public class CreateRawTransactionOutput + { + [JsonProperty] + public string Key { get; set; } + + [JsonProperty] + public string Value { get; set; } + } +} diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs b/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs index 506f626eec..4f9bd73b78 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs @@ -6,6 +6,7 @@ using NBitcoin.DataEncoders; using NBitcoin.Protocol; using Newtonsoft.Json.Linq; +using Stratis.Bitcoin.Features.RPC.Models; namespace Stratis.Bitcoin.Features.RPC { @@ -52,7 +53,7 @@ wallet settxfee wallet signmessage wallet walletlock wallet walletpassphrasechange - wallet walletpassphrase yes + wallet walletpassphrase Yes */ public partial class RPCClient { @@ -136,6 +137,39 @@ public IEnumerable GetAddressesByAccount(string account) return response.Result.Select(t => this.Network.Parse((string)t)); } + public Transaction CreateRawTransaction(CreateRawTransactionInput[] inputs, List> outputs, int locktime = 0, bool replaceable = false) + { + return CreateRawTransactionAsync(inputs, outputs, locktime, replaceable).GetAwaiter().GetResult(); + } + + public async Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, List> outputs, int locktime = 0, bool replaceable = false) + { + var jOutputs = new JArray(); + + /* Need the layout of the output array to look like the following, per bitcoind documentation: + + [ + { (json object) + "address": amount, (numeric or string, required) A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in BTC + }, + { (json object) + "data": "hex", (string, required) A key-value pair. The key must be "data", the value is hex-encoded data + }, + ... + ] + */ + foreach (KeyValuePair kv in outputs) + { + var temp = new JObject(); + temp.Add(new JProperty(kv.Key, kv.Value)); + jOutputs.Add(temp); + } + + RPCResponse response = await SendCommandAsync(RPCOperations.createrawtransaction, inputs, jOutputs, locktime, replaceable).ConfigureAwait(false); + + return this.network.CreateTransaction(response.ResultString, ProtocolVersion.WITNESS_VERSION - 1); + } + public FundRawTransactionResponse FundRawTransaction(Transaction transaction, FundRawTransactionOptions options = null, bool? isWitness = null) { return FundRawTransactionAsync(transaction, options, isWitness).GetAwaiter().GetResult(); diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCClient.cs b/src/Stratis.Bitcoin.Features.RPC/RPCClient.cs index edf94b760c..5fca236564 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCClient.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCClient.cs @@ -104,7 +104,7 @@ generating setgenerate generating generate ------------------ Raw transactions - rawtransactions createrawtransaction + rawtransactions createrawtransaction Yes rawtransactions decoderawtransaction Yes rawtransactions decodescript rawtransactions getrawtransaction Yes diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs b/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs index 97be3070fd..e974e00c56 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; +using NBitcoin; using NBitcoin.DataEncoders; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -72,12 +73,12 @@ public async Task InvokeAsync(HttpContext httpContext) if (token is JArray) { // Batch request, invoke each request and accumulate responses into JArray. - response = await this.InvokeBatchAsync(httpContext, token as JArray); + response = await this.InvokeBatchAsync(httpContext, token as JArray).ConfigureAwait(false); } else if (token is JObject) { // Single request, invoke single request and return single response object. - response = await this.InvokeSingleAsync(httpContext, token as JObject); + response = await this.InvokeSingleAsync(httpContext, token as JObject).ConfigureAwait(false); if (response == null) response = JValue.CreateNull(); @@ -248,11 +249,13 @@ private async Task InvokeSingleAsync(HttpContext httpContext, JObject r responseMemoryStream.Position = 0; using (var streamReader = new StreamReader(responseMemoryStream)) - using (var textReader = new JsonTextReader(streamReader)) { - // Ensure floats are parsed as decimals and not as doubles. - textReader.FloatParseHandling = FloatParseHandling.Decimal; - response = await JObject.LoadAsync(textReader); + using (var textReader = new JsonTextReader(streamReader)) + { + // Ensure floats are parsed as decimals and not as doubles. + textReader.FloatParseHandling = FloatParseHandling.Decimal; + response = await JObject.LoadAsync(textReader).ConfigureAwait(false); + } } } catch (Exception ex) @@ -266,9 +269,9 @@ private async Task InvokeSingleAsync(HttpContext httpContext, JObject r // Ensure floats are parsed as decimals and not as doubles. textReader.FloatParseHandling = FloatParseHandling.Decimal; - string val = streamReader.ReadToEnd(); + string val = await streamReader.ReadToEndAsync().ConfigureAwait(false); context.Response.Body.Position = 0; - response = await JObject.LoadAsync(textReader); + response = await JObject.LoadAsync(textReader).ConfigureAwait(false); } } diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCOperations.cs b/src/Stratis.Bitcoin.Features.RPC/RPCOperations.cs index 5a831638ba..a6d6da0fca 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCOperations.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCOperations.cs @@ -16,6 +16,7 @@ public enum RPCOperations importpubkey, dumpwallet, importwallet, + setwallet, getgenerate, setgenerate, diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCRequest.cs b/src/Stratis.Bitcoin.Features.RPC/RPCRequest.cs index eba7970889..f7d549cbbd 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCRequest.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCRequest.cs @@ -2,22 +2,22 @@ using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Stratis.Bitcoin.Utilities.JsonConverters; namespace Stratis.Bitcoin.Features.RPC { public class RPCRequest { - public RPCRequest(RPCOperations method, object[] parameters) - : this(method.ToString(), parameters) + public RPCRequest(RPCOperations method, object[] parameters) : this(method.ToString(), parameters) { - } - public RPCRequest(string method, object[] parameters) - : this() + + public RPCRequest(string method, object[] parameters) : this() { this.Method = method; this.Params = parameters; } + public RPCRequest() { this.JsonRpc = "1.0"; @@ -33,7 +33,7 @@ public RPCRequest() public void WriteJSON(TextWriter writer) { - using (JsonTextWriter jsonWriter = new JsonTextWriter(writer)) + using (var jsonWriter = new JsonTextWriter(writer)) { jsonWriter.CloseOutput = false; WriteJSON(jsonWriter); @@ -51,34 +51,54 @@ internal void WriteJSON(JsonTextWriter writer) writer.WritePropertyName("params"); writer.WriteStartArray(); - if(this.Params != null) + if (this.Params == null) + { + writer.WriteEndArray(); + writer.WriteEndObject(); + return; + } + + for (int i = 0; i < this.Params.Length; i++) { - for(int i = 0; i < this.Params.Length; i++) + if (this.Params[i] is JToken) { - if(this.Params[i] is JToken) - { - ((JToken) this.Params[i]).WriteTo(writer); - } - else if(this.Params[i] is Array) - { - writer.WriteStartArray(); - foreach(object x in (Array) this.Params[i]) - { - writer.WriteValue(x); - } - writer.WriteEndArray(); - } - else + ((JToken) this.Params[i]).WriteTo(writer); + } + else if (this.Params[i] is Array) + { + writer.WriteStartArray(); + + foreach (object x in (Array) this.Params[i]) { - writer.WriteValue(this.Params[i]); + // Primitive types are handled well by the writer's WriteValue method, but classes need to be serialised using the same converter set as the rest of the codebase. + WriteValueOrSerializeAndWrite(writer, x); } + + writer.WriteEndArray(); + } + else + { + WriteValueOrSerializeAndWrite(writer, this.Params[i]); } } - + writer.WriteEndArray(); writer.WriteEndObject(); } + private void WriteValueOrSerializeAndWrite(JsonTextWriter writer, object valueToWrite) + { + if (valueToWrite == null || valueToWrite.GetType().IsValueType) + { + writer.WriteValue(valueToWrite); + return; + } + + // TODO: It did not appear that the RPC subsystem was automatically handling complex class parameters in requests. So we will need to start passing the network into the RPCRequest constructor to properly handle every possible type + JToken token = Serializer.ToToken(valueToWrite); + token.WriteTo(writer); + } + private void WriteProperty(JsonTextWriter writer, string property, TValue value) { writer.WritePropertyName(property); diff --git a/src/Stratis.Bitcoin.Features.RPC/Stratis.Bitcoin.Features.RPC.csproj b/src/Stratis.Bitcoin.Features.RPC/Stratis.Bitcoin.Features.RPC.csproj index 2a60137eca..15403b1866 100644 --- a/src/Stratis.Bitcoin.Features.RPC/Stratis.Bitcoin.Features.RPC.csproj +++ b/src/Stratis.Bitcoin.Features.RPC/Stratis.Bitcoin.Features.RPC.csproj @@ -3,7 +3,7 @@ Stratis Bitcoin Features RPC Stratis.Bitcoin.Features.RPC - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.RPC Stratis.Features.RPC false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.RPC/WebHostExtensions.cs b/src/Stratis.Bitcoin.Features.RPC/WebHostExtensions.cs index abd36f5fad..c5e38022dc 100644 --- a/src/Stratis.Bitcoin.Features.RPC/WebHostExtensions.cs +++ b/src/Stratis.Bitcoin.Features.RPC/WebHostExtensions.cs @@ -18,6 +18,8 @@ public static IWebHostBuilder ForFullNode(this IWebHostBuilder hostBuilder, Full o.ModelBinderProviders.Insert(0, new DestinationModelBinder()); o.ModelBinderProviders.Insert(0, new MoneyModelBinder()); o.ModelBinderProviders.Insert(0, new FundRawTransactionOptionsBinder()); + o.ModelBinderProviders.Insert(0, new CreateRawTransactionInputBinder()); + o.ModelBinderProviders.Insert(0, new CreateRawTransactionOutputBinder()); }); // Include all feature assemblies for action discovery otherwise RPC actions will not execute diff --git a/src/Stratis.Bitcoin.Features.SignalR/DaemonConfiguration.cs b/src/Stratis.Bitcoin.Features.SignalR/DaemonConfiguration.cs index e569d2647d..533814ea77 100644 --- a/src/Stratis.Bitcoin.Features.SignalR/DaemonConfiguration.cs +++ b/src/Stratis.Bitcoin.Features.SignalR/DaemonConfiguration.cs @@ -11,7 +11,14 @@ public static class DaemonConfiguration new ReconstructFederationClientEvent(), new FullNodeClientEvent(), new TransactionReceivedClientEvent(), - new WalletProcessedTransactionOfInterestClientEvent() + new WalletProcessedTransactionOfInterestClientEvent(), + new MultiSigMemberStateRequestClientEvent(), + new TransactionAddedToMemoryPoolClientEvent(), + new MiningStatisticsClientEvent(), + new ConsensusManagerStatusClientEvent(), + new PeerConnectionInfoClientEvent(), + new FederationWalletStatusClientEvent(), + new AddressIndexerStatusClientEvent(), }; private static ClientEventBroadcasterSettings Settings = new ClientEventBroadcasterSettings diff --git a/src/Stratis.Bitcoin.Features.SignalR/Events/AddressIndexerStatusClientEvent.cs b/src/Stratis.Bitcoin.Features.SignalR/Events/AddressIndexerStatusClientEvent.cs new file mode 100644 index 0000000000..a10b29bf00 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.SignalR/Events/AddressIndexerStatusClientEvent.cs @@ -0,0 +1,24 @@ +using System; +using Stratis.Bitcoin.EventBus; +using Stratis.Bitcoin.EventBus.CoreEvents; + +namespace Stratis.Bitcoin.Features.SignalR.Events +{ + public sealed class AddressIndexerStatusClientEvent : IClientEvent + { + public int Tip { get; set; } + + public Type NodeEventType { get; } = typeof(AddressIndexerStatusEvent); + + public void BuildFrom(EventBase @event) + { + if (@event is AddressIndexerStatusEvent addressIndexerStatusEvent) + { + this.Tip = addressIndexerStatusEvent.Tip; + return; + } + + throw new NotImplementedException(); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.SignalR/Events/ConsensusManagerStatusClientEvent.cs b/src/Stratis.Bitcoin.Features.SignalR/Events/ConsensusManagerStatusClientEvent.cs new file mode 100644 index 0000000000..f49a7de108 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.SignalR/Events/ConsensusManagerStatusClientEvent.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Stratis.Bitcoin.EventBus; +using Stratis.Bitcoin.EventBus.CoreEvents; + +namespace Stratis.Bitcoin.Features.SignalR.Events +{ + public class ConsensusManagerStatusClientEvent : IClientEvent + { + public bool IsIbd { get; set; } + public int? HeaderHeight { get; set; } + public Type NodeEventType { get; } = typeof(ConsensusManagerStatusEvent); + + public void BuildFrom(EventBase @event) + { + if (@event is ConsensusManagerStatusEvent consensusManagerStatusEvent) + { + this.IsIbd = consensusManagerStatusEvent.IsIbd; + this.HeaderHeight = consensusManagerStatusEvent.HeaderHeight; + return; + } + throw new NotImplementedException(); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.SignalR/Events/FederationWalletStatusClientEvent.cs b/src/Stratis.Bitcoin.Features.SignalR/Events/FederationWalletStatusClientEvent.cs new file mode 100644 index 0000000000..dfa174fdd6 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.SignalR/Events/FederationWalletStatusClientEvent.cs @@ -0,0 +1,25 @@ +using System; +using Stratis.Bitcoin.EventBus; +using Stratis.Bitcoin.EventBus.CoreEvents; + +namespace Stratis.Bitcoin.Features.SignalR.Events +{ + public sealed class FederationWalletStatusClientEvent : IClientEvent + { + public string ConfirmedBalance { get; set; } + public string UnconfirmedBalance { get; set; } + public Type NodeEventType { get; } = typeof(FederationWalletStatusEvent); + + public void BuildFrom(EventBase @event) + { + if (@event is FederationWalletStatusEvent federationWalletStatusEvent) + { + this.ConfirmedBalance = federationWalletStatusEvent.ConfirmedBalance.ToString(); + this.UnconfirmedBalance = federationWalletStatusEvent.UnconfirmedBalance.ToString(); + return; + } + + throw new NotImplementedException(); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.SignalR/Events/MiningStatisticsClientEvent.cs b/src/Stratis.Bitcoin.Features.SignalR/Events/MiningStatisticsClientEvent.cs new file mode 100644 index 0000000000..53889ef330 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.SignalR/Events/MiningStatisticsClientEvent.cs @@ -0,0 +1,27 @@ +using System; +using Stratis.Bitcoin.EventBus; +using Stratis.Bitcoin.Features.PoA.Events; + +namespace Stratis.Bitcoin.Features.SignalR.Events +{ + public class MiningStatisticsClientEvent : IClientEvent + { + public bool IsMining { get; set; } + public int BlockProducerHit { get; set; } + public int FederationMemberSize { get; set; } + public Type NodeEventType { get; } = typeof(MiningStatisticsEvent); + + public void BuildFrom(EventBase @event) + { + if (@event is MiningStatisticsEvent miningStatisticsEvent) + { + this.IsMining = miningStatisticsEvent.MiningStatistics.ProducedBlockInLastRound; + this.BlockProducerHit = miningStatisticsEvent.MiningStatistics.MinerHits; + this.FederationMemberSize = miningStatisticsEvent.FederationMemberSize; + return; + } + + throw new ArgumentException(); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.SignalR/Events/MultiSigMemberStateRequestClientEvent.cs b/src/Stratis.Bitcoin.Features.SignalR/Events/MultiSigMemberStateRequestClientEvent.cs new file mode 100644 index 0000000000..df38f6ec30 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.SignalR/Events/MultiSigMemberStateRequestClientEvent.cs @@ -0,0 +1,34 @@ +using System; +using Stratis.Bitcoin.EventBus; +using Stratis.Bitcoin.EventBus.CoreEvents; + +namespace Stratis.Bitcoin.Features.SignalR.Events +{ + public class MultiSigMemberStateRequestClientEvent : IClientEvent + { + public int CrossChainStoreHeight { get; set; } + public int CrossChainStoreNextDepositHeight { get; set; } + public int PartialTransactions { get; set; } + public int SuspendedPartialTransactions { get; set; } + + public string PubKey { get; set; } + + public Type NodeEventType { get; } = typeof(MultiSigMemberStateRequestEvent); + + public void BuildFrom(EventBase @event) + { + if (@event is MultiSigMemberStateRequestEvent multiSigState) + { + this.PubKey = multiSigState.PubKey; + + this.CrossChainStoreHeight = multiSigState.CrossChainStoreHeight; + this.CrossChainStoreNextDepositHeight = multiSigState.CrossChainStoreNextDepositHeight; + this.PartialTransactions = multiSigState.PartialTransactions; + this.SuspendedPartialTransactions = multiSigState.SuspendedPartialTransactions; + return; + } + + throw new ArgumentException(); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.SignalR/Events/PeerConnectionInfoClientEvent.cs b/src/Stratis.Bitcoin.Features.SignalR/Events/PeerConnectionInfoClientEvent.cs new file mode 100644 index 0000000000..dc71868e67 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.SignalR/Events/PeerConnectionInfoClientEvent.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Stratis.Bitcoin.Connection; +using Stratis.Bitcoin.EventBus; +using Stratis.Bitcoin.EventBus.CoreEvents; + +namespace Stratis.Bitcoin.Features.SignalR.Events +{ + public class PeerConnectionInfoClientEvent : IClientEvent + { + public IEnumerable PeerConnectionModels { get; set; } + public Type NodeEventType { get; } = typeof(PeerConnectionInfoEvent); + + public void BuildFrom(EventBase @event) + { + if (@event is PeerConnectionInfoEvent peerConnectionInfo) + { + this.PeerConnectionModels = peerConnectionInfo.PeerConnectionModels; + + return; + } + } + } +} diff --git a/src/Stratis.Bitcoin.Features.SignalR/Events/TransactionAddedToMemoryPoolClientEvent.cs b/src/Stratis.Bitcoin.Features.SignalR/Events/TransactionAddedToMemoryPoolClientEvent.cs new file mode 100644 index 0000000000..fc35af2fe0 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.SignalR/Events/TransactionAddedToMemoryPoolClientEvent.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Stratis.Bitcoin.EventBus; +using Stratis.Bitcoin.Features.MemoryPool; + +namespace Stratis.Bitcoin.Features.SignalR.Events +{ + public class TransactionAddedToMemoryPoolClientEvent : IClientEvent + { + public long MemPoolSize { get; set; } + public Type NodeEventType { get; } = typeof(TransactionAddedToMemoryPoolEvent); + public void BuildFrom(EventBase @event) + { + if (@event is TransactionAddedToMemoryPoolEvent transactionAddedToMemoryPool) + { + this.MemPoolSize = transactionAddedToMemoryPool.MemPoolSize; + return; + } + + throw new ArgumentException(); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.SignalR/Stratis.Bitcoin.Features.SignalR.csproj b/src/Stratis.Bitcoin.Features.SignalR/Stratis.Bitcoin.Features.SignalR.csproj index 1b1d641863..31520d8d71 100644 --- a/src/Stratis.Bitcoin.Features.SignalR/Stratis.Bitcoin.Features.SignalR.csproj +++ b/src/Stratis.Bitcoin.Features.SignalR/Stratis.Bitcoin.Features.SignalR.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 - 1.3.2.4 + net6.0 + 1.4.0.7 Stratis.Features.SignalR Stratis.Features.SignalR Stratis Group Ltd. @@ -14,7 +14,6 @@ - diff --git a/src/Stratis.Bitcoin.Features.SmartContracts.Tests/Stratis.Bitcoin.Features.SmartContracts.Tests.csproj b/src/Stratis.Bitcoin.Features.SmartContracts.Tests/Stratis.Bitcoin.Features.SmartContracts.Tests.csproj index 89393855db..6f2e9858bf 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts.Tests/Stratis.Bitcoin.Features.SmartContracts.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.SmartContracts.Tests/Stratis.Bitcoin.Features.SmartContracts.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.SmartContracts.Tests Stratis.Bitcoin.Features.SmartContracts.Tests true @@ -23,7 +23,7 @@ all runtime; build; native; contentfiles; analyzers - + diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/Stratis.Bitcoin.Features.SmartContracts.csproj b/src/Stratis.Bitcoin.Features.SmartContracts/Stratis.Bitcoin.Features.SmartContracts.csproj index f105520aaf..652e132ba5 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/Stratis.Bitcoin.Features.SmartContracts.csproj +++ b/src/Stratis.Bitcoin.Features.SmartContracts/Stratis.Bitcoin.Features.SmartContracts.csproj @@ -1,8 +1,8 @@  - netcoreapp3.1 - 1.3.2.4 + net6.0 + 1.4.0.7 Stratis Group Ltd. Stratis.Features.SmartContracts Stratis.Features.SmartContracts diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/ISmartContractTransactionService.cs b/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/ISmartContractTransactionService.cs index 67ee58ebc2..47db10f0a4 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/ISmartContractTransactionService.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/ISmartContractTransactionService.cs @@ -26,5 +26,16 @@ public interface ISmartContractTransactionService /// The block number where searching finishes. /// A list of all matching receipts. List ReceiptSearch(string contractAddress, string eventName, List topics = null, int fromBlock = 0, int? toBlock = null); + + /// + /// Searches for receipts that match the given filter criteria. Filter criteria are ANDed together. + /// + /// The contract addresses from which events were raised. + /// The name of the event raised. + /// The topics to search. All specified topics must be present. + /// The block number from which to start searching. + /// The block number where searching finishes. + /// A list of all matching receipts. + List ReceiptSearch(List contractAddresses, string eventName, List topics = null, int fromBlock = 0, int? toBlock = null); } -} \ No newline at end of file +} diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractTransactionService.cs b/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractTransactionService.cs index a6168264d5..ec8868c9d4 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractTransactionService.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractTransactionService.cs @@ -407,23 +407,38 @@ public ContractTxData BuildLocalCallTxData(LocalCallContractRequest request) /// public List ReceiptSearch(string contractAddress, string eventName, List topics = null, int fromBlock = 0, int? toBlock = null) { - uint160 address = contractAddress.ToUint160(this.network); + return ReceiptSearch(new List() { contractAddress }, eventName, topics, fromBlock, toBlock); + } - byte[] contractCode = this.stateRoot.GetCode(address); + /// + public List ReceiptSearch(List contractAddresses, string eventName, List topics = null, int fromBlock = 0, int? toBlock = null) + { + var filteredContractAddresses = new HashSet(); - if (contractCode == null || !contractCode.Any()) + if (contractAddresses != null) { - return null; - } + foreach (string contractAddress in contractAddresses) + { + uint160 address = contractAddress.ToUint160(this.network); - IEnumerable topicsBytes = topics != null ? topics.Where(topic => topic != null).Select(t => t.HexToByteArray()) : new List(); + byte[] contractCode = this.stateRoot.GetCode(address); + if (contractCode == null || !contractCode.Any()) + { + continue; + } + + filteredContractAddresses.Add(contractAddress); + } + } + IEnumerable topicsBytes = topics != null ? topics.Where(topic => topic != null).Select(t => t.HexToByteArray()) : new List(); + var deserializer = new ApiLogDeserializer(this.primitiveSerializer, this.network, this.stateRoot, this.contractAssemblyCache); var receiptSearcher = new ReceiptSearcher(this.chainIndexer, this.blockStore, this.receiptRepository, this.network); - List receipts = receiptSearcher.SearchReceipts(contractAddress, eventName, fromBlock, toBlock, topicsBytes); + List receipts = receiptSearcher.SearchReceipts(filteredContractAddresses, eventName, fromBlock, toBlock, topicsBytes); var result = new List(); diff --git a/src/Stratis.Bitcoin.Features.Wallet.Tests/Stratis.Bitcoin.Features.Wallet.Tests.csproj b/src/Stratis.Bitcoin.Features.Wallet.Tests/Stratis.Bitcoin.Features.Wallet.Tests.csproj index c777ed250e..8f91265c46 100644 --- a/src/Stratis.Bitcoin.Features.Wallet.Tests/Stratis.Bitcoin.Features.Wallet.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.Wallet.Tests/Stratis.Bitcoin.Features.Wallet.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.Wallet.Tests Stratis.Bitcoin.Features.Wallet.Tests true diff --git a/src/Stratis.Bitcoin.Features.Wallet.Tests/WalletTransactionHandlerTest.cs b/src/Stratis.Bitcoin.Features.Wallet.Tests/WalletTransactionHandlerTest.cs index 4afe478813..06f5209c87 100644 --- a/src/Stratis.Bitcoin.Features.Wallet.Tests/WalletTransactionHandlerTest.cs +++ b/src/Stratis.Bitcoin.Features.Wallet.Tests/WalletTransactionHandlerTest.cs @@ -355,8 +355,11 @@ public void FundTransaction_Given__a_wallet_has_enough_inputs__When__adding_inpu Assert.Contains(fundTransaction.Outputs, a => a.ScriptPubKey == destinationKeys3.PubKey.ScriptPubKey); } + /// + /// Given_AnInvalidAccountIsUsed_When_GetMaximumSpendableAmountIsCalled_Then_AnExceptionIsThrown + /// [Fact] - public void Given_AnInvalidAccountIsUsed_When_GetMaximumSpendableAmountIsCalled_Then_AnExceptionIsThrown() + public void WalletInvalidAccountTest() { DataFolder dataFolder = CreateDataFolder(this); @@ -419,7 +422,7 @@ public void WalletMaxSpendableTest3() HdAddress accountAddress2 = account.InternalAddresses.First(); accountAddress2.Transactions.Add(WalletTestsHelpers.CreateTransaction(new uint256(3), new Money(20000), 3, new SpendingDetails())); accountAddress2.Transactions.Add(WalletTestsHelpers.CreateTransaction(new uint256(4), new Money(120000), 4, new SpendingDetails())); - + (Money max, Money fee) result = walletTransactionHandler.GetMaximumSpendableAmount(new WalletAccountReference("wallet1", "account 1"), FeeType.Low, true); Assert.Equal(Money.Zero, result.max); Assert.Equal(Money.Zero, result.fee); @@ -490,7 +493,7 @@ public void WalletMaxSpendableTest1() var reserveUtxoService = new ReserveUtxoService(this.loggerFactory, new Mock().Object); var walletTransactionHandler = new WalletTransactionHandler(this.LoggerFactory.Object, walletManager, walletFeePolicy.Object, this.Network, this.standardTransactionPolicy, reserveUtxoService); - + (Wallet wallet, ExtKey extKey) = WalletTestsHelpers.GenerateBlankWalletWithExtKey("wallet1", "password", walletRepository); // Passing a null extpubkey into account creation causes problems later, so we need to obtain it first @@ -507,7 +510,7 @@ public void WalletMaxSpendableTest1() HdAddress accountAddress2 = account.InternalAddresses.Skip(1).First(); accountAddress2.Transactions.Add(WalletTestsHelpers.CreateTransaction(new uint256(3), new Money(20000), null, null, null, accountAddress2.ScriptPubKey)); accountAddress2.Transactions.Add(WalletTestsHelpers.CreateTransaction(new uint256(4), new Money(120000), null, null, null, accountAddress2.ScriptPubKey)); - + (Money max, Money fee) result = walletTransactionHandler.GetMaximumSpendableAmount(new WalletAccountReference("wallet1", "account 1"), FeeType.Low, true); Assert.Equal(new Money(165000), result.max + result.fee); } @@ -698,8 +701,11 @@ public void When_BuildTransactionIsCalled_Then_FeeIsDeductedFromAmountsInTransac Assert.True(transaction.Outputs.Count(i => i.Value.Satoshi < 5_000_000_000) == 2); // 2 outputs should have fees taken from the amount } + /// + /// When_BuildTransactionIsCalledWithoutTransactionFee_Then_FeeIsDeductedFromSingleOutputInTransaction + /// [Fact] - public void When_BuildTransactionIsCalledWithoutTransactionFee_Then_FeeIsDeductedFromSingleOutputInTransaction() + public void BuildTransactionWithoutTransactionFeeTest1() { DataFolder dataFolder = CreateDataFolder(this); @@ -766,8 +772,11 @@ public void When_BuildTransactionIsCalledWithoutTransactionFee_Then_FeeIsDeducte Assert.True(transaction.Outputs.Count(i => i.Value.Satoshi < 5_000_000_000) == 1); // 1 output should have fees taken from the amount } + /// + /// When_BuildTransactionIsCalledWithoutTransactionFee_Then_MultipleSubtractFeeRecipients_ThrowsException + /// [Fact] - public void When_BuildTransactionIsCalledWithoutTransactionFee_Then_MultipleSubtractFeeRecipients_ThrowsException() + public void BuildTransactionWithoutTransactionFeeTest2() { DataFolder dataFolder = CreateDataFolder(this); diff --git a/src/Stratis.Bitcoin.Features.Wallet/Broadcasting/BroadcasterBehavior.cs b/src/Stratis.Bitcoin.Features.Wallet/Broadcasting/BroadcasterBehavior.cs index 9f4db5dec0..882899366c 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/Broadcasting/BroadcasterBehavior.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/Broadcasting/BroadcasterBehavior.cs @@ -98,6 +98,8 @@ private void ProcessInvPayload(InvPayload invPayload) { this.broadcasterManager.AddOrUpdate(txEntry.Transaction, TransactionBroadcastState.Propagated); } + + // TODO: Do we need a NOTFOUND response here, if the mempool behaviour is already sending them? } } @@ -115,6 +117,8 @@ protected async Task ProcessGetDataPayloadAsync(INetworkPeer peer, GetDataPayloa this.broadcasterManager.AddOrUpdate(txEntry.Transaction, TransactionBroadcastState.Broadcasted); } } + + // TODO: Do we need a NOTFOUND response here, if the mempool behaviour is already sending them? } } diff --git a/src/Stratis.Bitcoin.Features.Wallet/Controllers/WalletController.cs b/src/Stratis.Bitcoin.Features.Wallet/Controllers/WalletController.cs index 4a82b28ae1..aa73721b39 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/Controllers/WalletController.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/Controllers/WalletController.cs @@ -753,7 +753,7 @@ public async Task RetrievePrivateKeyAsync([FromBody] RetrievePriv { return await this.ExecuteAsAsync(request, cancellationToken, (req, token) => - this.Json(this.walletManager.RetrievePrivateKey(request.Password, request.WalletName, request.Address))); + this.Json(this.walletManager.RetrievePrivateKey(request.WalletName, request.Address, request.Password))); } /// diff --git a/src/Stratis.Bitcoin.Features.Wallet/Interfaces/IWalletManager.cs b/src/Stratis.Bitcoin.Features.Wallet/Interfaces/IWalletManager.cs index 7013768cca..7d8602aca8 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/Interfaces/IWalletManager.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/Interfaces/IWalletManager.cs @@ -83,11 +83,11 @@ public interface IWalletManager /// /// Gets the private key associated with an address in the wallet. /// - /// The user's password. /// The name of the wallet. /// Address to extract the private key of. + /// The user's password. /// The private key associated with the given address, in WIF representation. - string RetrievePrivateKey(string password, string walletName, string address); + string RetrievePrivateKey(string walletName, string address, string password = null); /// /// Signs a string message. diff --git a/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs b/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs index 721f27ead7..fcc44801be 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs @@ -445,6 +445,7 @@ public class BuildTransactionRequest : TxFeeEstimateRequest, IValidatableObject /// /// Whether to send the change to a P2WPKH (segwit bech32) addresses, or a regular P2PKH address /// + [DefaultValue(false)] public bool SegwitChangeAddress { get; set; } /// diff --git a/src/Stratis.Bitcoin.Features.Wallet/Stratis.Bitcoin.Features.Wallet.csproj b/src/Stratis.Bitcoin.Features.Wallet/Stratis.Bitcoin.Features.Wallet.csproj index 6ad42ffa61..3db8c9b429 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/Stratis.Bitcoin.Features.Wallet.csproj +++ b/src/Stratis.Bitcoin.Features.Wallet/Stratis.Bitcoin.Features.Wallet.csproj @@ -3,7 +3,7 @@ Stratis Bitcoin Features Wallet Stratis.Bitcoin.Features.Wallet - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.Wallet Stratis.Features.Wallet false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs b/src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs index 870fae42a1..3463b1625d 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs @@ -349,9 +349,8 @@ public int GetTransactionCount(string walletName, string accountName = null) } /// - public string RetrievePrivateKey(string password, string walletName, string address) + public string RetrievePrivateKey(string walletName, string address, string password = null) { - Guard.NotEmpty(password, nameof(password)); Guard.NotEmpty(walletName, nameof(walletName)); Guard.NotEmpty(address, nameof(address)); @@ -369,8 +368,25 @@ public string RetrievePrivateKey(string password, string walletName, string addr new WalletAccountReference(walletName, a.Name), 1, int.MaxValue)).Select(a => a).FirstOrDefault(addr => addr.Address.ToString() == address); } - ISecret privateKey = wallet.GetExtendedPrivateKeyForAddress(password, hdAddress).PrivateKey.GetWif(this.network); - return privateKey.ToString(); + if (hdAddress == null) + throw new SecurityException("The address does not exist in the wallet."); + + Key walletPrivateKey; + string cacheKey = wallet.EncryptedSeed; + + if (this.privateKeyCache.TryGetValue(cacheKey, out SecureString secretValue)) + { + walletPrivateKey = wallet.Network.CreateBitcoinSecret(secretValue.FromSecureString()).PrivateKey; + } + else + { + walletPrivateKey = Key.Parse(wallet.EncryptedSeed, password, wallet.Network); + } + + ISecret addressExtendedPrivateKey = HdOperations.GetExtendedPrivateKey(walletPrivateKey, wallet.ChainCode, hdAddress.HdPath, wallet.Network); + ISecret addressPrivateKey = addressExtendedPrivateKey.PrivateKey.GetWif(this.network); + + return addressPrivateKey.ToString(); } /// diff --git a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs index 1572567a54..5b3301760f 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs @@ -12,9 +12,11 @@ using Newtonsoft.Json.Linq; using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Controllers; +using Stratis.Bitcoin.Controllers.Models; using Stratis.Bitcoin.Features.BlockStore; using Stratis.Bitcoin.Features.RPC; using Stratis.Bitcoin.Features.RPC.Exceptions; +using Stratis.Bitcoin.Features.RPC.Models; using Stratis.Bitcoin.Features.Wallet.Interfaces; using Stratis.Bitcoin.Features.Wallet.Models; using Stratis.Bitcoin.Features.Wallet.Services; @@ -22,6 +24,7 @@ using Stratis.Bitcoin.Primitives; using Stratis.Bitcoin.Utilities; using TracerAttributes; +using Script = NBitcoin.Script; namespace Stratis.Bitcoin.Features.Wallet { @@ -150,6 +153,88 @@ public async Task SendToAddressAsync(BitcoinAddress address, decimal am } } + [ActionName("dumpprivkey")] + [ActionDescription("Reveals the private key corresponding to ‘address’.")] + public object DumpPrivKey(string address) + { + try + { + return new StringModel(this.walletManager.RetrievePrivateKey(this.GetWallet(), address)); + } + catch (SecurityException) + { + throw new RPCServerException(RPCErrorCode.RPC_WALLET_UNLOCK_NEEDED, "Wallet unlock needed"); + } + catch (WalletException exception) + { + throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, exception.Message); + } + } + + [ActionName("createrawtransaction")] + [ActionDescription("Create a transaction spending the given inputs and creating new outputs.")] + public async Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, CreateRawTransactionOutput[] outputs, int locktime = 0, bool replaceable = false) + { + try + { + if (locktime != 0) + throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "Setting input locktime is currently not supported"); + + if (replaceable) + throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "Replaceable transactions are currently not supported"); + + Transaction rawTx = this.Network.CreateTransaction(); + + foreach (CreateRawTransactionInput input in inputs) + { + rawTx.AddInput(new TxIn() + { + PrevOut = new OutPoint(input.TxId, input.VOut), + Sequence = input.Sequence ?? uint.MaxValue + // Since this is a raw unsigned transaction, ScriptSig and WitScript must not be populated. + }); + } + + bool dataSeen = false; + + foreach (CreateRawTransactionOutput output in outputs) + { + if (output.Key == "data") + { + if (dataSeen) + throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "Only one data output can be specified"); + + dataSeen = true; + + byte[] data = Encoders.Hex.DecodeData(output.Value); + + rawTx.AddOutput(new TxOut + { + ScriptPubKey = TxNullDataTemplate.Instance.GenerateScriptPubKey(new[] { data }), + Value = 0 + }); + + continue; + } + + var address = BitcoinAddress.Create(output.Key, this.Network); + var amount = Money.Parse(output.Value); + + rawTx.AddOutput(new TxOut + { + ScriptPubKey = address.ScriptPubKey, + Value = amount + }); + } + + return new TransactionBriefModel(rawTx); + } + catch (WalletException exception) + { + throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, exception.Message); + } + } + [ActionName("fundrawtransaction")] [ActionDescription("Add inputs to a transaction until it has enough in value to meet its out value. Note that signing is performed separately.")] public Task FundRawTransactionAsync(string rawHex, FundRawTransactionOptions options = null, bool? isWitness = null) @@ -164,7 +249,10 @@ public Task FundRawTransactionAsync(string rawHex, F // If this was not done the transaction deserialisation would attempt to use witness deserialisation and the transaction data would get mangled. rawTx.FromBytes(Encoders.Hex.DecodeData(rawHex), this.Network.Consensus.ConsensusFactory, ProtocolVersion.WITNESS_VERSION - 1); - WalletAccountReference account = this.GetWalletAccountReference(); + // It is difficult to combine multiple accounts as a source of funds given the existing transaction building logic. + // We would need to essentially run the process multiple times and combine the results if the non-watchonly account fails to provide sufficient funds. + // With the class of user expected to use this functionality it makes more sense in the interim to use the watchonly account exclusively if told to do so. + WalletAccountReference account = (options?.IncludeWatching ?? false) ? this.GetWatchOnlyWalletAccountReference() : this.GetWalletAccountReference(); HdAddress changeAddress = null; @@ -174,10 +262,31 @@ public Task FundRawTransactionAsync(string rawHex, F if (options?.ChangeAddress != null) { - changeAddress = this.walletManager.GetAllAccounts().SelectMany(a => a.GetCombinedAddresses()).FirstOrDefault(a => a.Address == options?.ChangeAddress); + if (options?.IncludeWatching ?? false) + { + Script changeAddressScriptPubKey = BitcoinAddress.Create(options.ChangeAddress).ScriptPubKey; + bool segwit = changeAddressScriptPubKey.IsScriptType(ScriptType.Witness); + + // For the watch-only account we need to construct a dummy HdAddress, as the wallet manager may not be able to find the address otherwise. + changeAddress = new HdAddress() + { + Address = !segwit ? options.ChangeAddress : null, + ScriptPubKey = changeAddressScriptPubKey, + Bech32Address = segwit ? options.ChangeAddress : null + }; + } + else + { + changeAddress = this.walletManager.GetAllAccounts().SelectMany(a => a.GetCombinedAddresses()).FirstOrDefault(a => a.Address == options.ChangeAddress); + } } else { + if (options?.IncludeWatching ?? false) + { + throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, "A change address needs to be specified when using watch-only funds"); + } + changeAddress = this.walletManager.GetUnusedChangeAddress(account); } @@ -196,17 +305,34 @@ public Task FundRawTransactionAsync(string rawHex, F Shuffle = false, UseSegwitChangeAddress = changeAddress != null && (options?.ChangeAddress == changeAddress.Bech32Address), + // Signing is deferred until the signrawtransaction RPC is called. Sign = false }; - context.Recipients.AddRange(rawTx.Outputs - .Select(s => new Recipient + foreach (TxOut output in rawTx.Outputs) + { + // We can't add OP_RETURN outputs as recipients, they need to be added explicitly to the context's provided fields. + if (output.ScriptPubKey.IsUnspendable) { - ScriptPubKey = s.ScriptPubKey, - Amount = s.Value, - SubtractFeeFromAmount = false // TODO: Do we properly support only subtracting the fee from particular recipients? - })); + byte[][] data = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(output.ScriptPubKey); + + // This encoder uses 8-bit ASCII, so it is safe to use it on arbitrary bytes. + string opReturn = Encoders.ASCII.EncodeData(data[0]); + + context.OpReturnData = opReturn; + context.OpReturnAmount = output.Value; + + continue; + } + context.Recipients.Add(new Recipient + { + ScriptPubKey = output.ScriptPubKey, + Amount = output.Value, + SubtractFeeFromAmount = false // TODO: Do we properly support only subtracting the fee from particular recipients? + }); + } + context.AllowOtherInputs = true; foreach (TxIn transactionInput in rawTx.Inputs) @@ -772,7 +898,7 @@ public UnspentCoinModel[] ListUnspent(int minConfirmations = 1, int maxConfirmat continue; WalletAccountReference accountReference = new WalletAccountReference(walletName, account.Name); - + IEnumerable spendableTransactions = this.walletManager.GetSpendableTransactionsInAccount(accountReference, minConfirmations); foreach (var spendableTx in spendableTransactions) @@ -783,6 +909,11 @@ public UnspentCoinModel[] ListUnspent(int minConfirmations = 1, int maxConfirmat if (addresses.Any() && !addresses.Contains(BitcoinAddress.Create(spendableTx.Address.Address, this.FullNode.Network))) continue; + // Check if the transaction is already in the list. + // The node operator could be using the wallet for both "actual" and watch only accounts. + if (unspentCoins.Any(u => u.Id == spendableTx.Transaction.Id && u.Index == spendableTx.Transaction.Index)) + continue; + unspentCoins.Add(new UnspentCoinModel() { Account = accountReference.AccountName, diff --git a/src/Stratis.Bitcoin.Features.Wallet/WalletSyncManager.cs b/src/Stratis.Bitcoin.Features.Wallet/WalletSyncManager.cs index 37a357e3b8..fef4a435cb 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/WalletSyncManager.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/WalletSyncManager.cs @@ -97,12 +97,12 @@ public void Start() TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); - this.transactionAddedSubscription = this.signals.Subscribe(this.OnTransactionAdded); + this.transactionAddedSubscription = this.signals.Subscribe(this.OnTransactionAdded); this.transactionRemovedSubscription = this.signals.Subscribe(this.OnTransactionRemoved); this.blockConnectedSubscription = this.signals.Subscribe(this.OnBlockConnected); } - private void OnTransactionAdded(TransactionAddedToMemoryPool transactionAddedToMempool) + private void OnTransactionAdded(TransactionAddedToMemoryPoolEvent transactionAddedToMempool) { this.logger.LogDebug("Adding transaction '{0}' as it was added to the mempool.", transactionAddedToMempool.AddedTransaction.GetHash()); this.walletManager.ProcessTransaction(transactionAddedToMempool.AddedTransaction); diff --git a/src/Stratis.Bitcoin.Features.WatchOnlyWallet.Tests/Stratis.Bitcoin.Features.WatchOnlyWallet.Tests.csproj b/src/Stratis.Bitcoin.Features.WatchOnlyWallet.Tests/Stratis.Bitcoin.Features.WatchOnlyWallet.Tests.csproj index 35e5e3cdc4..e7595a46ab 100644 --- a/src/Stratis.Bitcoin.Features.WatchOnlyWallet.Tests/Stratis.Bitcoin.Features.WatchOnlyWallet.Tests.csproj +++ b/src/Stratis.Bitcoin.Features.WatchOnlyWallet.Tests/Stratis.Bitcoin.Features.WatchOnlyWallet.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.WatchOnlyWallet.Tests Stratis.Bitcoin.Features.WatchOnlyWallet.Tests true diff --git a/src/Stratis.Bitcoin.Features.WatchOnlyWallet/Stratis.Bitcoin.Features.WatchOnlyWallet.csproj b/src/Stratis.Bitcoin.Features.WatchOnlyWallet/Stratis.Bitcoin.Features.WatchOnlyWallet.csproj index 02dfece354..a7e2f4e2c9 100644 --- a/src/Stratis.Bitcoin.Features.WatchOnlyWallet/Stratis.Bitcoin.Features.WatchOnlyWallet.csproj +++ b/src/Stratis.Bitcoin.Features.WatchOnlyWallet/Stratis.Bitcoin.Features.WatchOnlyWallet.csproj @@ -3,7 +3,7 @@ Stratis Bitcoin Features WatchOnlyWallet Stratis.Bitcoin.Features.WatchOnlyWallet - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Features.WatchOnlyWallet Stratis.Features.WatchOnlyWallet false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.IntegrationTests.Common/EnvironmentMockUpHelpers/CoreNode.cs b/src/Stratis.Bitcoin.IntegrationTests.Common/EnvironmentMockUpHelpers/CoreNode.cs index bc49d5f25b..f6c3bdc006 100644 --- a/src/Stratis.Bitcoin.IntegrationTests.Common/EnvironmentMockUpHelpers/CoreNode.cs +++ b/src/Stratis.Bitcoin.IntegrationTests.Common/EnvironmentMockUpHelpers/CoreNode.cs @@ -524,11 +524,14 @@ public void Kill() { lock (this.lockObject) { - this.runner.Stop(); - if (!this.runner.IsDisposed) { - throw new Exception($"Problem disposing of a node of type {this.runner.GetType()}."); + this.runner.Stop(); + + if (!this.runner.IsDisposed) + { + throw new Exception($"Problem disposing of a node of type {this.runner.GetType()}."); + } } this.State = CoreNodeState.Killed; diff --git a/src/Stratis.Bitcoin.IntegrationTests.Common/Stratis.Bitcoin.IntegrationTests.Common.csproj b/src/Stratis.Bitcoin.IntegrationTests.Common/Stratis.Bitcoin.IntegrationTests.Common.csproj index 9cfc69bc8f..7f8accae71 100644 --- a/src/Stratis.Bitcoin.IntegrationTests.Common/Stratis.Bitcoin.IntegrationTests.Common.csproj +++ b/src/Stratis.Bitcoin.IntegrationTests.Common/Stratis.Bitcoin.IntegrationTests.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.IntegrationTests.Common Stratis.Features.IntegrationTests.Common @@ -13,7 +13,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False diff --git a/src/Stratis.Bitcoin.IntegrationTests/API/ApiSpecification.cs b/src/Stratis.Bitcoin.IntegrationTests/API/ApiSpecification.cs index a6d1e06db4..04570b8ea4 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/API/ApiSpecification.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/API/ApiSpecification.cs @@ -192,7 +192,7 @@ public void Proof_of_work_node_calls_getstakinginfo_and_receives_error() } [Fact] - public void Proof_of_stake_node_calls_generate_after_last_Pow_block_and_receives_error() + public void PoS_node_calls_generate_after_last_Pow_block_and_receives_error() { Given(a_proof_of_stake_node_with_api_enabled); And(the_proof_of_stake_node_has_passed_LastPOWBlock); diff --git a/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs b/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs index e68e672e0b..8ec9fc3483 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; using NBitcoin; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Stratis.Bitcoin.Connection; using Stratis.Bitcoin.Controllers.Models; @@ -29,6 +30,7 @@ using Stratis.Bitcoin.Networks; using Stratis.Bitcoin.Tests.Common; using Stratis.Bitcoin.Tests.Common.TestFramework; +using Stratis.Bitcoin.Utilities.JsonConverters; using Xunit.Abstractions; namespace Stratis.Bitcoin.IntegrationTests.API @@ -419,13 +421,13 @@ private void a_full_list_of_available_commands_is_returned() { var commands = JsonDataSerializer.Instance.Deserialize>(this.responseText); - commands.Count.Should().Be(37); + commands.Count.Should().Be(39); } private void status_information_is_returned() { var statusNode = this.firstStratisPowApiNode.FullNode; - var statusResponse = JsonDataSerializer.Instance.Deserialize(this.responseText); + var statusResponse = JsonConvert.DeserializeObject(this.responseText, new JsonSerializerSettings() { Converters = new List() { { new DateTimeToUnixTimeConverter() } } }); statusResponse.Agent.Should().Contain(statusNode.Settings.Agent); statusResponse.Version.Should().Be(statusNode.Version.ToString()); statusResponse.Network.Should().Be(statusNode.Network.Name); diff --git a/src/Stratis.Bitcoin.IntegrationTests/BlockStore/BlockStoreSignaledTests.cs b/src/Stratis.Bitcoin.IntegrationTests/BlockStore/BlockStoreSignaledTests.cs index dc243643fa..f3014dd4f0 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/BlockStore/BlockStoreSignaledTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/BlockStore/BlockStoreSignaledTests.cs @@ -79,7 +79,7 @@ public BlockStoreSignaledTests() } [Fact] - public void CheckBlocksAnnounced_AndQueueEmptiesOverTime() + public void CheckBlocksAnnounced_AndQueueEmptiesOverTimeTest1() { using (NodeBuilder builder = NodeBuilder.Create(this)) { @@ -123,8 +123,11 @@ public void CheckBlocksAnnounced_AndQueueEmptiesOverTime() } } + /// + /// CheckBlocksAnnounced_AndQueueEmptiesOverTime_ForMultiplePeers_WhenOneIsDisconnected + /// [Fact] - public void CheckBlocksAnnounced_AndQueueEmptiesOverTime_ForMultiplePeers_WhenOneIsDisconnected() + public void CheckBlocksAnnounced_AndQueueEmptiesOverTimeTest2() { using (NodeBuilder builder = NodeBuilder.Create(this)) { diff --git a/src/Stratis.Bitcoin.IntegrationTests/BlockStore/ReorgToLongestChainSpecification.cs b/src/Stratis.Bitcoin.IntegrationTests/BlockStore/ReorgToLongestChainSpecification.cs index 95453a0c78..701710d8ad 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/BlockStore/ReorgToLongestChainSpecification.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/BlockStore/ReorgToLongestChainSpecification.cs @@ -5,8 +5,11 @@ namespace Stratis.Bitcoin.IntegrationTests.BlockStore { public partial class ReorgToLongestChainSpecification : BddSpecification { + /// + /// A_cut_off_miner_advanced_ahead_of_network_causes_reorg_on_reconnect + /// [Fact] - public void A_cut_off_miner_advanced_ahead_of_network_causes_reorg_on_reconnect() + public void MinerCausesReorgOnReconnect() { Given(four_miners); And(each_mine_a_block); diff --git a/src/Stratis.Bitcoin.IntegrationTests/Connectivity/ConnectivityTests.cs b/src/Stratis.Bitcoin.IntegrationTests/Connectivity/ConnectivityTests.cs index 06573faacb..d1dbd616cd 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/Connectivity/ConnectivityTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/Connectivity/ConnectivityTests.cs @@ -96,8 +96,11 @@ public void Ensure_Peer_CanDiscover_Address_From_ConnectedPeers_And_Connect_ToTh } } + /// + /// When_Connecting_WithAddnode_Connect_ToPeer_AndAnyPeers_InTheAddressManager + /// [Fact] - public void When_Connecting_WithAddnode_Connect_ToPeer_AndAnyPeers_InTheAddressManager() + public void ConnectToPeer_AndAnyPeers_InTheAddressManager() { // TS101_Connectivity_CallAddNode. diff --git a/src/Stratis.Bitcoin.IntegrationTests/ConsensusManagerFailedReorgTests.cs b/src/Stratis.Bitcoin.IntegrationTests/ConsensusManagerFailedReorgTests.cs index 686036f962..bef3a217f5 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/ConsensusManagerFailedReorgTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/ConsensusManagerFailedReorgTests.cs @@ -224,7 +224,7 @@ void interceptorDisconnect(ChainedHeaderBlock chainedHeaderBlock) // This test is non-async to prevent locking when run in parallel with Reorg_FailsFV_Reconnect_OldChain_Nodes_DisconnectedAsync. [Fact] - public void Reorg_FailsFV_Reconnect_OldChain_From2ndMiner_DisconnectedAsync() + public void Reorg_FailsFV_Reconnect_OldChain_DisconnectedAsync() { using (var builder = NodeBuilder.Create(this)) { diff --git a/src/Stratis.Bitcoin.IntegrationTests/RPC/GetRawTransactionTest.cs b/src/Stratis.Bitcoin.IntegrationTests/RPC/GetRawTransactionTest.cs index f94889004a..422bbb60ac 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/RPC/GetRawTransactionTest.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/RPC/GetRawTransactionTest.cs @@ -277,8 +277,11 @@ public async Task GetRawTransactionWithBlockHashNonVerboseAsync() } } + /// + /// GetRawTransactionWithTransactionAndBlockHashInBlockchainAndNotIndexedAsync + /// [Fact] - public async Task GetRawTransactionWithTransactionAndBlockHashInBlockchainAndNotIndexedAsync() + public async Task GetRawTransactionAndBlockHashAsync() { using (NodeBuilder builder = NodeBuilder.Create(this)) { diff --git a/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs b/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs index 6b26b87db4..f6f0edf568 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs @@ -1,6 +1,10 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using NBitcoin; +using NBitcoin.DataEncoders; using Stratis.Bitcoin.Features.RPC; +using Stratis.Bitcoin.Features.RPC.Models; using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.IntegrationTests.Common; using Stratis.Bitcoin.IntegrationTests.Common.EnvironmentMockUpHelpers; @@ -54,6 +58,263 @@ private Money CheckFunding(CoreNode node, Transaction fundedTransaction) return totalInputs - totalOutputs; } + [Fact] + public void CanCreateRawTransaction() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): + uint256 txHash = node.GetTip().HashBlock; + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + Transaction response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = txHash, + VOut = 0 + } + }, + new List>() + { + new KeyValuePair(recipient.ToString(), amount.ToString()), + }); + + Assert.NotNull(response); + + Assert.Equal(txHash, response.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Inputs[0].PrevOut.N); + + Assert.Equal((Sequence)uint.MaxValue, response.Inputs[0].Sequence); + + Assert.Equal(recipient.ScriptPubKey, response.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Outputs[0].Value); + } + } + + [Fact] + public void CanCreateRawTransactionWithNonDefaultSequence() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): + uint256 txHash = node.GetTip().HashBlock; + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + Transaction response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = txHash, + VOut = 0, + Sequence = 5 + } + }, + new List>() + { + new KeyValuePair(recipient.ToString(), amount.ToString()), + }); + + Assert.NotNull(response); + + Assert.Equal(txHash, response.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Inputs[0].PrevOut.N); + + Assert.Equal((Sequence)5, response.Inputs[0].Sequence); + + Assert.Equal(recipient.ScriptPubKey, response.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Outputs[0].Value); + } + } + + [Fact] + public void CanCreateRawTransactionWithDataOutput() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): + uint256 txHash = node.GetTip().HashBlock; + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + Transaction response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = txHash, + VOut = 0 + } + }, + new List>() + { + new KeyValuePair(recipient.ToString(), amount.ToString()), + new KeyValuePair("data", "0011223344") + }); + + Assert.NotNull(response); + + Assert.Equal(txHash, response.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Inputs[0].PrevOut.N); + + Assert.Equal(recipient.ScriptPubKey, response.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Outputs[0].Value); + + Assert.True(response.Outputs[1].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Outputs[1].Value); + + byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Outputs[1].ScriptPubKey); + byte[] opReturn = extracted[0]; + + string opReturnHexString = Encoders.Hex.EncodeData(opReturn); + + Assert.Equal("0011223344", opReturnHexString); + } + } + + [Fact] + public void CanCreateRawTransactionWithDataOutputOnly() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): + uint256 txHash = node.GetTip().HashBlock; + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + Transaction response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = txHash, + VOut = 0 + } + }, + new List>() + { + new KeyValuePair("data", "0011223344") + }); + + Assert.NotNull(response); + + Assert.Equal(txHash, response.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Inputs[0].PrevOut.N); + + Assert.True(response.Outputs[0].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Outputs[0].Value); + + byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Outputs[0].ScriptPubKey); + byte[] opReturn = extracted[0]; + + string opReturnHexString = Encoders.Hex.EncodeData(opReturn); + + Assert.Equal("0011223344", opReturnHexString); + } + } + + [Fact] + public void CanCreateRawTransactionWithoutInputs() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + Transaction response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + }, + new List>() + { + new KeyValuePair(recipient.ToString(), amount.ToString()), + new KeyValuePair("data", "0011223344") + }); + + Assert.NotNull(response); + + Assert.Empty(response.Inputs); + + Assert.Equal(recipient.ScriptPubKey, response.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Outputs[0].Value); + + Assert.True(response.Outputs[1].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Outputs[1].Value); + } + } + + [Fact] + public void CanCreateRawTransactionWithoutOutputs() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): + uint256 txHash = node.GetTip().HashBlock; + + Transaction response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = txHash, + VOut = 0 + } + }, + new List>() + { + }); + + Assert.NotNull(response); + + Assert.Equal(txHash, response.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Inputs[0].PrevOut.N); + + Assert.Empty(response.Outputs); + } + } + + [Fact] + public void CanCreateRawTransactionWithoutInputsOrOutputs() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + Transaction response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + }, + new List>() + { + }); + + Assert.NotNull(response); + + Assert.Empty(response.Inputs); + Assert.Empty(response.Outputs); + } + } + [Fact] public void CanFundRawTransactionWithoutOptions() { @@ -151,6 +412,93 @@ public void CanFundRawTransactionWithChangePositionSpecified() } } + [Fact] + public void CanFundRawTransactionWithIncludeWatchingSpecified() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode nodeWithWallet = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start(); + + UnspentCoin[] unspent = nodeWithWallet.CreateRPCClient().ListUnspent(10, Int32.MaxValue); + + string pubKey = nodeWithWallet.FullNode.WalletManager().GetPubKey("mywallet", unspent[0].Address.ToString()); + + // Watch-only wallet node. + // Need a wallet to exist for importpubkey to work, or alternatively a default wallet needs to be configured. + var configParams = new NodeConfigParameters + { + { "-defaultwalletname", "test" }, + { "-defaultwalletpassword", "testpassword" }, + { "-unlockdefaultwallet", "1" } + }; + + CoreNode nodeWithWatchOnly = builder.CreateStratisPosNode(this.network, configParameters: configParams).Start(); + + nodeWithWatchOnly.CreateRPCClient().ImportPubKey(pubKey); + + TestHelper.ConnectAndSync(nodeWithWallet, nodeWithWatchOnly); + + var tx = this.network.CreateTransaction(); + var dest = new Key().ScriptPubKey; + tx.Outputs.Add(new TxOut(Money.Coins(1.0m), dest)); + + string changeAddress = new Key().PubKey.GetAddress(this.network).ToString(); + + var options = new FundRawTransactionOptions() + { + ChangeAddress = BitcoinAddress.Create(changeAddress, this.network).ToString(), + IncludeWatching = true + }; + + FundRawTransactionResponse funded = nodeWithWatchOnly.CreateRPCClient().FundRawTransaction(tx, options); + + Money fee = CheckFunding(nodeWithWatchOnly, funded.Transaction); + + Assert.Equal(new Money(this.network.MinRelayTxFee), fee); + Assert.True(funded.ChangePos > -1); + } + } + + [Fact] + public void CannotFundRawTransactionWithIncludeWatchingSpecifiedAndNoChangeAddress() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode nodeWithWallet = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start(); + + UnspentCoin[] unspent = nodeWithWallet.CreateRPCClient().ListUnspent(10, Int32.MaxValue); + + string pubKey = nodeWithWallet.FullNode.WalletManager().GetPubKey("mywallet", unspent[0].Address.ToString()); + + // Watch-only wallet node. + // Need a wallet to exist for importpubkey to work, or alternatively a default wallet needs to be configured. + var configParams = new NodeConfigParameters + { + { "-defaultwalletname", "test" }, + { "-defaultwalletpassword", "testpassword" }, + { "-unlockdefaultwallet", "1" } + }; + + CoreNode nodeWithWatchOnly = builder.CreateStratisPosNode(this.network, configParameters: configParams).Start(); + + nodeWithWatchOnly.CreateRPCClient().ImportPubKey(pubKey); + + TestHelper.ConnectAndSync(nodeWithWallet, nodeWithWatchOnly); + + var tx = this.network.CreateTransaction(); + var dest = new Key().ScriptPubKey; + tx.Outputs.Add(new TxOut(Money.Coins(1.0m), dest)); + + var options = new FundRawTransactionOptions() + { + ChangeAddress = null, + IncludeWatching = true + }; + + Assert.Throws(() => nodeWithWatchOnly.CreateRPCClient().FundRawTransaction(tx, options)); + } + } + [Fact] public void CanSignRawTransaction() { @@ -213,5 +561,72 @@ public void CannotSignRawTransactionWithUnownedUtxo() Assert.Throws(() => node.CreateRPCClient().SignRawTransaction(funded.Transaction)); } } + + [Fact] + public void CanCreateFundAndSignRawTransaction() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start(); + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + Transaction response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + }, + new List>() + { + new KeyValuePair(recipient.ToString(), amount.ToString()), + new KeyValuePair("data", "0011223344") + }); + + Assert.NotNull(response); + + Assert.Empty(response.Inputs); + + Assert.Equal(recipient.ScriptPubKey, response.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Outputs[0].Value); + + Assert.True(response.Outputs[1].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Outputs[1].Value); + + byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Outputs[1].ScriptPubKey); + byte[] opReturn = extracted[0]; + + string opReturnHexString = Encoders.Hex.EncodeData(opReturn); + + Assert.Equal("0011223344", opReturnHexString); + + FundRawTransactionResponse funded = node.CreateRPCClient().FundRawTransaction(response); + + Money fee = CheckFunding(node, funded.Transaction); + + Assert.Equal(new Money(this.network.MinRelayTxFee), fee); + Assert.True(funded.ChangePos > -1); + + node.CreateRPCClient().WalletPassphrase("password", 600); + + Transaction signed = node.CreateRPCClient().SignRawTransaction(funded.Transaction); + + Assert.NotNull(signed); + Assert.NotEmpty(signed.Inputs); + + foreach (var input in signed.Inputs) + { + Assert.NotNull(input.ScriptSig); + + // Basic sanity check that the transaction has actually been signed. + // A segwit transaction would fail this check but we aren't checking that here. + // In any case, the mempool count test shows definitively if the transaction passes validation. + Assert.NotEqual(input.ScriptSig, Script.Empty); + } + + node.CreateRPCClient().SendRawTransaction(signed); + + TestBase.WaitLoop(() => node.CreateRPCClient().GetRawMempool().Length == 1); + } + } } } diff --git a/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs b/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs index 4f19c12bd1..84bb33d83a 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs @@ -7,12 +7,10 @@ using NBitcoin; using Newtonsoft.Json.Linq; using Stratis.Bitcoin.Features.RPC; +using Stratis.Bitcoin.Features.RPC.Models; using Stratis.Bitcoin.IntegrationTests.Common; using Stratis.Bitcoin.IntegrationTests.Common.EnvironmentMockUpHelpers; -using Stratis.Bitcoin.Networks; -using Stratis.Bitcoin.Networks.Deployments; using Stratis.Bitcoin.Tests.Common; -using Stratis.Bitcoin.Utilities.Extensions; using Xunit; namespace Stratis.Bitcoin.IntegrationTests.RPC @@ -160,6 +158,118 @@ public void CanGetGenesisFromRPC() } } + [Fact] + public void CanCreateRawTransactionWithInput() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateBitcoinCoreNode(version: "0.18.0", useNewConfigStyle: true).Start(); + + CoreNode sfn = builder.CreateStratisPowNode(this.regTest).WithWallet().Start(); + + TestHelper.ConnectAndSync(node, sfn); + + RPCClient rpcClient = node.CreateRPCClient(); + RPCClient sfnRpc = sfn.CreateRPCClient(); + + // Need one block per node so they can each fund a transaction. + rpcClient.Generate(1); + + TestHelper.ConnectAndSync(node, sfn); + + sfnRpc.Generate(1); + + TestHelper.ConnectAndSync(node, sfn); + + // And then enough blocks mined on top for the coinbases to mature. + rpcClient.Generate(101); + + TestHelper.ConnectAndSync(node, sfn); + + Key dest = new Key(); + + var tx = rpcClient.CreateRawTransaction(new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = uint256.One, + VOut = 2 + } + }, + new List>() + { + new KeyValuePair(dest.PubKey.GetAddress(this.regTest).ToString(), "1") + }); + + Assert.NotNull(tx); + + var tx2 = sfnRpc.CreateRawTransaction(new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = uint256.One, + VOut = 2 + } + }, + new List>() + { + new KeyValuePair(dest.PubKey.GetAddress(this.regTest).ToString(), "1") + }); + + Assert.NotNull(tx2); + + // TODO: Need to verify our adherence to BIP68 (https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki#specification). But in the meantime the raw transaction we produce is identical to bitcoind except for the version field. + tx2.Version = 2; + + Assert.True(tx.GetHash() == tx2.GetHash()); + } + } + + [Fact] + public void CanCreateRawTransactionWithoutInput() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateBitcoinCoreNode(version: "0.18.0", useNewConfigStyle: true).Start(); + + CoreNode sfn = builder.CreateStratisPowNode(this.regTest).WithWallet().Start(); + + TestHelper.ConnectAndSync(node, sfn); + + RPCClient rpcClient = node.CreateRPCClient(); + RPCClient sfnRpc = sfn.CreateRPCClient(); + + TestHelper.ConnectAndSync(node, sfn); + + Key dest = new Key(); + + var tx = rpcClient.CreateRawTransaction(new CreateRawTransactionInput[] + { + }, + new List>() + { + new KeyValuePair(dest.PubKey.GetAddress(this.regTest).ToString(), "1") + }); + + Assert.NotNull(tx); + + var tx2 = sfnRpc.CreateRawTransaction(new CreateRawTransactionInput[] + { + }, + new List>() + { + new KeyValuePair(dest.PubKey.GetAddress(this.regTest).ToString(), "1") + }); + + Assert.NotNull(tx2); + + // TODO: Need to verify our adherence to BIP68 (https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki#specification). But in the meantime the raw transaction we produce is identical to bitcoind except for the version field. + tx2.Version = 2; + + Assert.True(tx.GetHash() == tx2.GetHash()); + } + } + [Fact] public void CanSignRawTransaction() { @@ -188,8 +298,11 @@ public void CanSignRawTransaction() TestHelper.ConnectAndSync(node, sfn); - var tx = new Transaction(); - tx.Outputs.Add(new TxOut(Money.Coins(1.0m), new Key())); + var tx = rpcClient.CreateRawTransaction(new CreateRawTransactionInput[] {}, new List>() + { + new KeyValuePair((new Key()).PubKey.GetAddress(this.regTest).ToString(), "1") + }); + FundRawTransactionResponse funded = rpcClient.FundRawTransaction(tx); // signrawtransaction was removed in 0.18. So just use its equivalent so that we can test SFN's ability to call signrawtransaction. diff --git a/src/Stratis.Bitcoin.IntegrationTests/Stratis.Bitcoin.IntegrationTests.csproj b/src/Stratis.Bitcoin.IntegrationTests/Stratis.Bitcoin.IntegrationTests.csproj index eae5f02496..c5e6f0f1ae 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/Stratis.Bitcoin.IntegrationTests.csproj +++ b/src/Stratis.Bitcoin.IntegrationTests/Stratis.Bitcoin.IntegrationTests.csproj @@ -5,7 +5,7 @@ - netcoreapp3.1 + net6.0 Stratis.Bitcoin.IntegrationTests Stratis.Bitcoin.IntegrationTests true diff --git a/src/Stratis.Bitcoin.IntegrationTests/Wallet/WalletRPCControllerTest.cs b/src/Stratis.Bitcoin.IntegrationTests/Wallet/WalletRPCControllerTest.cs index 028c46f541..d2e276997b 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/Wallet/WalletRPCControllerTest.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/Wallet/WalletRPCControllerTest.cs @@ -29,6 +29,67 @@ public WalletRPCControllerTest() this.network = new StraxRegTest(); } + [Fact] + public async Task DumpPrivKeyForExistingAddressAsync() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithWallet().Start(); + + RPCClient rpc = node.CreateRPCClient(); + + BitcoinAddress address = await rpc.GetNewAddressAsync(); + + string apiPrivateKeyWif = await $"http://localhost:{node.ApiPort}/api" + .AppendPathSegment("Wallet/privatekey") + .PostJsonAsync(new RetrievePrivateKeyModel() { Address = address.ToString(), Password = node.WalletPassword, WalletName = node.WalletName }) + .ReceiveJson(); + + await rpc.WalletPassphraseAsync(node.WalletPassword, 3600); + + BitcoinSecret rpcPrivateKey = await rpc.DumpPrivKeyAsync(address); + + string rpcPrivateKeyWif = rpcPrivateKey.ToWif(); + + Assert.Equal(apiPrivateKeyWif, rpcPrivateKeyWif); + } + } + + [Fact] + public async Task DumpPrivKeyResultGivesCorrectPubkey() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithWallet().Start(); + + RPCClient rpc = node.CreateRPCClient(); + + BitcoinAddress address = await rpc.GetNewAddressAsync(); + + string apiPubkey = await $"http://localhost:{node.ApiPort}/api" + .AppendPathSegment("Wallet/pubkey") + .PostJsonAsync(new PubKeyRequest() { ExternalAddress = address.ToString(), WalletName = node.WalletName }) + .ReceiveJson(); + + string apiPrivateKeyWif = await $"http://localhost:{node.ApiPort}/api" + .AppendPathSegment("Wallet/privatekey") + .PostJsonAsync(new RetrievePrivateKeyModel() { Address = address.ToString(), Password = node.WalletPassword, WalletName = node.WalletName }) + .ReceiveJson(); + + string apiPubkeyFromWif = this.network.Parse(apiPrivateKeyWif).PubKey.ToHex(); + + Assert.Equal(apiPubkey, apiPubkeyFromWif); + + await rpc.WalletPassphraseAsync(node.WalletPassword, 3600); + + BitcoinSecret rpcPrivateKey = await rpc.DumpPrivKeyAsync(address); + + string rpcPubkeyHex = rpcPrivateKey.PubKey.ToHex(); + + Assert.Equal(apiPubkey, rpcPubkeyHex); + } + } + [Fact] public void GetTransactionDoesntExistInWalletOrBlock() { diff --git a/src/Stratis.Bitcoin.Networks/Stratis.Bitcoin.Networks.csproj b/src/Stratis.Bitcoin.Networks/Stratis.Bitcoin.Networks.csproj index 575e2a69b0..a8b0be4850 100644 --- a/src/Stratis.Bitcoin.Networks/Stratis.Bitcoin.Networks.csproj +++ b/src/Stratis.Bitcoin.Networks/Stratis.Bitcoin.Networks.csproj @@ -3,7 +3,7 @@ Stratis Full Node Networks Stratis.Bitcoin.Networks - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Networks Stratis.Core.Networks false @@ -14,9 +14,9 @@ false false false - 1.3.2.4 - 1.3.2.4 - 1.3.2.4 + 1.4.0.7 + 1.4.0.7 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Tests.Common/MockingContext.cs b/src/Stratis.Bitcoin.Tests.Common/MockingContext.cs index 087aa27be4..90105a1227 100644 --- a/src/Stratis.Bitcoin.Tests.Common/MockingContext.cs +++ b/src/Stratis.Bitcoin.Tests.Common/MockingContext.cs @@ -16,9 +16,9 @@ public static IServiceCollection AddSingleton(this IServiceCollection service return serviceCollection; } } - + /// - /// Implements a GetService that concretizes services on-demand and also mocks services that can't be otherwise concretized. + /// Implements a GetService that concretizes services on-demand and also mocks services that can't otherwise be concretized. /// public class MockingContext : IServiceProvider { @@ -35,7 +35,7 @@ public MockingContext(IServiceCollection serviceCollection) /// /// The service type. /// The service instance. - /// A mocked type can be passed in which case the mock object is returned for setup purposes. + /// A mocked type can be passed in which case the mock base class is returned for setup purposes. /// An enumerable type can be passed in which case multiple service instances are returned. public object GetService(Type serviceType) { @@ -109,23 +109,24 @@ private object MakeConcrete(Type serviceType) if (serviceType.IsInterface) { + // Mock mock = Activator.CreateInstance(mockType); } else { + // Mock + // If mocking a class (instead of an interface) then the constructor arguments need to be provided as well. ConstructorInfo constructorInfo = GetConstructor(serviceType); object[] args = GetConstructorArguments(this, constructorInfo); + mock = Activator.CreateInstance(mockType, args); + + // Enables use of existing class methods by default. + mock.SetPrivatePropertyValue("CallBase", true); } this.serviceCollection.AddSingleton(mockType, mock); - // If we're mocking an interface then there is no separate singleton for the internal object. - if (isMock && serviceType.IsInterface) - return mock; - - mock.SetPrivatePropertyValue("CallBase", true); - service = ((dynamic)mock).Object; } else diff --git a/src/Stratis.Bitcoin.Tests.Common/Stratis.Bitcoin.Tests.Common.csproj b/src/Stratis.Bitcoin.Tests.Common/Stratis.Bitcoin.Tests.Common.csproj index 949cd5ab7e..696fd1928c 100644 --- a/src/Stratis.Bitcoin.Tests.Common/Stratis.Bitcoin.Tests.Common.csproj +++ b/src/Stratis.Bitcoin.Tests.Common/Stratis.Bitcoin.Tests.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Tests.Common Stratis.Core.Tests.Common @@ -13,7 +13,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False diff --git a/src/Stratis.Bitcoin.Tests.Wallet.Common/Stratis.Bitcoin.Tests.Wallet.Common.csproj b/src/Stratis.Bitcoin.Tests.Wallet.Common/Stratis.Bitcoin.Tests.Wallet.Common.csproj index 4132c6465f..bf2a4acf3d 100644 --- a/src/Stratis.Bitcoin.Tests.Wallet.Common/Stratis.Bitcoin.Tests.Wallet.Common.csproj +++ b/src/Stratis.Bitcoin.Tests.Wallet.Common/Stratis.Bitcoin.Tests.Wallet.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Tests.Wallet.Common Stratis.Bitcoin.Tests.Wallet.Common diff --git a/src/Stratis.Bitcoin.Tests/Consensus/ConsensusTestContext.cs b/src/Stratis.Bitcoin.Tests/Consensus/ConsensusTestContext.cs index 4eaf2a27e2..176544ad71 100644 --- a/src/Stratis.Bitcoin.Tests/Consensus/ConsensusTestContext.cs +++ b/src/Stratis.Bitcoin.Tests/Consensus/ConsensusTestContext.cs @@ -139,7 +139,7 @@ public TestContext() this.connectionManager = new ConnectionManager(this.dateTimeProvider, this.loggerFactory, this.Network, this.networkPeerFactory, this.nodeSettings, this.nodeLifetime, new NetworkPeerConnectionParameters(), this.peerAddressManager, new IPeerConnector[] { }, - peerDiscovery, this.selfEndpointTracker, connectionSettings, new VersionProvider(), this.nodeStats, this.asyncProvider, new PayloadProvider()); + peerDiscovery, this.selfEndpointTracker, connectionSettings, new VersionProvider(), this.nodeStats, this.asyncProvider, new PayloadProvider(), this.signals); this.deployments = new NodeDeployments(this.Network, this.chainIndexer); diff --git a/src/Stratis.Bitcoin.Tests/P2P/PeerConnectorTests.cs b/src/Stratis.Bitcoin.Tests/P2P/PeerConnectorTests.cs index eb06349d8d..d8a224ae18 100644 --- a/src/Stratis.Bitcoin.Tests/P2P/PeerConnectorTests.cs +++ b/src/Stratis.Bitcoin.Tests/P2P/PeerConnectorTests.cs @@ -552,7 +552,7 @@ private IConnectionManager CreateConnectionManager( new VersionProvider(), new Mock().Object, this.asyncProvider, - new Bitcoin.P2P.Protocol.Payloads.PayloadProvider()); + new Bitcoin.P2P.Protocol.Payloads.PayloadProvider(), this.signals); networkPeerParameters.TemplateBehaviors.Add(new ConnectionManagerBehavior(connectionManager, this.extendedLoggerFactory)); diff --git a/src/Stratis.Bitcoin.Tests/Stratis.Bitcoin.Tests.csproj b/src/Stratis.Bitcoin.Tests/Stratis.Bitcoin.Tests.csproj index 48313169e1..c3421e65e5 100644 --- a/src/Stratis.Bitcoin.Tests/Stratis.Bitcoin.Tests.csproj +++ b/src/Stratis.Bitcoin.Tests/Stratis.Bitcoin.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Bitcoin.Tests Stratis.Bitcoin.Tests true diff --git a/src/Stratis.Bitcoin/Builder/FullNodeFeatureExecutor.cs b/src/Stratis.Bitcoin/Builder/FullNodeFeatureExecutor.cs index 84b6c49873..fb9cf1a8c6 100644 --- a/src/Stratis.Bitcoin/Builder/FullNodeFeatureExecutor.cs +++ b/src/Stratis.Bitcoin/Builder/FullNodeFeatureExecutor.cs @@ -161,13 +161,14 @@ private void Execute(Action callback, bool disposing = false) private void LogAndAddException(IFullNodeFeature feature, bool disposing, List exceptions, Exception exception) { - exceptions.Add(exception); - var messageText = disposing ? "disposing" : "starting"; - var exceptionText = "An error occurred {0} full node feature '{1}' : '{2}'"; + var exceptionText = "An error occurred {0} full node feature '{1}'."; + var message = string.Format(exceptionText, messageText, feature.GetType().Name); + + exceptions.Add(new Exception(message, exception)); this.logger.LogError(exceptionText, messageText, feature.GetType().Name, exception); - this.signals.Publish(new FullNodeEvent() { Message = string.Format(exceptionText, messageText, feature.GetType().Name, exception.Message), State = FullNodeState.Failed.ToString() }); + this.signals.Publish(new FullNodeEvent() { Message = message, State = FullNodeState.Failed.ToString() }); } } } diff --git a/src/Stratis.Bitcoin/Configuration/Logging/LoggingConfiguration.cs b/src/Stratis.Bitcoin/Configuration/Logging/LoggingConfiguration.cs index a513db4bd0..98228cb16d 100644 --- a/src/Stratis.Bitcoin/Configuration/Logging/LoggingConfiguration.cs +++ b/src/Stratis.Bitcoin/Configuration/Logging/LoggingConfiguration.cs @@ -172,6 +172,9 @@ private static void AddFilters(LogSettings settings = null, DataFolder dataFolde AutoFlush = true, }; + // Ensure non-ascii characters don't get omitted from the console output (e.g. if this defaults to 'OSEncoding'). + consoleTarget.Encoding = Encoding.UTF8; + consoleTarget.RowHighlightingRules.Add(new ConsoleRowHighlightingRule("level == LogLevel.Info", ConsoleOutputColor.Gray, ConsoleOutputColor.Black)); consoleTarget.RowHighlightingRules.Add(new ConsoleRowHighlightingRule("level == LogLevel.Warn", ConsoleOutputColor.Gray, ConsoleOutputColor.Black)); consoleTarget.RowHighlightingRules.Add(new ConsoleRowHighlightingRule("level == LogLevel.Error", ConsoleOutputColor.Gray, ConsoleOutputColor.Black)); diff --git a/src/Stratis.Bitcoin/Connection/ConnectionManager.cs b/src/Stratis.Bitcoin/Connection/ConnectionManager.cs index aedb4c82bc..461a088aff 100644 --- a/src/Stratis.Bitcoin/Connection/ConnectionManager.cs +++ b/src/Stratis.Bitcoin/Connection/ConnectionManager.cs @@ -12,10 +12,12 @@ using Stratis.Bitcoin.Configuration.Logging; using Stratis.Bitcoin.Configuration.Settings; using Stratis.Bitcoin.Consensus; +using Stratis.Bitcoin.EventBus.CoreEvents; using Stratis.Bitcoin.Interfaces; using Stratis.Bitcoin.P2P; using Stratis.Bitcoin.P2P.Peer; using Stratis.Bitcoin.P2P.Protocol.Payloads; +using Stratis.Bitcoin.Signals; using Stratis.Bitcoin.Utilities; using Stratis.Bitcoin.Utilities.Extensions; @@ -89,6 +91,8 @@ public IReadOnlyNetworkPeerCollection ConnectedPeers private readonly PayloadProvider payloadProvider; + private readonly ISignals signals; + public ConnectionManager(IDateTimeProvider dateTimeProvider, ILoggerFactory loggerFactory, Network network, @@ -104,7 +108,8 @@ public ConnectionManager(IDateTimeProvider dateTimeProvider, IVersionProvider versionProvider, INodeStats nodeStats, IAsyncProvider asyncProvider, - PayloadProvider payloadProvider) + PayloadProvider payloadProvider, + ISignals signals) { this.connectedPeers = new NetworkPeerCollection(); this.dateTimeProvider = dateTimeProvider; @@ -132,6 +137,7 @@ public ConnectionManager(IDateTimeProvider dateTimeProvider, this.Parameters.UserAgent = $"{this.ConnectionSettings.Agent}:{versionProvider.GetVersion()} ({(int)this.NodeSettings.ProtocolVersion})"; this.Parameters.Version = this.NodeSettings.ProtocolVersion; + this.signals = signals; nodeStats.RegisterStats(this.AddComponentStats, StatsType.Component, this.GetType().Name, 1100); } @@ -279,6 +285,8 @@ void AddPeerInfo(StringBuilder peerBuilder, INetworkPeer peer) var addNodeDict = this.ConnectionSettings.RetrieveAddNodes().ToDictionary(ep => ep.MapToIpv6(), ep => ep); var connectDict = this.ConnectionSettings.Connect.ToDictionary(ep => ep.MapToIpv6(), ep => ep); + var peerList = new List(); + foreach (INetworkPeer peer in this.ConnectedPeers) { bool added = false; @@ -312,6 +320,22 @@ void AddPeerInfo(StringBuilder peerBuilder, INetworkPeer peer) { AddPeerInfo(otherBuilder, peer); } + + // peer connection info to send in signalr message + ConsensusManagerBehavior chainHeadersBehavior = peer.Behavior(); + var peerNode = new PeerConnectionModel + { + SubVersion = peer.PeerVersion.UserAgent, + Address = peer.RemoteSocketEndpoint.ToString(), + Height = chainHeadersBehavior.BestReceivedTip != null ? chainHeadersBehavior.BestReceivedTip.Height : peer.PeerVersion?.StartHeight ?? -1, + Inbound = peer.Inbound + }; + peerList.Add(peerNode); + } + + if (this.signals != null) + { + this.signals.Publish(new PeerConnectionInfoEvent(peerList)); } int inbound = this.ConnectedPeers.Count(x => x.Inbound); diff --git a/src/Stratis.Bitcoin/Connection/PeerConnectionModel.cs b/src/Stratis.Bitcoin/Connection/PeerConnectionModel.cs new file mode 100644 index 0000000000..ee7c1c8170 --- /dev/null +++ b/src/Stratis.Bitcoin/Connection/PeerConnectionModel.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; + +namespace Stratis.Bitcoin.Connection +{ + public class PeerConnectionModel + { + /// + /// The IP address and port of the peer. + /// + [JsonProperty(PropertyName = "address")] + public string Address { get; internal set; } + + /// + /// The user agent this node sends in its version message. + /// + [JsonProperty(PropertyName = "subversion")] + public string SubVersion { get; internal set; } + + /// + /// Whether node is inbound or outbound connection. + /// + [JsonProperty(PropertyName = "inbound")] + public bool Inbound { get; internal set; } + + [JsonProperty(PropertyName = "height")] + public int Height { get; internal set; } + } +} diff --git a/src/Stratis.Bitcoin/Consensus/ConsensusManager.cs b/src/Stratis.Bitcoin/Consensus/ConsensusManager.cs index a0dc11b24d..30d372236d 100644 --- a/src/Stratis.Bitcoin/Consensus/ConsensusManager.cs +++ b/src/Stratis.Bitcoin/Consensus/ConsensusManager.cs @@ -1508,6 +1508,11 @@ private void AddComponentStats(StringBuilder log) log.AppendLine("Tip Age".PadRight(LoggingConfiguration.ColumnLength, ' ') + $": { TimeSpan.FromSeconds(tipAge):dd\\.hh\\:mm\\:ss} (maximum is { TimeSpan.FromSeconds(maxTipAge):dd\\.hh\\:mm\\:ss})"); log.AppendLine("Synced with Network".PadRight(LoggingConfiguration.ColumnLength, ' ') + $": { (this.isIbd ? "No" : "Yes") }"); + if (this.signals != null) + { + this.signals.Publish(new ConsensusManagerStatusEvent(this.isIbd, this.HeaderTip)); + } + string unconsumedBlocks = this.FormatBigNumber(this.chainedHeaderTree.UnconsumedBlocksCount); double filledPercentage = Math.Round((this.chainedHeaderTree.UnconsumedBlocksDataBytes / (double)this.maxUnconsumedBlocksDataBytes) * 100, 2); diff --git a/src/Stratis.Bitcoin/Controllers/Models/StatusModel.cs b/src/Stratis.Bitcoin/Controllers/Models/StatusModel.cs index 170b066f67..5cfb181354 100644 --- a/src/Stratis.Bitcoin/Controllers/Models/StatusModel.cs +++ b/src/Stratis.Bitcoin/Controllers/Models/StatusModel.cs @@ -84,6 +84,9 @@ public StatusModel() /// Returns whether or not the node is in Initial Block Download (syncing). public bool? InIbd { get; set; } + + /// Returns the Node Start Datetime. + public DateTime NodeStarted { get; set; } } /// diff --git a/src/Stratis.Bitcoin/Controllers/Models/StringModel.cs b/src/Stratis.Bitcoin/Controllers/Models/StringModel.cs new file mode 100644 index 0000000000..03effbc34a --- /dev/null +++ b/src/Stratis.Bitcoin/Controllers/Models/StringModel.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using Stratis.Bitcoin.Controllers.Converters; + +namespace Stratis.Bitcoin.Controllers.Models +{ + /// + /// Temporary workaround; the RPC middleware does not correctly return JSON for a raw string object, so we have to wrap a string response with this model. + /// See also how 'getblockheader' returns a hex string. + /// + [JsonConverter(typeof(ToStringJsonConverter))] + public class StringModel + { + public string Str { get; set; } + + public StringModel(string str) + { + this.Str = str; + } + + public override string ToString() + { + return this.Str; + } + } +} diff --git a/src/Stratis.Bitcoin/Controllers/RestApiClientBase.cs b/src/Stratis.Bitcoin/Controllers/RestApiClientBase.cs index 6e8bff62cb..3e09d8f52b 100644 --- a/src/Stratis.Bitcoin/Controllers/RestApiClientBase.cs +++ b/src/Stratis.Bitcoin/Controllers/RestApiClientBase.cs @@ -111,13 +111,6 @@ protected async Task SendPostRequestAsync(Model reque { HttpResponseMessage response = await this.SendPostRequestAsync(requestModel, apiMethodName, cancellation).ConfigureAwait(false); - if (response != null && !response.IsSuccessStatusCode && response.Content != null) - { - string errorJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var errorResponse = JsonConvert.DeserializeObject(errorJson); - throw new Exception(errorResponse.Errors[0].Message); - } - return await this.ParseHttpResponseMessageAsync(response).ConfigureAwait(false); } diff --git a/src/Stratis.Bitcoin/Database/IDb.cs b/src/Stratis.Bitcoin/Database/IDb.cs new file mode 100644 index 0000000000..807271a29e --- /dev/null +++ b/src/Stratis.Bitcoin/Database/IDb.cs @@ -0,0 +1,126 @@ +using System; + +namespace Stratis.Bitcoin.Database +{ + /// + /// This interface and its relevant implementations provide a standardized interface to databases such as and , or other databases + /// capable of supporting key-based value retrieval and key iteration. + /// + /// + /// The interface expects keys to be specified as separate table and key identifiers. Similarly iterators are expected to be constrained to operate within single tables. + /// + public interface IDb : IDisposable + { + /// + /// Opens the database at the specified path. + /// + /// The path where the database is located. + void Open(string dbPath); + + /// + /// Gets the value associated with a table and key. + /// + /// The table identifier. + /// The key of the value to retrieve. + /// The value for the specified table and key. + byte[] Get(byte table, byte[] key); + + /// + /// Gets an iterator that allows iteration over keys in a table. + /// + /// The table that will be iterated. + /// See . + IDbIterator GetIterator(byte table); + + /// + /// Gets a batch that can be used to record changes that can be applied atomically. + /// + /// The method will not reflect these changes until they are committed. Use + /// the class if uncommitted changes need to be accessed. + /// See . + IDbBatch GetWriteBatch(); + + /// + /// Removes all tables and their contents. + /// + void Clear(); + } + + /// + /// A batch that can be used to record changes that can be applied atomically. + /// + /// The database's method will not reflect these changes until they are committed. + public interface IDbBatch : IDisposable + { + /// + /// Records a value that will be written to the database when the method is invoked. + /// + /// The table that will be updated. + /// The table key that identifies the value to be updated. + /// The value to be written to the table. + /// This class for fluent operations. + IDbBatch Put(byte table, byte[] key, byte[] value); + + /// + /// Records a key that will be deleted from the database when the method is invoked. + /// + /// The table that will be updated. + /// The table key that will be removed. + /// This class for fluent operations. + IDbBatch Delete(byte table, byte[] key); + + /// + /// Writes the recorded changes to the database. + /// + void Write(); + } + + /// + /// An iterator that can be used to iterate the keys and values in an compliant database. + /// + public interface IDbIterator : IDisposable + { + /// + /// Seeks to a first key >= in the relevant table. + /// If no such key is found then will return false. + /// + /// The key to find. + void Seek(byte[] key); + + /// + /// Seeks to the last key in the relevant table. + /// If no such key is found then will return false. + /// + void SeekToLast(); + + /// + /// Seeks to the next key in the relevant table. + /// If no such key is found then will return false. + /// + void Next(); + + /// + /// Seeks to the previous key in the relevant table. + /// If no such key is found then will return false. + /// + void Prev(); + + /// + /// Determines if the current key is valid. + /// + /// true if a , , or operation found a valid key. false otherwise. + bool IsValid(); + + /// + /// The current key. + /// + /// The key. + byte[] Key(); + + /// + /// The current value. + /// + /// The value. + byte[] Value(); + } +} \ No newline at end of file diff --git a/src/Stratis.Bitcoin/Database/IDbIteratorExt.cs b/src/Stratis.Bitcoin/Database/IDbIteratorExt.cs new file mode 100644 index 0000000000..35c4796649 --- /dev/null +++ b/src/Stratis.Bitcoin/Database/IDbIteratorExt.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using NBitcoin; + +namespace Stratis.Bitcoin.Database +{ + /// + /// Extension methods that build on the interface. + /// + public static class IDbIteratorExt + { + private static ByteArrayComparer byteArrayComparer = new ByteArrayComparer(); + + /// + /// Gets all the keys in the relevant table subject to any supplied constraints. + /// + /// The iterator that also identifies the table being iterated. + /// Defaults to false. Set to true if values should be ommitted - i.e. set to null. + /// Defaults to true. Set to false to return keys in ascending order. + /// Can be set optionally to specify the lower bound of keys to return. + /// Can be set optionally to specify the upper bound of keys to return. + /// Defaults to true. Set to false to omit the key specified in . + /// Defaults to true. Set to false to omit the key specified in . + /// An enumeration containing all the keys and values according to the specified constraints. + public static IEnumerable<(byte[], byte[])> GetAll(this IDbIterator iterator, bool keysOnly = false, bool ascending = true, + byte[] firstKey = null, byte[] lastKey = null, bool includeFirstKey = true, bool includeLastKey = true) + { + bool done = false; + Func breakLoop; + Action next; + + if (!ascending) + { + // Seek to the last key if it was provided. + if (lastKey == null) + iterator.SeekToLast(); + else + { + iterator.Seek(lastKey); + if (iterator.IsValid()) + { + if (!(includeLastKey && byteArrayComparer.Equals(iterator.Key(), lastKey))) + iterator.Prev(); + } + else + iterator.SeekToLast(); + } + + breakLoop = (firstKey == null) ? (Func)null : (keyBytes) => + { + int compareResult = byteArrayComparer.Compare(keyBytes, firstKey); + if (compareResult <= 0) + { + // If this is the first key and its not included or we've overshot the range then stop without yielding a value. + if (!includeFirstKey || compareResult < 0) + return true; + + // Stop after yielding the value. + done = true; + } + + // Keep going. + return false; + }; + + next = () => iterator.Prev(); + } + else /* Ascending */ + { + // Seek to the first key if it was provided. + if (firstKey == null) + iterator.Seek(new byte[0]); + else + { + iterator.Seek(firstKey); + if (iterator.IsValid()) + { + if (!(includeFirstKey && byteArrayComparer.Equals(iterator.Key(), firstKey))) + iterator.Next(); + } + } + + breakLoop = (lastKey == null) ? (Func)null : (keyBytes) => + { + int compareResult = byteArrayComparer.Compare(keyBytes, lastKey); + if (compareResult >= 0) + { + // If this is the last key and its not included or we've overshot the range then stop without yielding a value. + if (!includeLastKey || compareResult > 0) + return true; + + // Stop after yielding the value. + done = true; + } + + // Keep going. + return false; + }; + + next = () => iterator.Next(); + } + + while (iterator.IsValid()) + { + byte[] keyBytes = iterator.Key(); + + if (breakLoop != null && breakLoop(keyBytes)) + break; + + yield return (keyBytes, keysOnly ? null : iterator.Value()); + + if (done) + break; + + next(); + } + } + } +} \ No newline at end of file diff --git a/src/Stratis.Bitcoin/Database/LevelDb.cs b/src/Stratis.Bitcoin/Database/LevelDb.cs new file mode 100644 index 0000000000..67f82ac593 --- /dev/null +++ b/src/Stratis.Bitcoin/Database/LevelDb.cs @@ -0,0 +1,136 @@ +using System.Linq; +using LevelDB; + +namespace Stratis.Bitcoin.Database +{ + /// A minimal LevelDb wrapper that makes it compliant with the interface. + public class LevelDb : IDb + { + private string dbPath; + + private DB db; + + public IDbIterator GetIterator(byte table) + { + return new LevelDbIterator(table, this.db.CreateIterator()); + } + + public void Open(string dbPath) + { + this.dbPath = dbPath; + this.db = new DB(new Options() { CreateIfMissing = true }, dbPath); + } + + public void Clear() + { + this.db.Dispose(); + System.IO.Directory.Delete(this.dbPath, true); + this.db = new DB(new Options() { CreateIfMissing = true }, this.dbPath); + } + + public IDbBatch GetWriteBatch() => new LevelDbBatch(this.db); + + public byte[] Get(byte table, byte[] key) + { + return this.db.Get(new[] { table }.Concat(key).ToArray()); + } + + public void Dispose() + { + this.db.Dispose(); + } + } + + /// A minimal LevelDb wrapper that makes it compliant with the interface. + public class LevelDbBatch : WriteBatch, IDbBatch + { + private DB db; + + public LevelDbBatch(DB db) + { + this.db = db; + } + + public IDbBatch Put(byte table, byte[] key, byte[] value) + { + return (IDbBatch)this.Put(new[] { table }.Concat(key).ToArray(), value); + } + + public IDbBatch Delete(byte table, byte[] key) + { + return (IDbBatch)this.Delete(new[] { table }.Concat(key).ToArray()); + } + + public void Write() + { + this.db.Write(this, new WriteOptions() { Sync = true }); + } + } + + /// A minimal LevelDb wrapper that makes it compliant with the interface. + public class LevelDbIterator : IDbIterator + { + private byte table; + private Iterator iterator; + + public LevelDbIterator(byte table, Iterator iterator) + { + this.table = table; + this.iterator = iterator; + } + + public void Seek(byte[] key) + { + this.iterator.Seek(new[] { this.table }.Concat(key).ToArray()); + } + + public void SeekToLast() + { + if (this.table != 255) + { + // First seek past the last record in the table by attempting to seek to the start of the next table (if any). + this.iterator.Seek(new[] { (byte)(this.table + 1) }); + + // If we managed to seek to the start of the next table then go back one record to arrive at the last record of 'table'. + if (this.iterator.IsValid()) + { + this.iterator.Prev(); + return; + } + } + + // If there is no next table then simply seek to the last record in the db as that will be the last record of 'table'. + this.iterator.SeekToLast(); + } + + public void Next() + { + this.iterator.Next(); + } + + public void Prev() + { + this.iterator.Prev(); + } + + public bool IsValid() + { + return this.iterator.IsValid() && this.iterator.Key()[0] == this.table; + } + + public byte[] Key() + { + return this.iterator.Key().Skip(1).ToArray(); + } + + public byte[] Value() + { + return this.iterator.Value(); + } + + public void Dispose() + { + this.iterator.Dispose(); + } + } +} diff --git a/src/Stratis.Bitcoin/Database/RocksDb.cs b/src/Stratis.Bitcoin/Database/RocksDb.cs new file mode 100644 index 0000000000..39092245f8 --- /dev/null +++ b/src/Stratis.Bitcoin/Database/RocksDb.cs @@ -0,0 +1,137 @@ +using System.Linq; +using NBitcoin; +using RocksDbSharp; + +namespace Stratis.Bitcoin.Database +{ + /// A minimal RocksDb wrapper that makes it compliant with the interface. + public class RocksDb : IDb + { + private string dbPath; + + private RocksDbSharp.RocksDb db; + + public IDbIterator GetIterator(byte table) + { + return new RocksDbIterator(table, this.db.NewIterator()); + } + + public void Open(string dbPath) + { + this.dbPath = dbPath; + this.db = RocksDbSharp.RocksDb.Open(new DbOptions().SetCreateIfMissing(), dbPath); + } + + public void Clear() + { + this.db.Dispose(); + System.IO.Directory.Delete(this.dbPath, true); + this.db = RocksDbSharp.RocksDb.Open(new DbOptions().SetCreateIfMissing(), this.dbPath); + } + + public IDbBatch GetWriteBatch() => new RocksDbBatch(this.db); + + public byte[] Get(byte table, byte[] key) + { + return this.db.Get(new[] { table }.Concat(key).ToArray()); + } + + public void Dispose() + { + this.db.Dispose(); + } + } + + /// A minimal RocksDb wrapper that makes it compliant with the interface. + public class RocksDbBatch : WriteBatch, IDbBatch + { + private RocksDbSharp.RocksDb db; + + public RocksDbBatch(RocksDbSharp.RocksDb db) + { + this.db = db; + } + + public IDbBatch Put(byte table, byte[] key, byte[] value) + { + return (IDbBatch)this.Put(new[] { table }.Concat(key).ToArray(), value); + } + + public IDbBatch Delete(byte table, byte[] key) + { + return (IDbBatch)this.Delete(new[] { table }.Concat(key).ToArray()); + } + + public void Write() + { + this.db.Write(this); + } + } + + /// A minimal RocksDb wrapper that makes it compliant with the interface. + public class RocksDbIterator : IDbIterator + { + private byte table; + private Iterator iterator; + + public RocksDbIterator(byte table, Iterator iterator) + { + this.table = table; + this.iterator = iterator; + } + + public void Seek(byte[] key) + { + this.iterator.Seek(new[] { this.table }.Concat(key).ToArray()); + } + + public void SeekToLast() + { + if (this.table != 255) + { + // First seek past the last record in the table by attempting to seek to the start of the next table (if any). + this.iterator.Seek(new[] { (byte)(this.table + 1) }); + + // If we managed to seek to the start of the next table then go back one record to arrive at the last record of 'table'. + if (this.iterator.Valid()) + { + this.iterator.Prev(); + return; + } + } + + // If there is no next table then simply seek to the last record in the db as that will be the last record of 'table'. + this.iterator.SeekToLast(); + } + + public void Next() + { + this.iterator.Next(); + } + + public void Prev() + { + this.iterator.Prev(); + } + + public bool IsValid() + { + return this.iterator.Valid() && this.iterator.Value()[0] == this.table; + } + + public byte[] Key() + { + return this.iterator.Key().Skip(1).ToArray(); + } + + public byte[] Value() + { + return this.iterator.Value(); + } + + public void Dispose() + { + this.iterator.Dispose(); + } + } +} diff --git a/src/Stratis.Bitcoin/EventBus/CoreEvents/AddressIndexerStatusEvent.cs b/src/Stratis.Bitcoin/EventBus/CoreEvents/AddressIndexerStatusEvent.cs new file mode 100644 index 0000000000..59f431ff63 --- /dev/null +++ b/src/Stratis.Bitcoin/EventBus/CoreEvents/AddressIndexerStatusEvent.cs @@ -0,0 +1,8 @@ + +namespace Stratis.Bitcoin.EventBus.CoreEvents +{ + public sealed class AddressIndexerStatusEvent : EventBase + { + public int Tip { get; set; } + } +} diff --git a/src/Stratis.Bitcoin/EventBus/CoreEvents/ConsensusManagerStatusEvent.cs b/src/Stratis.Bitcoin/EventBus/CoreEvents/ConsensusManagerStatusEvent.cs new file mode 100644 index 0000000000..35637b1f2b --- /dev/null +++ b/src/Stratis.Bitcoin/EventBus/CoreEvents/ConsensusManagerStatusEvent.cs @@ -0,0 +1,15 @@ +namespace Stratis.Bitcoin.EventBus.CoreEvents +{ + public class ConsensusManagerStatusEvent : EventBase + { + public bool IsIbd { get; } + + public int? HeaderHeight { get; } + + public ConsensusManagerStatusEvent(bool isIbd, int? headerHeight) + { + this.IsIbd = isIbd; + this.HeaderHeight = headerHeight; + } + } +} diff --git a/src/Stratis.Bitcoin/EventBus/CoreEvents/FederationWalletStatusEvent.cs b/src/Stratis.Bitcoin/EventBus/CoreEvents/FederationWalletStatusEvent.cs new file mode 100644 index 0000000000..ca92bffd25 --- /dev/null +++ b/src/Stratis.Bitcoin/EventBus/CoreEvents/FederationWalletStatusEvent.cs @@ -0,0 +1,17 @@ +using NBitcoin; + +namespace Stratis.Bitcoin.EventBus.CoreEvents +{ + public sealed class FederationWalletStatusEvent : EventBase + { + public Money ConfirmedBalance { get; private set; } + + public Money UnconfirmedBalance { get; private set; } + + public FederationWalletStatusEvent(Money confirmedBalance, Money unconfirmedBalance) + { + this.ConfirmedBalance = confirmedBalance; + this.UnconfirmedBalance = unconfirmedBalance; + } + } +} diff --git a/src/Stratis.Bitcoin/EventBus/CoreEvents/MultiSigMemberStateRequestEvent.cs b/src/Stratis.Bitcoin/EventBus/CoreEvents/MultiSigMemberStateRequestEvent.cs new file mode 100644 index 0000000000..2135ac596f --- /dev/null +++ b/src/Stratis.Bitcoin/EventBus/CoreEvents/MultiSigMemberStateRequestEvent.cs @@ -0,0 +1,19 @@ +namespace Stratis.Bitcoin.EventBus.CoreEvents +{ + public sealed class MultiSigMemberStateRequestEvent : EventBase + { + public MultiSigMemberStateRequestEvent() + { + } + + public string PubKey { get; set; } + + public int CrossChainStoreHeight { get; set; } + + public int CrossChainStoreNextDepositHeight { get; set; } + + public int PartialTransactions { get; set; } + + public int SuspendedPartialTransactions { get; set; } + } +} \ No newline at end of file diff --git a/src/Stratis.Bitcoin/EventBus/CoreEvents/PeerConnectionInfoEvent.cs b/src/Stratis.Bitcoin/EventBus/CoreEvents/PeerConnectionInfoEvent.cs new file mode 100644 index 0000000000..5e40823bda --- /dev/null +++ b/src/Stratis.Bitcoin/EventBus/CoreEvents/PeerConnectionInfoEvent.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Stratis.Bitcoin.Connection; + +namespace Stratis.Bitcoin.EventBus.CoreEvents +{ + public class PeerConnectionInfoEvent : EventBase + { + public IEnumerable PeerConnectionModels { get; set; } + + public PeerConnectionInfoEvent(IEnumerable peerConnectionModels) + { + this.PeerConnectionModels = peerConnectionModels; + } + } +} diff --git a/src/Stratis.Bitcoin/FullNode.cs b/src/Stratis.Bitcoin/FullNode.cs index 53b2608b53..6272e362f0 100644 --- a/src/Stratis.Bitcoin/FullNode.cs +++ b/src/Stratis.Bitcoin/FullNode.cs @@ -50,7 +50,7 @@ public class FullNode : IFullNode public FullNodeState State { get; private set; } /// - public DateTime StartTime { get; set; } + public DateTime StartTime => this.NodeStats.NodeStartedOn; /// Component responsible for connections to peers in P2P network. public IConnectionManager ConnectionManager { get; set; } @@ -189,8 +189,6 @@ public IFullNode Initialize(IFullNodeServiceProvider serviceProvider) this.Signals.Publish(new FullNodeEvent() { Message = $"Full node initialized on {this.Network.Name}.", State = this.State.ToString() }); - this.StartTime = this.DateTimeProvider.GetUtcNow(); - return this; } diff --git a/src/Stratis.Bitcoin/P2P/PeerAddressManager.cs b/src/Stratis.Bitcoin/P2P/PeerAddressManager.cs index 47167d1650..0263b4a7fa 100644 --- a/src/Stratis.Bitcoin/P2P/PeerAddressManager.cs +++ b/src/Stratis.Bitcoin/P2P/PeerAddressManager.cs @@ -45,6 +45,11 @@ public sealed class PeerAddressManager : IPeerAddressManager private const int MaxAddressesToStoreFromSingleIp = 1500; + /// + /// Bounds the maximum memory the peer address manager can be allowed to consume. + /// + private const int MaxAddressesToStore = 100_000; + /// Constructor used by dependency injection. public PeerAddressManager(IDateTimeProvider dateTimeProvider, DataFolder peerFilePath, ILoggerFactory loggerFactory, ISelfEndpointTracker selfEndpointTracker) { @@ -129,7 +134,8 @@ private PeerAddress AddPeerWithoutCleanup(IPEndPoint endPoint, IPAddress source) IPEndPoint ipv6EndPoint = endPoint.MapToIpv6(); PeerAddress peerToAdd = PeerAddress.Create(ipv6EndPoint, source.MapToIPv6()); - var added = this.peerInfoByPeerAddress.TryAdd(ipv6EndPoint, peerToAdd); + bool added = this.peerInfoByPeerAddress.TryAdd(ipv6EndPoint, peerToAdd); + if (added) { this.logger.LogTrace("(-)[PEER_ADDED]:{0}", endPoint); @@ -143,12 +149,72 @@ private PeerAddress AddPeerWithoutCleanup(IPEndPoint endPoint, IPAddress source) /// public void AddPeers(IEnumerable endPoints, IPAddress source) { - foreach (IPEndPoint endPoint in endPoints) + // Pre-filter for peers that exist already. + IEnumerable cleanedList = endPoints.Where(proposed => !this.peerInfoByPeerAddress.ContainsKey(proposed)); + + if (cleanedList.IsEmpty()) + return; + + cleanedList = this.EnsureMaxItems(cleanedList); + + foreach (IPEndPoint endPoint in cleanedList) this.AddPeerWithoutCleanup(endPoint, source); this.EnsureMaxItemsPerSource(source); } + private IEnumerable EnsureMaxItems(IEnumerable endPoints) + { + int numberToEvict = (this.peerInfoByPeerAddress.Count + endPoints.Count()) - MaxAddressesToStore; + + if (numberToEvict <= 0) + return endPoints; + + // Otherwise, we need to figure out whether to evict already-stored addresses or just trim the incoming list to fit. + // If we never evict already-stored addresses there is a potential risk that we only store dud addresses forever and land up with no valid peers. + + var evictions = new List(); + + foreach (IPEndPoint endPoint in this.peerInfoByPeerAddress.Keys) + { + if (!this.peerInfoByPeerAddress.TryGetValue(endPoint, out PeerAddress address)) + continue; + + // If the peer is not 'good', or we have never connected to them, then they are an eviction candidate. + if (address.Attempted || + address.IsBanned(this.dateTimeProvider.GetUtcNow()) || + address.Fresh) + { + evictions.Add(endPoint); + } + } + + // Shuffle the candidates. + evictions.Shuffle(); + + foreach (IPEndPoint endPointToEvict in evictions) + { + if (numberToEvict == 0) + break; + + this.peerInfoByPeerAddress.TryRemove(endPointToEvict, out _); + + numberToEvict--; + } + + if (numberToEvict >= endPoints.Count()) + { + return new List(); + } + + if (numberToEvict > 0) + { + return endPoints.Skip(numberToEvict); + } + + return endPoints; + } + private void EnsureMaxItemsPerSource(IPAddress source) { IEnumerable itemsFromSameSource = this.peerInfoByPeerAddress.Values.Where(x => x.Loopback.Equals(source.MapToIPv6())).Select(x => x.Endpoint); diff --git a/src/Stratis.Bitcoin/P2P/Protocol/Payloads/NotFoundPayload.cs b/src/Stratis.Bitcoin/P2P/Protocol/Payloads/NotFoundPayload.cs index fe142fb0d1..e7ab1b044e 100644 --- a/src/Stratis.Bitcoin/P2P/Protocol/Payloads/NotFoundPayload.cs +++ b/src/Stratis.Bitcoin/P2P/Protocol/Payloads/NotFoundPayload.cs @@ -7,6 +7,7 @@ namespace Stratis.Bitcoin.P2P.Protocol.Payloads /// /// A getdata message for an asked hash is not found by the remote peer. /// + [Payload("notfound")] public class NotFoundPayload : Payload, IEnumerable { private List inventory = new List(); diff --git a/src/Stratis.Bitcoin/Properties/AssemblyInfo.cs b/src/Stratis.Bitcoin/Properties/AssemblyInfo.cs index 8d1766b87e..bb3b89b91b 100644 --- a/src/Stratis.Bitcoin/Properties/AssemblyInfo.cs +++ b/src/Stratis.Bitcoin/Properties/AssemblyInfo.cs @@ -32,6 +32,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.3.2.4")] -[assembly: AssemblyFileVersion("1.3.2.4")] +[assembly: AssemblyVersion("1.4.0.7")] +[assembly: AssemblyFileVersion("1.4.0.7")] [assembly: InternalsVisibleTo("Stratis.Bitcoin.Tests")] \ No newline at end of file diff --git a/src/Stratis.Bitcoin/Stratis.Bitcoin.csproj b/src/Stratis.Bitcoin/Stratis.Bitcoin.csproj index c0cde9d110..4488a76c77 100644 --- a/src/Stratis.Bitcoin/Stratis.Bitcoin.csproj +++ b/src/Stratis.Bitcoin/Stratis.Bitcoin.csproj @@ -3,7 +3,7 @@ Stratis Full Node Stratis.Bitcoin - netcoreapp3.1 + net6.0 Stratis.Bitcoin Stratis.Core false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False ..\Stratis.ruleset Stratis Group Ltd. @@ -71,10 +71,6 @@ - - - - Library diff --git a/src/Stratis.Bitcoin/Utilities/AsyncLock.cs b/src/Stratis.Bitcoin/Utilities/AsyncLock.cs index 261e86dcdf..1351620008 100644 --- a/src/Stratis.Bitcoin/Utilities/AsyncLock.cs +++ b/src/Stratis.Bitcoin/Utilities/AsyncLock.cs @@ -59,7 +59,8 @@ internal Releaser(AsyncLock toRelease) /// public void Dispose() { - this.toRelease.semaphore.Release(); + if (this.toRelease.semaphore.CurrentCount == 0) + this.toRelease.semaphore.Release(); } } @@ -71,7 +72,7 @@ public void Dispose() /// This releaser is used when the lock has been acquired and disposing it will release the lock. /// /// We use the disposable interfaced in a task here to avoid allocations on acquiring the lock when it is free. - private readonly Task releaser; + private readonly IDisposable releaser; /// /// Initializes an instance of the object. @@ -79,7 +80,7 @@ public void Dispose() public AsyncLock() { this.semaphore = new SemaphoreSlim(1, 1); - this.releaser = Task.FromResult(new Releaser(this)); + this.releaser = new Releaser(this); } /// @@ -99,7 +100,7 @@ public AsyncLock() // If the task was cancelled, we don't hold the lock and we need to throw. if (wait.IsCanceled) throw new OperationCanceledException(); - return this.releaser; + return Task.FromResult(this.releaser); } // If the lock is not available, we wait until it is available @@ -111,7 +112,7 @@ public AsyncLock() if (task.IsCanceled) throw new OperationCanceledException(); return (IDisposable)state; - }, this.releaser.Result, cancel, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + }, this.releaser, cancel, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } /// @@ -125,12 +126,13 @@ public AsyncLock() this.semaphore.Wait(cancel); // We are holding the lock here, so we will want unlocking. - return this.releaser.Result; + return this.releaser; } /// public void Dispose() { + this.releaser.Dispose(); this.semaphore.Dispose(); } } diff --git a/src/Stratis.Bitcoin/Utilities/Extensions/ListExtensions.cs b/src/Stratis.Bitcoin/Utilities/Extensions/ListExtensions.cs new file mode 100644 index 0000000000..172329f9d1 --- /dev/null +++ b/src/Stratis.Bitcoin/Utilities/Extensions/ListExtensions.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System; + +namespace Stratis.Bitcoin.Utilities.Extensions +{ + public static class ListExtensions + { + private static Random random = new Random(); + + public static void Shuffle(this IList list) + { + if (list.Count == 0) + return; + + for (int i = list.Count - 1; i > 1; i--) + { + int rnd = random.Next(i + 1); + + T value = list[rnd]; + list[rnd] = list[i]; + list[i] = value; + } + } + } +} diff --git a/src/Stratis.Bitcoin/Utilities/Extensions/TypeExtensions.cs b/src/Stratis.Bitcoin/Utilities/Extensions/TypeExtensions.cs index 44d33fc603..7eab97a861 100644 --- a/src/Stratis.Bitcoin/Utilities/Extensions/TypeExtensions.cs +++ b/src/Stratis.Bitcoin/Utilities/Extensions/TypeExtensions.cs @@ -9,7 +9,7 @@ public static class TypeExtensions /// public static decimal BytesToKiloBytes(this long input, int decimals = 4) { - decimal result = Convert.ToDecimal(input / Math.Pow(2, 17)); + decimal result = Convert.ToDecimal(input / Math.Pow(2, 10)); return Math.Round(result, decimals); } diff --git a/src/Stratis.Bitcoin/Utilities/JsonConverters/Serializer.cs b/src/Stratis.Bitcoin/Utilities/JsonConverters/Serializer.cs index f92aec0a2b..2ccec368a6 100644 --- a/src/Stratis.Bitcoin/Utilities/JsonConverters/Serializer.cs +++ b/src/Stratis.Bitcoin/Utilities/JsonConverters/Serializer.cs @@ -1,5 +1,6 @@ using NBitcoin; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace Stratis.Bitcoin.Utilities.JsonConverters @@ -37,7 +38,9 @@ public static T ToObject(string data, Network network = null) { Formatting = Formatting.Indented }; + RegisterFrontConverters(settings, network); + return JsonConvert.DeserializeObject(data, settings); } @@ -47,8 +50,22 @@ public static string ToString(T response, Network network = null) { Formatting = Formatting.Indented }; + RegisterFrontConverters(settings, network); + return JsonConvert.SerializeObject(response, settings); } + + public static JToken ToToken(T response, Network network = null) + { + var settings = new JsonSerializerSettings + { + Formatting = Formatting.Indented + }; + + RegisterFrontConverters(settings, network); + + return JToken.FromObject(response, JsonSerializer.Create(settings)); + } } -} \ No newline at end of file +} diff --git a/src/Stratis.Bitcoin/Utilities/LruHashSet.cs b/src/Stratis.Bitcoin/Utilities/LruHashSet.cs new file mode 100644 index 0000000000..cc5c52c7fa --- /dev/null +++ b/src/Stratis.Bitcoin/Utilities/LruHashSet.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; + +namespace Stratis.Bitcoin.Utilities +{ + public class LruHashSet + { + private readonly LinkedList lru; + + private HashSet items; + + private long maxSize; + + private long itemCount; + + private readonly object lockObject = new object(); + + public LruHashSet(long maxSize = long.MaxValue) + { + this.lru = new LinkedList(); + this.items = new HashSet(); + + this.maxSize = maxSize; + this.itemCount = 0; + } + + public void AddOrUpdate(T item) + { + lock (this.lockObject) + { + // First check if we are performing the 'Update' case. No change to item count. + if (this.items.Contains(item)) + { + this.lru.Remove(item); + this.lru.AddLast(item); + + return; + } + + // Otherwise it's 'Add'. + // First perform the size test. + if ((this.itemCount + 1) > this.maxSize) + { + LinkedListNode tempItem = this.lru.First; + this.lru.RemoveFirst(); + this.items.Remove(tempItem.Value); + this.itemCount--; + } + + this.lru.AddLast(item); + this.items.Add(item); + this.itemCount++; + } + } + + public void Clear() + { + lock (this.lockObject) + { + this.lru.Clear(); + this.items.Clear(); + this.itemCount = 0; + } + } + + public bool Contains(T item) + { + lock (this.lockObject) + { + // Fastest to check the hashmap. + return this.items.Contains(item); + } + } + + public void Remove(T item) + { + lock (this.lockObject) + { + this.lru.Remove(item); + this.items.Remove(item); + this.itemCount--; + } + } + } +} diff --git a/src/Stratis.Bitcoin/Utilities/NodeStats.cs b/src/Stratis.Bitcoin/Utilities/NodeStats.cs index 646502d732..4ea1513cea 100644 --- a/src/Stratis.Bitcoin/Utilities/NodeStats.cs +++ b/src/Stratis.Bitcoin/Utilities/NodeStats.cs @@ -38,6 +38,8 @@ public interface INodeStats /// Collects benchmark stats. string GetBenchmark(); + + DateTime NodeStartedOn { get; } } public class NodeStats : INodeStats @@ -54,10 +56,11 @@ public class NodeStats : INodeStats private readonly IDateTimeProvider dateTimeProvider; private readonly ILogger logger; private readonly NodeSettings nodeSettings; - private readonly string nodeStartedOn; private List stats; private readonly IVersionProvider versionProvider; + public DateTime NodeStartedOn { get; private set; } + public NodeStats(IDateTimeProvider dateTimeProvider, NodeSettings nodeSettings, IVersionProvider versionProvider) { this.dateTimeProvider = dateTimeProvider; @@ -68,7 +71,7 @@ public NodeStats(IDateTimeProvider dateTimeProvider, NodeSettings nodeSettings, this.stats = new List(); this.versionProvider = versionProvider; - this.nodeStartedOn = this.dateTimeProvider.GetUtcNow().ToString(CultureInfo.InvariantCulture); + this.NodeStartedOn = this.dateTimeProvider.GetUtcNow(); } /// @@ -169,7 +172,7 @@ public string GetStats() statsBuilder.AppendLine("Agent".PadRight(LoggingConfiguration.ColumnLength, ' ') + $": {this.nodeSettings.Agent}:{this.versionProvider.GetVersion()} ({(int)this.nodeSettings.ProtocolVersion})"); statsBuilder.AppendLine("Network".PadRight(LoggingConfiguration.ColumnLength, ' ') + $": {this.nodeSettings.Network.Name}"); statsBuilder.AppendLine("Database".PadRight(LoggingConfiguration.ColumnLength, ' ') + $": {this.nodeSettings.ConfigReader.GetOrDefault("dbtype", "leveldb")}"); - statsBuilder.AppendLine("Node Started".PadRight(LoggingConfiguration.ColumnLength, ' ') + $": {this.nodeStartedOn}"); + statsBuilder.AppendLine("Node Started".PadRight(LoggingConfiguration.ColumnLength, ' ') + $": {this.NodeStartedOn.ToString(CultureInfo.InvariantCulture)}"); statsBuilder.AppendLine("Current Date".PadRight(LoggingConfiguration.ColumnLength, ' ') + $": {currentDateTime}"); if (this.nodeSettings.ConfigReader.GetOrDefault("displayextendednodestats", false)) diff --git a/src/Stratis.BitcoinD/Stratis.BitcoinD.csproj b/src/Stratis.BitcoinD/Stratis.BitcoinD.csproj index a14270710f..eec636798f 100644 --- a/src/Stratis.BitcoinD/Stratis.BitcoinD.csproj +++ b/src/Stratis.BitcoinD/Stratis.BitcoinD.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.BitcoinD Exe Stratis.BitcoinD diff --git a/src/Stratis.BreezeD/Stratis.BreezeD.csproj b/src/Stratis.BreezeD/Stratis.BreezeD.csproj index dd71c73960..e4b23a57c0 100644 --- a/src/Stratis.BreezeD/Stratis.BreezeD.csproj +++ b/src/Stratis.BreezeD/Stratis.BreezeD.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.BreezeD Exe Stratis.BreezeD diff --git a/src/Stratis.CirrusD/Properties/launchSettings.json b/src/Stratis.CirrusD/Properties/launchSettings.json index ffc6738c73..23473a9201 100644 --- a/src/Stratis.CirrusD/Properties/launchSettings.json +++ b/src/Stratis.CirrusD/Properties/launchSettings.json @@ -1,7 +1,8 @@ { "profiles": { "Stratis.CirrusD": { - "commandName": "Project" + "commandName": "Project", + "commandLineArgs": "" }, "Stratis.CirrusD Test": { "commandName": "Project", diff --git a/src/Stratis.CirrusD/Stratis.CirrusD.csproj b/src/Stratis.CirrusD/Stratis.CirrusD.csproj index 0b069d7add..e0243ed729 100644 --- a/src/Stratis.CirrusD/Stratis.CirrusD.csproj +++ b/src/Stratis.CirrusD/Stratis.CirrusD.csproj @@ -2,8 +2,8 @@ Exe - netcoreapp3.1 - 1.3.2.4 + net6.0 + 1.4.0.7 Stratis Group Ltd. Stratis Group Ltd. diff --git a/src/Stratis.CirrusDnsD/Stratis.CirrusDnsD.csproj b/src/Stratis.CirrusDnsD/Stratis.CirrusDnsD.csproj index c08a65ce67..2c146e2aaf 100644 --- a/src/Stratis.CirrusDnsD/Stratis.CirrusDnsD.csproj +++ b/src/Stratis.CirrusDnsD/Stratis.CirrusDnsD.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.CirrusDnsD Exe Stratis.CirrusDnsD @@ -17,7 +17,7 @@ latest Stratis Group Ltd. - 1.3.2.4 + 1.4.0.7 diff --git a/src/Stratis.CirrusMinerD/Program.cs b/src/Stratis.CirrusMinerD/Program.cs index 545e1802c8..0b9fe21943 100644 --- a/src/Stratis.CirrusMinerD/Program.cs +++ b/src/Stratis.CirrusMinerD/Program.cs @@ -103,6 +103,10 @@ private static IFullNode BuildCirrusMiningNode(string[] args) }) .UseSmartContractWallet() .AddSQLiteWalletRepository() + .AddSignalR(options => + { + DaemonConfiguration.ConfigureSignalRForCirrus(options); + }) .Build(); return node; @@ -175,6 +179,10 @@ private static IFullNode BuildStraxNode(string[] args) .UseWallet() .AddSQLiteWalletRepository() .AddPowPosMining(true) + .AddSignalR(options => + { + DaemonConfiguration.ConfigureSignalRForStrax(options); + }) .Build(); return node; diff --git a/src/Stratis.CirrusMinerD/Stratis.CirrusMinerD.csproj b/src/Stratis.CirrusMinerD/Stratis.CirrusMinerD.csproj index c3b4d90578..e471e9924f 100644 --- a/src/Stratis.CirrusMinerD/Stratis.CirrusMinerD.csproj +++ b/src/Stratis.CirrusMinerD/Stratis.CirrusMinerD.csproj @@ -2,8 +2,8 @@ Exe - netcoreapp3.1 - 1.3.2.4 + net6.0 + 1.4.0.7 Stratis Group Ltd. Stratis Group Ltd. diff --git a/src/Stratis.CirrusPegD/Program.cs b/src/Stratis.CirrusPegD/Program.cs index 5cce07ff61..e2ffc2f163 100644 --- a/src/Stratis.CirrusPegD/Program.cs +++ b/src/Stratis.CirrusPegD/Program.cs @@ -17,6 +17,7 @@ using Stratis.Bitcoin.Features.Miner; using Stratis.Bitcoin.Features.Notifications; using Stratis.Bitcoin.Features.RPC; +using Stratis.Bitcoin.Features.SignalR; using Stratis.Bitcoin.Features.SmartContracts; using Stratis.Bitcoin.Features.SmartContracts.PoA; using Stratis.Bitcoin.Features.SmartContracts.Wallet; @@ -103,6 +104,10 @@ private static IFullNode GetMainchainFullNode(string[] args) .UseWallet() .AddSQLiteWalletRepository() .AddPowPosMining(true) + .AddSignalR(options => + { + DaemonConfiguration.ConfigureSignalRForStrax(options); + }) .Build(); return node; @@ -141,6 +146,10 @@ private static IFullNode GetSidechainFullNode(string[] args) .AddInteroperability() .UseSmartContractWallet() .AddSQLiteWalletRepository() + .AddSignalR(options => + { + DaemonConfiguration.ConfigureSignalRForCirrus(options); + }) .Build(); return node; diff --git a/src/Stratis.CirrusPegD/Stratis.CirrusPegD.csproj b/src/Stratis.CirrusPegD/Stratis.CirrusPegD.csproj index 5fc0f5b024..ebb0ab4052 100644 --- a/src/Stratis.CirrusPegD/Stratis.CirrusPegD.csproj +++ b/src/Stratis.CirrusPegD/Stratis.CirrusPegD.csproj @@ -2,14 +2,15 @@ Exe - netcoreapp3.1 - 1.3.2.4 + net6.0 + 1.4.0.7 Stratis Group Ltd. + diff --git a/src/Stratis.External.Masternodes/Stratis.External.Masternodes.csproj b/src/Stratis.External.Masternodes/Stratis.External.Masternodes.csproj index 47791c4d2a..d35ad168d7 100644 --- a/src/Stratis.External.Masternodes/Stratis.External.Masternodes.csproj +++ b/src/Stratis.External.Masternodes/Stratis.External.Masternodes.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 diff --git a/src/Stratis.Features.Collateral/CollateralPoAMiner.cs b/src/Stratis.Features.Collateral/CollateralPoAMiner.cs index fb42585007..97884be2b7 100644 --- a/src/Stratis.Features.Collateral/CollateralPoAMiner.cs +++ b/src/Stratis.Features.Collateral/CollateralPoAMiner.cs @@ -17,6 +17,7 @@ using Stratis.Bitcoin.Interfaces; using Stratis.Bitcoin.Mining; using Stratis.Bitcoin.Primitives; +using Stratis.Bitcoin.Signals; using Stratis.Bitcoin.Utilities; using Stratis.Features.PoA.Collateral; using Stratis.Features.PoA.Collateral.CounterChain; @@ -38,23 +39,20 @@ public class CollateralPoAMiner : PoAMiner private readonly ChainIndexer chainIndexer; - private readonly JoinFederationRequestMonitor joinFederationRequestMonitor; - private readonly CancellationTokenSource cancellationSource; - public CollateralPoAMiner(IConsensusManager consensusManager, IDateTimeProvider dateTimeProvider, Network network, INodeLifetime nodeLifetime, IInitialBlockDownloadState ibdState, - BlockDefinition blockDefinition, ISlotsManager slotsManager, IConnectionManager connectionManager, JoinFederationRequestMonitor joinFederationRequestMonitor, PoABlockHeaderValidator poaHeaderValidator, + public CollateralPoAMiner(IConsensusManager consensusManager, IDateTimeProvider dateTimeProvider, Network network, INodeLifetime nodeLifetime, ILoggerFactory loggerFactory, + IInitialBlockDownloadState ibdState, BlockDefinition blockDefinition, ISlotsManager slotsManager, IConnectionManager connectionManager, PoABlockHeaderValidator poaHeaderValidator, IFederationManager federationManager, IFederationHistory federationHistory, IIntegrityValidator integrityValidator, IWalletManager walletManager, ChainIndexer chainIndexer, INodeStats nodeStats, VotingManager votingManager, PoASettings poAMinerSettings, ICollateralChecker collateralChecker, IAsyncProvider asyncProvider, ICounterChainSettings counterChainSettings, IIdleFederationMembersKicker idleFederationMembersKicker, - NodeSettings nodeSettings) - : base(consensusManager, dateTimeProvider, network, nodeLifetime, ibdState, blockDefinition, slotsManager, connectionManager, poaHeaderValidator, - federationManager, federationHistory, integrityValidator, walletManager, nodeStats, votingManager, poAMinerSettings, asyncProvider, idleFederationMembersKicker, nodeSettings) + ISignals signals, NodeSettings nodeSettings) + : base(consensusManager, dateTimeProvider, network, nodeLifetime, ibdState, blockDefinition, slotsManager, connectionManager, + poaHeaderValidator, federationManager, federationHistory, integrityValidator, walletManager, nodeStats, votingManager, poAMinerSettings, asyncProvider, idleFederationMembersKicker, signals, nodeSettings) { this.counterChainNetwork = counterChainSettings.CounterChainNetwork; this.collateralChecker = collateralChecker; this.encoder = new CollateralHeightCommitmentEncoder(); this.chainIndexer = chainIndexer; - this.joinFederationRequestMonitor = joinFederationRequestMonitor; this.cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(nodeLifetime.ApplicationStopping); } diff --git a/src/Stratis.Features.Collateral/Stratis.Features.Collateral.csproj b/src/Stratis.Features.Collateral/Stratis.Features.Collateral.csproj index 1e9b3b2c9b..22a8b53c45 100644 --- a/src/Stratis.Features.Collateral/Stratis.Features.Collateral.csproj +++ b/src/Stratis.Features.Collateral/Stratis.Features.Collateral.csproj @@ -1,8 +1,8 @@  - netcoreapp3.1 - 4.0.9.3 + net6.0 + 4.0.11.0 Stratis Group Ltd. diff --git a/src/Stratis.Features.Diagnostic/Stratis.Features.Diagnostic.csproj b/src/Stratis.Features.Diagnostic/Stratis.Features.Diagnostic.csproj index 43cae3b911..717149bc30 100644 --- a/src/Stratis.Features.Diagnostic/Stratis.Features.Diagnostic.csproj +++ b/src/Stratis.Features.Diagnostic/Stratis.Features.Diagnostic.csproj @@ -1,10 +1,10 @@  - netcoreapp3.1 + net6.0 ..\None.ruleset true - 1.3.2.4 + 1.4.0.7 Stratis Group Ltd. diff --git a/src/Stratis.Features.FederatedPeg.IntegrationTests/NodeInitialisationTests.cs b/src/Stratis.Features.FederatedPeg.IntegrationTests/NodeInitialisationTests.cs index 4dcd5d8af1..9354f5a475 100644 --- a/src/Stratis.Features.FederatedPeg.IntegrationTests/NodeInitialisationTests.cs +++ b/src/Stratis.Features.FederatedPeg.IntegrationTests/NodeInitialisationTests.cs @@ -167,17 +167,20 @@ public void GatewayPairStarts() { using (var nodeBuilder = SidechainNodeBuilder.CreateSidechainNodeBuilder(this)) { - CoreNode side = nodeBuilder.CreateSidechainFederationNode(this.sidechainNetwork, this.mainNetwork, this.sidechainNetwork.FederationKeys[0]); + CirrusRegTest collateralSidechainNetwork = new CirrusSingleCollateralRegTest(); + + CoreNode side = nodeBuilder.CreateSidechainFederationNode(collateralSidechainNetwork, this.mainNetwork, collateralSidechainNetwork.FederationKeys[0]); side.AppendToConfig("sidechain=1"); side.AppendToConfig($"redeemscript={this.scriptAndAddresses.payToMultiSig}"); - side.AppendToConfig($"publickey={this.sidechainNetwork.FederationMnemonics[0].DeriveExtKey().PrivateKey.PubKey}"); + side.AppendToConfig($"publickey={collateralSidechainNetwork.FederationMnemonics[0].DeriveExtKey().PrivateKey.PubKey}"); side.AppendToConfig("federationips=0.0.0.0,0.0.0.1"); // Placeholders side.AppendToConfig($"mindepositconfirmations={DepositConfirmations}"); - CoreNode main = nodeBuilder.CreateMainChainFederationNode(this.mainNetwork, this.sidechainNetwork).WithWallet(); + CoreNode main = nodeBuilder.CreateMainChainFederationNode(this.mainNetwork, collateralSidechainNetwork).WithWallet(); + main.AppendToConfig("addressindex=1"); main.AppendToConfig("mainchain=1"); main.AppendToConfig($"redeemscript={this.scriptAndAddresses.payToMultiSig}"); - main.AppendToConfig($"publickey={this.sidechainNetwork.FederationMnemonics[0].DeriveExtKey().PrivateKey.PubKey}"); + main.AppendToConfig($"publickey={collateralSidechainNetwork.FederationMnemonics[0].DeriveExtKey().PrivateKey.PubKey}"); main.AppendToConfig("federationips=0.0.0.0,0.0.0.1"); // Placeholders main.AppendToConfig($"mindepositconfirmations={DepositConfirmations}"); diff --git a/src/Stratis.Features.FederatedPeg.IntegrationTests/RewardClaimerTests.cs b/src/Stratis.Features.FederatedPeg.IntegrationTests/RewardClaimerTests.cs index dfdc902bff..55f3c68b7c 100644 --- a/src/Stratis.Features.FederatedPeg.IntegrationTests/RewardClaimerTests.cs +++ b/src/Stratis.Features.FederatedPeg.IntegrationTests/RewardClaimerTests.cs @@ -160,7 +160,7 @@ public void RewardsToSideChainCanHandleReorgs() minter.StopStake(); // Ensure nodes are synced. - TestBase.WaitLoop(() => TestHelper.AreNodesSynced(nodeA, nodeB)); + TestBase.WaitLoop(() => TestHelper.AreNodesSynced(nodeA, nodeB), waitTimeSeconds: 120); // Call the block connected again on block 40. // This is to simulate that a rewind occurred to block 39 and block 40 was reconnected. diff --git a/src/Stratis.Features.FederatedPeg.IntegrationTests/Stratis.Features.FederatedPeg.IntegrationTests.csproj b/src/Stratis.Features.FederatedPeg.IntegrationTests/Stratis.Features.FederatedPeg.IntegrationTests.csproj index 6a6be97faf..9142076b18 100644 --- a/src/Stratis.Features.FederatedPeg.IntegrationTests/Stratis.Features.FederatedPeg.IntegrationTests.csproj +++ b/src/Stratis.Features.FederatedPeg.IntegrationTests/Stratis.Features.FederatedPeg.IntegrationTests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Full false diff --git a/src/Stratis.Features.FederatedPeg.Tests/CollateralCheckerTests.cs b/src/Stratis.Features.FederatedPeg.Tests/CollateralCheckerTests.cs index 612acf5d8a..0cb08e6279 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/CollateralCheckerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/CollateralCheckerTests.cs @@ -87,6 +87,13 @@ public async Task InitializationTakesForeverIfCounterNodeIsOfflineAsync() { InitializeCollateralChecker(); + var blockStoreClientMock = new Mock(); + var collateralData = default(VerboseAddressBalancesResult); + + blockStoreClientMock.Setup(x => x.VerboseAddressesBalancesDataAsync(It.IsAny>(), It.IsAny())).ReturnsAsync(collateralData); + + this.collateralChecker.SetPrivateVariableValue("blockStoreClient", blockStoreClientMock.Object); + Task initTask = this.collateralChecker.InitializeAsync(); await Task.Delay(10_000); diff --git a/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs b/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs index 744ab85356..8b05cdccdf 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs @@ -20,6 +20,7 @@ using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Features.PoA; using Stratis.Bitcoin.Features.PoA.Voting; +using Stratis.Bitcoin.Interfaces; using Stratis.Bitcoin.Networks; using Stratis.Bitcoin.Primitives; using Stratis.Bitcoin.Signals; @@ -87,6 +88,7 @@ private FederationGatewayController CreateController(IFederatedPegSettings feder this.federationWalletManager, Substitute.For(), Substitute.For(), + Substitute.For(), this.federationManager); return controller; @@ -223,6 +225,7 @@ public void Call_Sidechain_Gateway_Get_Info() this.federationWalletManager, Substitute.For(), Substitute.For(), + Substitute.For(), this.federationManager); IActionResult result = controller.GetInfo(); @@ -317,6 +320,7 @@ public void Call_Mainchain_Gateway_Get_Info() this.federationWalletManager, Substitute.For(), Substitute.For(), + Substitute.For(), this.federationManager); IActionResult result = controller.GetInfo(); diff --git a/src/Stratis.Features.FederatedPeg.Tests/CrossChainTestBase.cs b/src/Stratis.Features.FederatedPeg.Tests/CrossChainTestBase.cs index f891616aaa..ed0868788b 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/CrossChainTestBase.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/CrossChainTestBase.cs @@ -213,7 +213,8 @@ protected void Init(DataFolder dataFolder) this.dateTimeProvider, this.federatedPegSettings, this.withdrawalExtractor, - this.blockRepository); + this.blockRepository, + Substitute.For()); // Starts and creates the wallet. this.federationWalletManager.Start(); diff --git a/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs b/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs index 045a689f24..699a9fb6a3 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs @@ -615,7 +615,7 @@ public async Task DoTestAsync() var counterChainNetwork = new CounterChainNetworkWrapper(CirrusNetwork.NetworksSelector.Testnet()); var reader = new OpReturnDataReader(counterChainNetwork.CounterChainNetwork); var extractor = new DepositExtractor(Substitute.For(), this.federatedPegSettings, this.network, reader, Substitute.For()); - IDeposit deposit = await extractor.ExtractDepositFromTransaction(transaction, 2, 1); + IDeposit deposit = await extractor.ExtractDepositFromTransaction(transaction, 2, 1, 0); Assert.NotNull(deposit); Assert.Equal(transaction.GetHash(), deposit.Id); diff --git a/src/Stratis.Features.FederatedPeg.Tests/Distribution/RewardClaimerTests.cs b/src/Stratis.Features.FederatedPeg.Tests/Distribution/RewardClaimerTests.cs index 9babc68db3..74d41345b9 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/Distribution/RewardClaimerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/Distribution/RewardClaimerTests.cs @@ -99,7 +99,7 @@ public async Task RewardClaimer_RetrieveSingleDepositsAsync() for (int i = 11; i <= 15; i++) { Transaction rewardTransaction = rewardClaimer.BuildRewardTransaction(false); - IDeposit deposit = await depositExtractor.ExtractDepositFromTransaction(rewardTransaction, i, this.blocks[i].Block.GetHash()); + IDeposit deposit = await depositExtractor.ExtractDepositFromTransaction(rewardTransaction, i, this.blocks[i].Block.GetHash(), 0); Assert.NotNull(deposit); } } @@ -130,7 +130,7 @@ public async Task RewardClaimer_RetrieveBatchedDepositsAsync() Assert.Equal(Money.Coins(90), rewardTransaction.TotalOut); var depositExtractor = new DepositExtractor(this.conversionRequestRepository, this.federatedPegSettings, this.network, this.opReturnDataReader, this.blockStore); - IDeposit deposit = await depositExtractor.ExtractDepositFromTransaction(rewardTransaction, 30, this.blocks[30].Block.GetHash()); + IDeposit deposit = await depositExtractor.ExtractDepositFromTransaction(rewardTransaction, 30, this.blocks[30].Block.GetHash(), 0); Assert.Equal(Money.Coins(90), deposit.Amount); } } diff --git a/src/Stratis.Features.FederatedPeg.Tests/Stratis.Features.FederatedPeg.Tests.csproj b/src/Stratis.Features.FederatedPeg.Tests/Stratis.Features.FederatedPeg.Tests.csproj index 068c496bc2..dd2295fc88 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/Stratis.Features.FederatedPeg.Tests.csproj +++ b/src/Stratis.Features.FederatedPeg.Tests/Stratis.Features.FederatedPeg.Tests.csproj @@ -8,7 +8,7 @@ - netcoreapp3.1 + net6.0 Full false diff --git a/src/Stratis.Features.FederatedPeg.Tests/Wallet/FederationWalletManagerTests.cs b/src/Stratis.Features.FederatedPeg.Tests/Wallet/FederationWalletManagerTests.cs index 0bd5b2559d..c7fe284155 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/Wallet/FederationWalletManagerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/Wallet/FederationWalletManagerTests.cs @@ -121,7 +121,8 @@ private FederationWalletManager CreateFederationWalletManager() new Mock().Object, federatedPegSettings.Object, new Mock().Object, - new Mock().Object); + new Mock().Object, + new Mock().Object); federationWalletManager.Start(); diff --git a/src/Stratis.Features.FederatedPeg/Controllers/FederationGatewayController.cs b/src/Stratis.Features.FederatedPeg/Controllers/FederationGatewayController.cs index c57e2d2c54..c70f984a8b 100644 --- a/src/Stratis.Features.FederatedPeg/Controllers/FederationGatewayController.cs +++ b/src/Stratis.Features.FederatedPeg/Controllers/FederationGatewayController.cs @@ -12,6 +12,7 @@ using Stratis.Bitcoin.Connection; using Stratis.Bitcoin.Controllers.Models; using Stratis.Bitcoin.Features.PoA; +using Stratis.Bitcoin.Interfaces; using Stratis.Bitcoin.P2P.Peer; using Stratis.Bitcoin.Utilities; using Stratis.Bitcoin.Utilities.Extensions; @@ -37,6 +38,7 @@ public static class FederationGatewayRouteEndPoint public const string GetTransfersFullySignedEndpoint = "transfers/fullysigned"; public const string GetTransfersSuspendedEndpoint = "transfers/suspended"; public const string VerifyPartialTransactionEndpoint = "transfer/verify"; + public const string GetMultiSigTransactionSignersEndpoint = "transfer/signers"; } /// @@ -58,6 +60,7 @@ public class FederationGatewayController : Controller private readonly IMaturedBlocksProvider maturedBlocksProvider; private readonly Network network; private readonly IPeerBanning peerBanning; + private readonly IBlockStore blockStore; public FederationGatewayController( IAsyncProvider asyncProvider, @@ -70,6 +73,7 @@ public FederationGatewayController( IFederationWalletManager federationWalletManager, IFullNode fullNode, IPeerBanning peerBanning, + IBlockStore blockStore, IFederationManager federationManager = null) { this.asyncProvider = asyncProvider; @@ -84,6 +88,7 @@ public FederationGatewayController( this.maturedBlocksProvider = maturedBlocksProvider; this.network = network; this.peerBanning = peerBanning; + this.blockStore = blockStore; } /// @@ -442,6 +447,78 @@ public async Task VerifyPartialTransactionAsync([FromQuery(Name = return this.Json($"{depositIdTransactionId} does not exist."); } + [Route(FederationGatewayRouteEndPoint.GetMultiSigTransactionSignersEndpoint)] + [HttpGet] + public async Task GetPartialTransactionSignersAsync([FromQuery] string trxid, [FromQuery] int input) + { + try + { + Guard.NotEmpty(trxid, nameof(trxid)); + + uint256 txid; + if (!uint256.TryParse(trxid, out txid)) + { + throw new ArgumentException(nameof(trxid)); + } + + ICrossChainTransfer[] cctx = await this.crossChainTransferStore.GetAsync(new[] { txid }).ConfigureAwait(false); + + Transaction trx = cctx.FirstOrDefault()?.PartialTransaction; + + if (trx == null) + return BadRequest("The Partial Transaction was null."); + + uint256 prevOutHash = trx.Inputs[input].PrevOut.Hash; + uint prevOutIndex = trx.Inputs[input].PrevOut.N; + + Transaction prevTrx = this.blockStore.GetTransactionById(prevOutHash); + + // Shouldn't be possible under normal circumstances. + if (prevTrx == null) + return BadRequest("The previous transaction was null."); + + TxOut txout = prevTrx.Outputs[prevOutIndex]; + + var txData = new PrecomputedTransactionData(trx); + var checker = new TransactionChecker(trx, input, txout.Value, txData); + + var ctx = new PartialTransactionScriptEvaluationContext(this.network) + { + ScriptVerify = ScriptVerify.Mandatory + }; + + // Run the verification to populate the context, but don't actually check the result as this is only a partially signed transaction. + ctx.VerifyScript(trx.Inputs[input].ScriptSig, txout.ScriptPubKey, checker); + + var signers = new HashSet(); + + (PubKey[] transactionSigningKeys, int signaturesRequired) federation = this.network.Federations.GetOnlyFederation().GetFederationDetails(); + + foreach (SignedHash signedHash in ctx.SignedHashes) + { + for (int i = 0; i < 3; i++) + { + PubKey pubKey = PubKey.RecoverFromSignature(i, signedHash.Signature.Signature, signedHash.Hash, true); + + if (pubKey == null) + continue; + + if (federation.transactionSigningKeys != null && federation.transactionSigningKeys.Contains(pubKey)) + { + signers.Add(pubKey.ToHex()); + } + } + } + + return Ok(this.Json(signers)); + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString()); + } + } + [Route(FederationGatewayRouteEndPoint.DeleteSuspended)] [HttpDelete] [ProducesResponseType((int)HttpStatusCode.OK)] diff --git a/src/Stratis.Features.FederatedPeg/Conversion/ConversionRequest.cs b/src/Stratis.Features.FederatedPeg/Conversion/ConversionRequest.cs index f75e3f9613..7129fbf4c6 100644 --- a/src/Stratis.Features.FederatedPeg/Conversion/ConversionRequest.cs +++ b/src/Stratis.Features.FederatedPeg/Conversion/ConversionRequest.cs @@ -93,6 +93,7 @@ public class ConversionRequest : IBitcoinSerializable /// wSTRAX burn transactions are already denominated in wei on the Ethereum chain and thus need to be converted back into satoshi when the /// conversion request is created. /// For ERC20-SRC20 transfers this amount field is the full-precision integral token amount being transferred, typically 18 decimal places for ERC20. + /// For ERC721-SRC721 transfers this amount field is the token identifier of the NFT. /// public uint256 Amount { get { return this.amount; } set { this.amount = value; } } @@ -102,12 +103,14 @@ public class ConversionRequest : IBitcoinSerializable public bool Processed { get { return this.processed; } set { this.processed = value; } } /// - /// Should the request failed, this field can be used for any error messages. + /// Should the request fail, this field can be used for any error messages. /// public string StatusMessage { get { return this.statusMessage; } set { this.statusMessage = value; } } public string TokenContract { get { return this.tokenContract; } set { this.tokenContract = value; } } + public string TokenUri { get { return this.tokenUri; } set { this.tokenUri = value; } } + private uint256 amount; private int blockHeight; @@ -136,6 +139,8 @@ public class ConversionRequest : IBitcoinSerializable private string tokenContract; + private string tokenUri; + public void ReadWrite(BitcoinStream stream) { stream.ReadWrite(ref this.requestId); @@ -164,6 +169,8 @@ public void ReadWrite(BitcoinStream stream) { this.amount = this.dummyAmount; } + + ReadWriteNullStringField(stream, ref this.tokenUri); } private void ReadWriteNullStringField(BitcoinStream stream, ref string nullField) diff --git a/src/Stratis.Features.FederatedPeg/Coordination/ConversionRequestCoordinationService.cs b/src/Stratis.Features.FederatedPeg/Coordination/ConversionRequestCoordinationService.cs index 71ddca6296..c9bac8d48e 100644 --- a/src/Stratis.Features.FederatedPeg/Coordination/ConversionRequestCoordinationService.cs +++ b/src/Stratis.Features.FederatedPeg/Coordination/ConversionRequestCoordinationService.cs @@ -44,6 +44,10 @@ public interface IConversionRequestCoordinationService /// A dictionary of pubkeys that voted on a request. Dictionary> GetStatus(); + /// Provides mapping of all request ids to the vote tally per transactionId for that request. + /// A dictionary of vote tallies per potential transactionId for a given request. + Dictionary> GetTransactionIdStatus(); + /// /// Registers the quorum for conversion request transactions, i.e. minimum amount of votes required to process it. /// @@ -170,6 +174,15 @@ public Dictionary> GetStatus() } } + /// + public Dictionary> GetTransactionIdStatus() + { + lock (this.lockObject) + { + return this.transactionIdVotes; + } + } + /// public void RegisterConversionRequestQuorum(int conversionRequestQuorum) { diff --git a/src/Stratis.Features.FederatedPeg/Distribution/MultiSigMembers.cs b/src/Stratis.Features.FederatedPeg/Distribution/MultiSigMembers.cs index 570fc1515f..16024c3a7a 100644 --- a/src/Stratis.Features.FederatedPeg/Distribution/MultiSigMembers.cs +++ b/src/Stratis.Features.FederatedPeg/Distribution/MultiSigMembers.cs @@ -20,7 +20,7 @@ public static bool IsContractOwner(Network network, PubKey pubKey) /// This is the current set of multisig members that are participating in the multisig contract. /// /// TODO: Refactor to make this list dynamic. - private static readonly List InteropMultisigContractPubKeysMainNet = new List() + public static readonly List InteropMultisigContractPubKeysMainNet = new List() { new PubKey("027e793fbf4f6d07de15b0aa8355f88759b8bdf92a9ffb8a65a87fa8ee03baeccd"),// new PubKey("03e8809be396745434ee8c875089e518a3eef40e31ade81869ce9cbef63484996d"),// @@ -41,7 +41,7 @@ public static bool IsContractOwner(Network network, PubKey pubKey) /// This is the current set of multisig members that are participating in the multisig contract. /// /// TODO: Refactor to make this list dynamic. - private static readonly List InteropMultisigContractPubKeysTestNet = new List() + public static readonly List InteropMultisigContractPubKeysTestNet = new List() { new PubKey("03cfc06ef56352038e1169deb3b4fa228356e2a54255cf77c271556d2e2607c28c"), // Cirrus 1 new PubKey("02fc828e06041ae803ab5378b5ec4e0def3d4e331977a69e1b6ef694d67f5c9c13"), // Cirrus 3 diff --git a/src/Stratis.Features.FederatedPeg/Distribution/RewardDistributionManager.cs b/src/Stratis.Features.FederatedPeg/Distribution/RewardDistributionManager.cs index 599360a789..d5b504be59 100644 --- a/src/Stratis.Features.FederatedPeg/Distribution/RewardDistributionManager.cs +++ b/src/Stratis.Features.FederatedPeg/Distribution/RewardDistributionManager.cs @@ -6,6 +6,7 @@ using Stratis.Bitcoin.Configuration.Logging; using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Features.PoA; +using Stratis.Bitcoin.Utilities; using Stratis.Features.FederatedPeg.Conversion; using Stratis.Features.FederatedPeg.Wallet; using Stratis.Features.PoA.Collateral; @@ -142,9 +143,11 @@ public List DistributeToMultisigNodes(uint256 depositId, Money fee) multiSigRecipients.Add(new Recipient() { Amount = feeReward, ScriptPubKey = multiSigMinerScript }); this.logger.LogDebug($"Paying multisig member '{multiSigMinerScript.ToHex()}' (hex) {feeReward} STRAX."); } - } + if (this.chainIndexer.Tip.Height >= (this.network.Consensus.Options as PoAConsensusOptions).Release1400ActivationHeight) + return multiSigRecipients.OrderBy(m => m.ScriptPubKey.ToHex()).ToList(); + return multiSigRecipients; } @@ -158,10 +161,45 @@ public List Distribute(int heightOfRecordedDistributionDeposit, Money // Then find the header on the sidechain that contains the applicable commitment height. int sidechainTipHeight = this.chainIndexer.Tip.Height; - ChainedHeader currentHeader = this.chainIndexer.GetHeader(sidechainTipHeight); + // Get the set of miners (more specifically, the scriptPubKeys they generated blocks with) to distribute rewards to. + // Based on the computed 'common block height' we define the distribution epoch: + int currentHeaderHeight = FindSidechainHeightWithCommitmentBelowOrEqualMainchainHeight(applicableMainChainDepositHeight, sidechainTipHeight); + int sidechainStartHeight = currentHeaderHeight; + + this.logger.LogTrace("Initial {0} : {1}", nameof(sidechainStartHeight), sidechainStartHeight); + + // This is a special case which will not be the case on the live network. + if (sidechainStartHeight < this.epoch) + sidechainStartHeight = 0; + + // If the sidechain start is more than the epoch, then deduct the epoch window. + if (sidechainStartHeight > this.epoch) + sidechainStartHeight -= this.epoch; + + this.logger.LogTrace("Adjusted {0} : {1}", nameof(sidechainStartHeight), sidechainStartHeight); + + // Ensure that the dictionary is cleared on every run. + // As this is a static class, new instances of this dictionary will + // only be cleaned up once the node shuts down. It is therefore better + // to use a single instance to work with. + this.blocksMinedEach.Clear(); + + var totalBlocks = CalculateBlocksMinedPerMiner(sidechainStartHeight, currentHeaderHeight); + return ConstructRecipients(heightOfRecordedDistributionDeposit, totalBlocks, totalReward); + } - do + private int FindSidechainHeightWithCommitmentBelowOrEqualMainchainHeight(int mainchainHeight, int sidechainTipHeight) + { + // Find the first sidechain height with commitment above the 'mainchainHeight'. + // Subtracting 1 will give us the height of the last commitment below the mainchainHeight. + + return BinarySearch.BinaryFindFirst((int height) => { + if (height > sidechainTipHeight) + return true; + + ChainedHeader currentHeader = this.chainIndexer[height]; + this.commitmentTransactionByHashDictionary.TryGetValue(currentHeader.HashBlock, out Transaction transactionToCheck); if (transactionToCheck == null) @@ -181,43 +219,17 @@ public List Distribute(int heightOfRecordedDistributionDeposit, Money commitmentHeightToCheck = heightOfMainChainCommitment.Value; this.commitmentHeightsByHash.Add(currentHeader.HashBlock, commitmentHeightToCheck); } - } - - if (commitmentHeightToCheck != null) - { - this.logger.LogTrace("{0} : {1}={2}", currentHeader, nameof(commitmentHeightToCheck), commitmentHeightToCheck); - if (commitmentHeightToCheck <= applicableMainChainDepositHeight) - break; + if (commitmentHeightToCheck == null) + { + return null; + } } - currentHeader = currentHeader.Previous; - - } while (currentHeader.Height != 0); - - // Get the set of miners (more specifically, the scriptPubKeys they generated blocks with) to distribute rewards to. - // Based on the computed 'common block height' we define the distribution epoch: - int sidechainStartHeight = currentHeader.Height; - this.logger.LogTrace("Initial {0} : {1}", nameof(sidechainStartHeight), sidechainStartHeight); - - // This is a special case which will not be the case on the live network. - if (sidechainStartHeight < this.epoch) - sidechainStartHeight = 0; - - // If the sidechain start is more than the epoch, then deduct the epoch window. - if (sidechainStartHeight > this.epoch) - sidechainStartHeight -= this.epoch; - - this.logger.LogTrace("Adjusted {0} : {1}", nameof(sidechainStartHeight), sidechainStartHeight); - - // Ensure that the dictionary is cleared on every run. - // As this is a static class, new instances of this dictionary will - // only be cleaned up once the node shuts down. It is therefore better - // to use a single instance to work with. - this.blocksMinedEach.Clear(); + this.logger.LogTrace("{0} : {1}={2}", currentHeader, nameof(commitmentHeightToCheck), commitmentHeightToCheck); - var totalBlocks = CalculateBlocksMinedPerMiner(sidechainStartHeight, currentHeader.Height); - return ConstructRecipients(heightOfRecordedDistributionDeposit, totalBlocks, totalReward); + return commitmentHeightToCheck > mainchainHeight; + }, 1, sidechainTipHeight + 1) - 1; } private long CalculateBlocksMinedPerMiner(int sidechainStartHeight, int sidechainEndHeight) diff --git a/src/Stratis.Features.FederatedPeg/FederatedPegFeature.cs b/src/Stratis.Features.FederatedPeg/FederatedPegFeature.cs index 8ba789150d..a917d33583 100644 --- a/src/Stratis.Features.FederatedPeg/FederatedPegFeature.cs +++ b/src/Stratis.Features.FederatedPeg/FederatedPegFeature.cs @@ -15,9 +15,11 @@ using Stratis.Bitcoin.Connection; using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Features.Notifications; +using Stratis.Bitcoin.Features.PoA; using Stratis.Bitcoin.Features.SmartContracts; using Stratis.Bitcoin.P2P.Peer; using Stratis.Bitcoin.P2P.Protocol.Payloads; +using Stratis.Bitcoin.Signals; using Stratis.Bitcoin.Utilities; using Stratis.Features.Collateral.CounterChain; using Stratis.Features.FederatedPeg.Controllers; @@ -26,6 +28,7 @@ using Stratis.Features.FederatedPeg.Distribution; using Stratis.Features.FederatedPeg.InputConsolidation; using Stratis.Features.FederatedPeg.Interfaces; +using Stratis.Features.FederatedPeg.Monitoring; using Stratis.Features.FederatedPeg.Notifications; using Stratis.Features.FederatedPeg.Payloads; using Stratis.Features.FederatedPeg.SourceChain; @@ -66,8 +69,14 @@ internal class FederatedPegFeature : FullNodeFeature private readonly IInputConsolidator inputConsolidator; + private readonly IFederationManager federationManager; + private readonly ILogger logger; + private readonly MultiSigStateMonitor multiSigStateMonitor; + + private readonly ISignals signals; + public FederatedPegFeature( IConnectionManager connectionManager, IFederatedPegSettings federatedPegSettings, @@ -81,7 +90,10 @@ public FederatedPegFeature( MempoolCleaner mempoolCleaner, ISignedMultisigTransactionBroadcaster signedBroadcaster, IMaturedBlocksSyncManager maturedBlocksSyncManager, - IInputConsolidator inputConsolidator) + IInputConsolidator inputConsolidator, + ISignals signals, + IFederationManager federationManager = null, + MultiSigStateMonitor multiSigStateMonitor = null) { this.connectionManager = connectionManager; this.federatedPegSettings = federatedPegSettings; @@ -95,12 +107,16 @@ public FederatedPegFeature( this.maturedBlocksSyncManager = maturedBlocksSyncManager; this.signedBroadcaster = signedBroadcaster; this.inputConsolidator = inputConsolidator; + this.federationManager = federationManager; + this.multiSigStateMonitor = multiSigStateMonitor; + this.signals = signals; this.logger = LogManager.GetCurrentClassLogger(); // add our payload var payloadProvider = (PayloadProvider)this.fullNode.Services.ServiceProvider.GetService(typeof(PayloadProvider)); payloadProvider.AddPayload(typeof(RequestPartialTransactionPayload)); + payloadProvider.AddPayload(typeof(MultiSigMemberStateRequestPayload)); nodeStats.RegisterStats(this.AddComponentStats, StatsType.Component, this.GetType().Name); } @@ -140,8 +156,13 @@ public override async Task InitializeAsync() // Respond to requests to sign transactions from other nodes. NetworkPeerConnectionParameters networkPeerConnectionParameters = this.connectionManager.Parameters; - networkPeerConnectionParameters.TemplateBehaviors.Add(new PartialTransactionsBehavior(this.federationWalletManager, this.network, - this.federatedPegSettings, this.crossChainTransferStore, this.inputConsolidator)); + networkPeerConnectionParameters.TemplateBehaviors.Add(new PartialTransactionsBehavior(this.federationWalletManager, this.network, this.federatedPegSettings, this.crossChainTransferStore, this.inputConsolidator)); + + if (!this.federatedPegSettings.IsMainChain) + { + this.multiSigStateMonitor.Initialize(); + networkPeerConnectionParameters.TemplateBehaviors.Add(new MultiSigStateMonitorBehavior(this.network, this.crossChainTransferStore, this.federationManager, this.federatedPegSettings, this.signals)); + } } /// @@ -303,6 +324,9 @@ public static IFullNodeBuilder AddFederatedPeg(this IFullNodeBuilder fullNodeBui services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } // The reward claimer only runs on the main chain. diff --git a/src/Stratis.Features.FederatedPeg/FederatedPegSettings.cs b/src/Stratis.Features.FederatedPeg/FederatedPegSettings.cs index 8df4ab44e0..75ae7af23b 100644 --- a/src/Stratis.Features.FederatedPeg/FederatedPegSettings.cs +++ b/src/Stratis.Features.FederatedPeg/FederatedPegSettings.cs @@ -144,6 +144,8 @@ public FederatedPegSettings(NodeSettings nodeSettings, CounterChainNetworkWrappe this.MaximumPartialTransactionThreshold = configReader.GetOrDefault(MaximumPartialTransactionsParam, CrossChainTransferStore.MaximumPartialTransactions); this.WalletSyncFromHeight = configReader.GetOrDefault(WalletSyncFromHeightParam, 0); + + this.EnableMultisigMonitoring = configReader.GetOrDefault("enablemultisigmonitoring", false); } /// @@ -200,5 +202,8 @@ public FederatedPegSettings(NodeSettings nodeSettings, CounterChainNetworkWrappe /// public Script MultiSigRedeemScript { get; } + + /// + public bool EnableMultisigMonitoring { get; } } } \ No newline at end of file diff --git a/src/Stratis.Features.FederatedPeg/Interfaces/ICrossChainTransfer.cs b/src/Stratis.Features.FederatedPeg/Interfaces/ICrossChainTransfer.cs index 690bc9a7fc..58f906faf1 100644 --- a/src/Stratis.Features.FederatedPeg/Interfaces/ICrossChainTransfer.cs +++ b/src/Stratis.Features.FederatedPeg/Interfaces/ICrossChainTransfer.cs @@ -41,7 +41,7 @@ public interface ICrossChainTransfer : IBitcoinSerializable long DepositAmount { get; } /// - /// The chain A deposit height of the transaction. Is null if only seen in a block. + /// The chain A maturity deposit height of the transaction. Is null if only seen in a block. /// int? DepositHeight { get; } diff --git a/src/Stratis.Features.FederatedPeg/Interfaces/ICrossChainTransferStore.cs b/src/Stratis.Features.FederatedPeg/Interfaces/ICrossChainTransferStore.cs index 6e8775f998..acab2e1be5 100644 --- a/src/Stratis.Features.FederatedPeg/Interfaces/ICrossChainTransferStore.cs +++ b/src/Stratis.Features.FederatedPeg/Interfaces/ICrossChainTransferStore.cs @@ -2,13 +2,14 @@ using System.Collections.Generic; using System.Threading.Tasks; using NBitcoin; +using Stratis.Bitcoin.Utilities; using Stratis.Features.FederatedPeg.Models; using Stratis.Features.FederatedPeg.TargetChain; namespace Stratis.Features.FederatedPeg.Interfaces { /// Interface for interacting with the cross-chain transfer database. - public interface ICrossChainTransferStore : IDisposable + public interface ICrossChainTransferStore : ILockProtected, IDisposable { /// Initializes the cross-chain-transfer store. void Initialize(); diff --git a/src/Stratis.Features.FederatedPeg/Interfaces/IDeposit.cs b/src/Stratis.Features.FederatedPeg/Interfaces/IDeposit.cs index 1aecefea1c..70dc2860ec 100644 --- a/src/Stratis.Features.FederatedPeg/Interfaces/IDeposit.cs +++ b/src/Stratis.Features.FederatedPeg/Interfaces/IDeposit.cs @@ -35,5 +35,8 @@ public interface IDeposit /// The hash of the block where the source deposit has been persisted. [JsonConverter(typeof(UInt256JsonConverter))] uint256 BlockHash { get; } + + /// Only used for InterFlux STRAX -> wSTRAX transfers, this field is not used by the . + uint BlockTime { get; set; } } } \ No newline at end of file diff --git a/src/Stratis.Features.FederatedPeg/Interfaces/IDepositExtractor.cs b/src/Stratis.Features.FederatedPeg/Interfaces/IDepositExtractor.cs index fe734e8817..5f13cda921 100644 --- a/src/Stratis.Features.FederatedPeg/Interfaces/IDepositExtractor.cs +++ b/src/Stratis.Features.FederatedPeg/Interfaces/IDepositExtractor.cs @@ -27,7 +27,8 @@ public interface IDepositExtractor /// The transaction to extract deposits from. /// The block height of the block containing the transaction. /// The block hash of the block containing the transaction. + /// The timestamp of the block containing the transaction. /// The extracted deposit (if any), otherwise null. - Task ExtractDepositFromTransaction(Transaction transaction, int blockHeight, uint256 blockHash); + Task ExtractDepositFromTransaction(Transaction transaction, int blockHeight, uint256 blockHash, uint blockTime); } } diff --git a/src/Stratis.Features.FederatedPeg/Interfaces/IFederatedPegSettings.cs b/src/Stratis.Features.FederatedPeg/Interfaces/IFederatedPegSettings.cs index 5fea7d4ab3..0423e3344b 100644 --- a/src/Stratis.Features.FederatedPeg/Interfaces/IFederatedPegSettings.cs +++ b/src/Stratis.Features.FederatedPeg/Interfaces/IFederatedPegSettings.cs @@ -107,5 +107,10 @@ public interface IFederatedPegSettings /// Pay2Multisig redeem script. /// Script MultiSigRedeemScript { get; } + + /// + /// Enables multisig monitoring (). + /// + bool EnableMultisigMonitoring { get; } } } diff --git a/src/Stratis.Features.FederatedPeg/Interfaces/IFederationWalletManager.cs b/src/Stratis.Features.FederatedPeg/Interfaces/IFederationWalletManager.cs index 257bed3748..021916e322 100644 --- a/src/Stratis.Features.FederatedPeg/Interfaces/IFederationWalletManager.cs +++ b/src/Stratis.Features.FederatedPeg/Interfaces/IFederationWalletManager.cs @@ -97,7 +97,8 @@ public interface IFederationWalletManager : ILockProtected /// /// Saves the wallet into the file system. /// - void SaveWallet(); + /// If false (default) the wallet is not guaranteed to be saved if saved recently. + void SaveWallet(bool force = false); /// /// Gets some general information about a wallet. @@ -169,5 +170,7 @@ public interface IFederationWalletManager : ILockProtected /// /// Returns true if within blocks of the chain's tip. bool IsSyncedWithChain(); + + string GetSpendingInfo(Transaction partialTransaction); } } diff --git a/src/Stratis.Features.FederatedPeg/Models/WithdrawalModel.cs b/src/Stratis.Features.FederatedPeg/Models/WithdrawalModel.cs index 92345a863e..937a7cde09 100644 --- a/src/Stratis.Features.FederatedPeg/Models/WithdrawalModel.cs +++ b/src/Stratis.Features.FederatedPeg/Models/WithdrawalModel.cs @@ -13,6 +13,7 @@ public WithdrawalModel() { } public WithdrawalModel(Network network, ICrossChainTransfer transfer) { this.DepositId = transfer.DepositTransactionId; + this.DepositHeight = transfer.DepositHeight; this.Id = transfer.PartialTransaction?.GetHash(); this.Amount = transfer.DepositAmount; var target = transfer.DepositTargetAddress.GetDestinationAddress(network).ToString(); @@ -25,6 +26,7 @@ public WithdrawalModel(Network network, IWithdrawal withdrawal, ICrossChainTrans { this.Id = withdrawal.Id; this.DepositId = withdrawal.DepositId; + this.DepositHeight = transfer.DepositHeight; this.Amount = withdrawal.Amount; this.BlockHash = withdrawal.BlockHash; this.BlockHeight = withdrawal.BlockNumber; @@ -36,6 +38,8 @@ public WithdrawalModel(Network network, IWithdrawal withdrawal, ICrossChainTrans public uint256 DepositId { get; set; } + public int? DepositHeight { get; set; } + public Money Amount { get; set; } public string PayingTo { get; set; } @@ -54,12 +58,13 @@ public override string ToString() { var stringBuilder = new StringBuilder(); - stringBuilder.Append(string.Format("Height={0,8} Paying={1} Amount={2,14} Status={3} DepositId={4}", + stringBuilder.Append(string.Format("Height={0,8} Paying={1} Amount={2,14} Status={3} DepositId={4} MaturityHeight={5}", this.BlockHeight == 0 ? "Unconfirmed" : this.BlockHeight.ToString(), this.PayingTo.Length > RewardsString.Length ? this.PayingTo.Substring(0, RewardsString.Length) : this.PayingTo, this.Amount.ToString(), this.TransferStatus, - this.DepositId.ToString())); + this.DepositId.ToString(), + this.DepositHeight)); if (this.SpendingOutputDetails != null) stringBuilder.Append($" Spending={this.SpendingOutputDetails} "); diff --git a/src/Stratis.Features.FederatedPeg/Monitoring/MultiSigMemberStateRequestPayload.cs b/src/Stratis.Features.FederatedPeg/Monitoring/MultiSigMemberStateRequestPayload.cs new file mode 100644 index 0000000000..8487e3542e --- /dev/null +++ b/src/Stratis.Features.FederatedPeg/Monitoring/MultiSigMemberStateRequestPayload.cs @@ -0,0 +1,73 @@ +using NBitcoin; +using Stratis.Bitcoin.P2P.Protocol.Payloads; + +namespace Stratis.Features.FederatedPeg.Monitoring +{ + [Payload("msstatereqst")] + public sealed class MultiSigMemberStateRequestPayload : Payload + { + private int crossChainStoreHeight; + private int crossChainStoreNextDepositHeight; + private int partialTransactions; + private int suspendedPartialTransactions; + + private bool isRequesting; + private string memberToCheck; + private string signature; + + /// + /// True if this payload is requesting state from another multisig member. + /// False if it is replying. + /// + public bool IsRequesting { get { return this.isRequesting; } } + + public string MemberToCheck { get { return this.memberToCheck; } set { this.memberToCheck = value; } } + + public string Signature { get { return this.signature; } } + + public int CrossChainStoreHeight { get { return this.crossChainStoreHeight; } set { this.crossChainStoreHeight = value; } } + public int CrossChainStoreNextDepositHeight { get { return this.crossChainStoreNextDepositHeight; } set { this.crossChainStoreNextDepositHeight = value; } } + public int PartialTransactions { get { return this.partialTransactions; } set { this.partialTransactions = value; } } + public int SuspendedPartialTransactions { get { return this.suspendedPartialTransactions; } set { this.suspendedPartialTransactions = value; } } + + /// Parameterless constructor needed for deserialization. + public MultiSigMemberStateRequestPayload() + { + } + + private MultiSigMemberStateRequestPayload(string memberToCheck, bool isRequesting, string signature) + { + this.memberToCheck = memberToCheck; + this.isRequesting = isRequesting; + this.signature = signature; + } + + /// + public override void ReadWriteCore(BitcoinStream stream) + { + stream.ReadWrite(ref this.memberToCheck); + stream.ReadWrite(ref this.isRequesting); + stream.ReadWrite(ref this.signature); + stream.ReadWriteNullIntField(ref this.crossChainStoreHeight); + stream.ReadWriteNullIntField(ref this.crossChainStoreNextDepositHeight); + stream.ReadWriteNullIntField(ref this.partialTransactions); + stream.ReadWriteNullIntField(ref this.suspendedPartialTransactions); + } + + public static MultiSigMemberStateRequestPayload Request(string memberToCheck, string signature) + { + return new MultiSigMemberStateRequestPayload(memberToCheck, true, signature); + } + + public static MultiSigMemberStateRequestPayload Reply(string memberToCheck, string signature) + { + return new MultiSigMemberStateRequestPayload(memberToCheck, false, signature); + } + + /// + public override string ToString() + { + return $"{nameof(this.Command)}:'{this.Command}',{nameof(this.MemberToCheck)}:'{this.MemberToCheck}'"; + } + } +} \ No newline at end of file diff --git a/src/Stratis.Features.FederatedPeg/Monitoring/MultiSigStateMonitor.cs b/src/Stratis.Features.FederatedPeg/Monitoring/MultiSigStateMonitor.cs new file mode 100644 index 0000000000..5f1fcee8ae --- /dev/null +++ b/src/Stratis.Features.FederatedPeg/Monitoring/MultiSigStateMonitor.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using NBitcoin; +using NLog; +using Stratis.Bitcoin.AsyncWork; +using Stratis.Bitcoin.Features.PoA; +using Stratis.Bitcoin.Interfaces; +using Stratis.Bitcoin.Utilities; +using Stratis.Features.FederatedPeg.Distribution; +using Stratis.Features.FederatedPeg.Interfaces; +using Stratis.Features.FederatedPeg.TargetChain; + +namespace Stratis.Features.FederatedPeg.Monitoring +{ + /// + /// This class performs logic that periodically requests information from the other multisig members: + /// + /// + /// 1. Ask each member the state of their . + /// + /// Not replying to the payload within 5 minutes assumes that the member is not online or not connected to. + /// + /// + public sealed class MultiSigStateMonitor : IDisposable + { + private IAsyncLoop periodicStateRequest; + private readonly IAsyncProvider asyncProvider; + private readonly IFederationManager federationManager; + private readonly IFederatedPegBroadcaster federatedPegBroadcaster; + private readonly IFederatedPegSettings federatedPegSettings; + private readonly IInitialBlockDownloadState initialBlockDownloadState; + private readonly ILogger logger; + private readonly Network network; + private readonly INodeLifetime nodeLifetime; + + public MultiSigStateMonitor( + IAsyncProvider asyncProvider, + IFederationManager federationManager, + IFederatedPegBroadcaster federatedPegBroadcaster, + IFederatedPegSettings federatedPegSettings, + IInitialBlockDownloadState initialBlockDownloadState, + Network network, + INodeLifetime nodeLifetime) + { + this.asyncProvider = asyncProvider; + this.federationManager = federationManager; + this.federatedPegBroadcaster = federatedPegBroadcaster; + this.federatedPegSettings = federatedPegSettings; + this.initialBlockDownloadState = initialBlockDownloadState; + this.network = network; + this.nodeLifetime = nodeLifetime; + this.logger = LogManager.GetCurrentClassLogger(); + } + + /// + /// Initializes the monitor, this starts the periodic async loop that requests information from the other multisig nodes. + /// + public void Initialize() + { + if (!this.federationManager.IsFederationMember) + { + this.logger.Warn("This node is not a federation member."); + return; + } + + if(!this.federatedPegSettings.EnableMultisigMonitoring) + { + this.logger.Info("Multisig monitoring is disabled."); + return; + } + + this.periodicStateRequest = this.asyncProvider.CreateAndRunAsyncLoop(nameof(this.periodicStateRequest), async (cancellation) => + { + if (this.initialBlockDownloadState.IsInitialBlockDownload()) + return; + + try + { + var multiSigMembers = new List(); + if (this.network.IsTest()) + multiSigMembers = MultiSigMembers.InteropMultisigContractPubKeysTestNet; + else + multiSigMembers = MultiSigMembers.InteropMultisigContractPubKeysMainNet; + + // Iterate over all the multisig nodes. + foreach (PubKey multisigMember in multiSigMembers) + { + await BroadcastMultiSigStateRequestPayloadAsync(multisigMember.ToHex()).ConfigureAwait(false); + } + } + catch (Exception e) + { + this.logger.Warn("Exception occurred when requesting state from other multisig nodes: {0}", e); + } + }, + this.nodeLifetime.ApplicationStopping, + repeatEvery: TimeSpan.FromMinutes(1), + startAfter: TimeSpans.Minute); + } + + private async Task BroadcastMultiSigStateRequestPayloadAsync(string memberToCheck) + { + // Include this multsig member's pubkey as signature so that the other node can check if the request was sent from a multisig member. + string signature = this.federationManager.CurrentFederationKey.SignMessage(memberToCheck); + await this.federatedPegBroadcaster.BroadcastAsync(MultiSigMemberStateRequestPayload.Request(memberToCheck, signature)).ConfigureAwait(false); + this.logger.Info($"Requesting state from multisig member '{memberToCheck}'"); + } + + /// + public void Dispose() + { + this.periodicStateRequest?.Dispose(); + } + } +} diff --git a/src/Stratis.Features.FederatedPeg/Monitoring/MultiSigStateMonitorBehavior.cs b/src/Stratis.Features.FederatedPeg/Monitoring/MultiSigStateMonitorBehavior.cs new file mode 100644 index 0000000000..f7ec23866c --- /dev/null +++ b/src/Stratis.Features.FederatedPeg/Monitoring/MultiSigStateMonitorBehavior.cs @@ -0,0 +1,172 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NBitcoin; +using Stratis.Bitcoin.Configuration.Logging; +using Stratis.Bitcoin.EventBus.CoreEvents; +using Stratis.Bitcoin.Features.PoA; +using Stratis.Bitcoin.P2P.Peer; +using Stratis.Bitcoin.P2P.Protocol; +using Stratis.Bitcoin.P2P.Protocol.Behaviors; +using Stratis.Bitcoin.Signals; +using Stratis.Features.FederatedPeg.Interfaces; +using TracerAttributes; + +namespace Stratis.Features.FederatedPeg.Monitoring +{ + public sealed class MultiSigStateMonitorBehavior : NetworkPeerBehavior + { + private readonly ICrossChainTransferStore crossChainTransferStore; + private readonly IFederationManager federationManager; + private readonly IFederatedPegSettings federatedPegSettings; + private readonly ILogger logger; + private readonly Network network; + private readonly ISignals signals; + + public MultiSigStateMonitorBehavior( + Network network, + ICrossChainTransferStore crossChainTransferStore, + IFederationManager federationManager, + IFederatedPegSettings federatedPegSettings, + ISignals signals) + { + this.crossChainTransferStore = crossChainTransferStore; + this.federationManager = federationManager; + this.federatedPegSettings = federatedPegSettings; + this.network = network; + this.signals = signals; + + this.logger = LogManager.GetCurrentClassLogger(); + } + + /// + [NoTrace] + public override object Clone() + { + return new MultiSigStateMonitorBehavior(this.network, this.crossChainTransferStore, this.federationManager, this.federatedPegSettings, this.signals); + } + + /// + protected override void AttachCore() + { + this.logger.LogDebug($"Attaching behaviour for {this.AttachedPeer.PeerEndPoint.Address}"); + this.AttachedPeer.MessageReceived.Register(this.OnMessageReceivedAsync, true); + } + + /// + protected override void DetachCore() + { + this.logger.LogDebug($"Detaching behaviour for {this.AttachedPeer.PeerEndPoint.Address}"); + this.AttachedPeer.MessageReceived.Unregister(this.OnMessageReceivedAsync); + } + + private async Task OnMessageReceivedAsync(INetworkPeer peer, IncomingMessage message) + { + try + { + await this.ProcessMessageAsync(peer, message).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + this.logger.LogTrace("(-)[CANCELED_EXCEPTION]"); + return; + } + catch (Exception ex) + { + this.logger.LogError("Exception occurred: {0}", ex.ToString()); + throw; + } + } + + private async Task ProcessMessageAsync(INetworkPeer peer, IncomingMessage message) + { + if (!this.federationManager.IsFederationMember) + return; + + try + { + switch (message.Message.Payload) + { + case MultiSigMemberStateRequestPayload payload: + await this.ProcessMultiSigMemberStateRequestAsync(peer, payload).ConfigureAwait(false); + break; + } + } + catch (OperationCanceledException) + { + this.logger.LogTrace("(-)[CANCELED_EXCEPTION]"); + } + } + + private async Task ProcessMultiSigMemberStateRequestAsync(INetworkPeer peer, MultiSigMemberStateRequestPayload payload) + { + this.logger.LogDebug($"State request payload received for member '{payload.MemberToCheck}' from '{peer.PeerEndPoint.Address}':'{peer.RemoteSocketEndpoint.Address}'."); + + // Check that the payload is signed by a multisig federation member. + PubKey pubKey = RecoverPubKeyAndValidateMultiSigMember(payload.MemberToCheck, payload.Signature); + if (pubKey == null) + return; + + if (payload.IsRequesting) + { + // Execute a small delay to prevent network congestion (this should be less than 1 minute) + await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false); + + string signature = this.federationManager.CurrentFederationKey.SignMessage(this.federationManager.CurrentFederationKey.PubKey.ToHex()); + var reply = MultiSigMemberStateRequestPayload.Reply(this.federationManager.CurrentFederationKey.PubKey.ToHex(), signature); + reply.CrossChainStoreHeight = this.crossChainTransferStore.TipHashAndHeight.Height; + reply.CrossChainStoreNextDepositHeight = this.crossChainTransferStore.NextMatureDepositHeight; + reply.PartialTransactions = this.crossChainTransferStore.GetTransfersByStatus(new[] { CrossChainTransferStatus.Partial }).Length; + reply.SuspendedPartialTransactions = this.crossChainTransferStore.GetTransfersByStatus(new[] { CrossChainTransferStatus.Suspended }).Length; + + await this.AttachedPeer.SendMessageAsync(reply).ConfigureAwait(false); + } + else + { + // Only raise SignalR events if multisig monitoring is enabled. + if (!this.federatedPegSettings.EnableMultisigMonitoring) + return; + + // Publish the results + this.signals.Publish(new MultiSigMemberStateRequestEvent() + { + PubKey = pubKey.ToHex(), + + CrossChainStoreHeight = payload.CrossChainStoreHeight, + CrossChainStoreNextDepositHeight = payload.CrossChainStoreNextDepositHeight, + PartialTransactions = payload.PartialTransactions, + SuspendedPartialTransactions = payload.SuspendedPartialTransactions, + }); + } + } + + /// + /// Check that the payload is signed by a multisig federation member. + /// + /// The signature test to verify. + /// The signature to verify against. + /// A valid if signed by a multisig member. + private PubKey RecoverPubKeyAndValidateMultiSigMember(string signatureText, string signature) + { + PubKey pubKey; + + try + { + pubKey = PubKey.RecoverFromMessage(signatureText, signature); + + if (!this.federationManager.IsMultisigMember(pubKey)) + { + this.logger.LogWarning($"'{pubKey?.ToHex()}' is not a multisig member."); + return null; + } + } + catch (Exception) + { + this.logger.LogWarning($"Received malformed payload signature for member '{signatureText}'."); + return null; + } + + return pubKey; + } + } +} diff --git a/src/Stratis.Features.FederatedPeg/SourceChain/Deposit.cs b/src/Stratis.Features.FederatedPeg/SourceChain/Deposit.cs index d2b4776e1d..29bf52b461 100644 --- a/src/Stratis.Features.FederatedPeg/SourceChain/Deposit.cs +++ b/src/Stratis.Features.FederatedPeg/SourceChain/Deposit.cs @@ -36,6 +36,8 @@ public Deposit(uint256 id, DepositRetrievalType retrievalType, Money amount, str /// public uint256 BlockHash { get; set; } + public uint BlockTime { get; set; } + /// public DepositRetrievalType RetrievalType { get; } diff --git a/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs b/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs index 4b6178ff81..4235a3d8d3 100644 --- a/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs +++ b/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs @@ -11,7 +11,6 @@ using Stratis.Bitcoin.Interfaces; using Stratis.Features.FederatedPeg.Conversion; using Stratis.Features.FederatedPeg.Interfaces; -using Stratis.SmartContracts; using Block = NBitcoin.Block; namespace Stratis.Features.FederatedPeg.SourceChain @@ -97,7 +96,7 @@ public async Task> ExtractDepositsFromBlock(Block block, foreach (Transaction transaction in block.Transactions) { - IDeposit deposit = await this.ExtractDepositFromTransaction(transaction, blockHeight, blockHash).ConfigureAwait(false); + IDeposit deposit = await this.ExtractDepositFromTransaction(transaction, blockHeight, blockHash, block.Header.Time).ConfigureAwait(false); if (deposit == null) continue; @@ -218,7 +217,7 @@ private Deposit CreateDeposit(ConversionRequest burnRequest, int inspectForDepos } /// - public Task ExtractDepositFromTransaction(Transaction transaction, int blockHeight, uint256 blockHash) + public Task ExtractDepositFromTransaction(Transaction transaction, int blockHeight, uint256 blockHash, uint blockTime) { // If there are no deposits to the multsig (i.e. cross chain transfers) do nothing. if (!DepositValidationHelper.TryGetDepositsToMultisig(this.network, transaction, FederatedPegSettings.CrossChainTransferMinimum, out List depositsToMultisig)) @@ -239,7 +238,11 @@ public Task ExtractDepositFromTransaction(Transaction transaction, int if (utxo.ScriptPubKey == StraxCoinstakeRule.CirrusRewardScript) { - return Task.FromResult((IDeposit)new Deposit(transaction.GetHash(), DepositRetrievalType.Distribution, amount, this.network.CirrusRewardDummyAddress, DestinationChain.STRAX, blockHeight, blockHash)); + IDeposit rewardDeposit = new Deposit(transaction.GetHash(), DepositRetrievalType.Distribution, amount, this.network.CirrusRewardDummyAddress, DestinationChain.STRAX, blockHeight, blockHash); + + rewardDeposit.BlockTime = blockTime; + + return Task.FromResult(rewardDeposit); } } } @@ -276,7 +279,11 @@ public Task ExtractDepositFromTransaction(Transaction transaction, int } } - return Task.FromResult((IDeposit)new Deposit(transaction.GetHash(), depositRetrievalType, amount, targetAddress, (DestinationChain)targetChain, blockHeight, blockHash)); + IDeposit deposit = new Deposit(transaction.GetHash(), depositRetrievalType, amount, targetAddress, (DestinationChain)targetChain, blockHeight, blockHash); + + deposit.BlockTime = blockTime; + + return Task.FromResult(deposit); } private DepositRetrievalType DetermineDepositRetrievalType(ulong satoshiAmount) diff --git a/src/Stratis.Features.FederatedPeg/Stratis.Features.FederatedPeg.csproj b/src/Stratis.Features.FederatedPeg/Stratis.Features.FederatedPeg.csproj index c5c1a3ef2b..1a87992a65 100644 --- a/src/Stratis.Features.FederatedPeg/Stratis.Features.FederatedPeg.csproj +++ b/src/Stratis.Features.FederatedPeg/Stratis.Features.FederatedPeg.csproj @@ -8,11 +8,11 @@ - netcoreapp3.1 + net6.0 Full ..\None.ruleset Stratis Group Ltd. - 4.0.9.3 + 4.0.11.0 diff --git a/src/Stratis.Features.FederatedPeg/TargetChain/CrossChainTransferStore.cs b/src/Stratis.Features.FederatedPeg/TargetChain/CrossChainTransferStore.cs index 44b4b61f9d..d3de4dce57 100644 --- a/src/Stratis.Features.FederatedPeg/TargetChain/CrossChainTransferStore.cs +++ b/src/Stratis.Features.FederatedPeg/TargetChain/CrossChainTransferStore.cs @@ -12,10 +12,12 @@ using NBitcoin; using Stratis.Bitcoin.Configuration; using Stratis.Bitcoin.Configuration.Logging; +using Stratis.Bitcoin.Controllers.Models; using Stratis.Bitcoin.Features.BlockStore; using Stratis.Bitcoin.Features.MemoryPool; using Stratis.Bitcoin.Signals; using Stratis.Bitcoin.Utilities; +using Stratis.Features.FederatedPeg.Controllers; using Stratis.Features.FederatedPeg.Events; using Stratis.Features.FederatedPeg.Exceptions; using Stratis.Features.FederatedPeg.Interfaces; @@ -25,12 +27,12 @@ namespace Stratis.Features.FederatedPeg.TargetChain { - public sealed class CrossChainTransferStore : ICrossChainTransferStore + public sealed class CrossChainTransferStore : LockProtected, ICrossChainTransferStore { /// /// Given that we can have up to 10 UTXOs going at once. /// - private const int TransfersToDisplay = 10; + private const int TransfersToDisplay = 20; /// /// Maximum number of partial transactions. @@ -86,9 +88,6 @@ public sealed class CrossChainTransferStore : ICrossChainTransferStore private readonly IWithdrawalHistoryProvider withdrawalHistoryProvider; private readonly IWithdrawalTransactionBuilder withdrawalTransactionBuilder; - /// Provider of time functions. - private readonly object lockObj; - public CrossChainTransferStore(Network network, INodeStats nodeStats, DataFolder dataFolder, ChainIndexer chainIndexer, IFederatedPegSettings settings, IDateTimeProvider dateTimeProvider, IWithdrawalExtractor withdrawalExtractor, IWithdrawalHistoryProvider withdrawalHistoryProvider, IBlockRepository blockRepository, IFederationWalletManager federationWalletManager, IWithdrawalTransactionBuilder withdrawalTransactionBuilder, DBreezeSerializer dBreezeSerializer, ISignals signals, IStateRepositoryRoot stateRepositoryRoot = null) @@ -113,7 +112,6 @@ public CrossChainTransferStore(Network network, INodeStats nodeStats, DataFolder this.blockRepository = blockRepository; this.federationWalletManager = federationWalletManager; this.dBreezeSerializer = dBreezeSerializer; - this.lockObj = new object(); this.logger = LogManager.GetCurrentClassLogger(); this.TipHashAndHeight = this.chainIndexer.GetHeader(0); this.NextMatureDepositHeight = 1; @@ -141,7 +139,7 @@ public CrossChainTransferStore(Network network, INodeStats nodeStats, DataFolder /// Performs any needed initialisation for the database. public void Initialize() { - lock (this.lockObj) + lock (this.lockObject) { using (DBreeze.Transactions.Transaction dbreezeTransaction = this.DBreeze.GetTransaction()) { @@ -177,7 +175,7 @@ public void Initialize() /// Starts the cross-chain-transfer store. public void Start() { - lock (this.lockObj) + lock (this.lockObject) { this.federationWalletManager.Synchronous(() => { @@ -189,7 +187,7 @@ public void Start() /// public bool HasSuspended() { - lock (this.lockObj) + lock (this.lockObject) { return this.depositsIdsByStatus[CrossChainTransferStatus.Suspended].Count != 0; } @@ -325,7 +323,7 @@ public Task SaveCurrentTipAsync() { return Task.Run(() => { - lock (this.lockObj) + lock (this.lockObject) { using (DBreeze.Transactions.Transaction dbreezeTransaction = this.DBreeze.GetTransaction()) { @@ -342,7 +340,7 @@ public void RejectTransfer(ICrossChainTransfer crossChainTransfer) { Guard.Assert(crossChainTransfer.Status == CrossChainTransferStatus.FullySigned); - lock (this.lockObj) + lock (this.lockObject) { var tracker = new StatusChangeTracker(); @@ -389,7 +387,7 @@ public Task RecordLatestMatureDepositsAsync(IL return Task.Run(() => { - lock (this.lockObj) + lock (this.lockObject) { int originalDepositHeight = this.NextMatureDepositHeight; @@ -431,6 +429,11 @@ public Task RecordLatestMatureDepositsAsync(IL this.logger.LogInformation($"{maturedBlockDeposits.Count} blocks received, containing a total of {maturedBlockDeposits.SelectMany(d => d.Deposits).Where(a => a.Amount > 0).Count()} deposits."); this.logger.LogInformation($"Block Range : {maturedBlockDeposits.Min(a => a.BlockInfo.BlockHeight)} to {maturedBlockDeposits.Max(a => a.BlockInfo.BlockHeight)}."); + // Deposits are assumed to be in order of occurrence on the source chain. + // If we fail to build a transaction the transfer and subsequent transfers + // in the ordered list will be set to suspended. + bool haveSuspendedTransfers = false; + foreach (MaturedBlockDepositsModel maturedDeposit in maturedBlockDeposits) { if (maturedDeposit.BlockInfo.BlockHeight != this.NextMatureDepositHeight) @@ -454,13 +457,17 @@ public Task RecordLatestMatureDepositsAsync(IL var tracker = new StatusChangeTracker(); bool walletUpdated = false; - // Deposits are assumed to be in order of occurrence on the source chain. - // If we fail to build a transaction the transfer and subsequent transfers - // in the ordered list will be set to suspended. - bool haveSuspendedTransfers = false; - for (int i = 0; i < deposits.Count; i++) { + // Can't have any transfers after a suspended transfer. + // Otherwise it will be impossible to allocate the correct inputs in deterministic order. + if (haveSuspendedTransfers && transfers[i]?.Status == CrossChainTransferStatus.Partial) + { + this.federationWalletManager.RemoveWithdrawalTransactions(transfers[i].DepositTransactionId); + // "ValidateTransfers" will detect the missing transaction and suspend this transfer. + continue; + } + if (transfers[i] != null && transfers[i].Status != CrossChainTransferStatus.Suspended) continue; @@ -470,7 +477,7 @@ public Task RecordLatestMatureDepositsAsync(IL // Log the target address in the event that it fails. this.logger.LogDebug($"Attempting to create script pubkey from target address '{deposit.TargetAddress}'."); - Script scriptPubKey = BitcoinAddress.Create(deposit.TargetAddress, this.network).ScriptPubKey; + NBitcoin.Script scriptPubKey = BitcoinAddress.Create(deposit.TargetAddress, this.network).ScriptPubKey; if (!haveSuspendedTransfers) { @@ -621,7 +628,7 @@ public Transaction MergeTransactionSignatures(uint256 depositId, Transaction[] p Guard.NotNull(depositId, nameof(depositId)); Guard.NotNull(partialTransactions, nameof(partialTransactions)); - lock (this.lockObj) + lock (this.lockObject) { return this.federationWalletManager.Synchronous(() => { @@ -815,7 +822,7 @@ private void Put(List blocks, Dictionary chainedH if (crossChainTransfers[i] == null) { - Script scriptPubKey = BitcoinAddress.Create(withdrawal.TargetAddress, this.network).ScriptPubKey; + NBitcoin.Script scriptPubKey = BitcoinAddress.Create(withdrawal.TargetAddress, this.network).ScriptPubKey; crossChainTransfers[i] = new CrossChainTransfer(CrossChainTransferStatus.SeenInBlock, withdrawal.DepositId, scriptPubKey, withdrawal.Amount, null, transaction, withdrawal.BlockHash, withdrawal.BlockNumber); @@ -952,7 +959,7 @@ private void RewindIfRequiredLocked() /// Returns true if the store is in sync or false otherwise. private bool Synchronize() { - lock (this.lockObj) + lock (this.lockObject) { if (this.TipHashAndHeight == null) { @@ -1142,7 +1149,7 @@ public Task GetAsync(uint256[] depositIds, bool validate { return Task.Run(() => { - lock (this.lockObj) + lock (this.lockObject) { return this.federationWalletManager.Synchronous(() => { @@ -1209,7 +1216,7 @@ private OutPoint EarliestOutput(Transaction transaction) /// public ICrossChainTransfer[] GetTransfersByStatus(CrossChainTransferStatus[] statuses, bool sort = false, bool validate = true) { - lock (this.lockObj) + lock (this.lockObject) { return this.federationWalletManager.Synchronous(() => { @@ -1407,7 +1414,7 @@ public List GetCompletedWithdrawalsForTransactions(IEnumerable(); - lock (this.lockObj) + lock (this.lockObject) { HashSet inProgress = this.depositsIdsByStatus[CrossChainTransferStatus.Partial].Union( this.depositsIdsByStatus[CrossChainTransferStatus.FullySigned].Union( @@ -1464,7 +1471,7 @@ private void AddComponentStats(StringBuilder benchLog) try { - foreach (CrossChainTransferStatus status in new[] { CrossChainTransferStatus.FullySigned, CrossChainTransferStatus.Partial }) + foreach (CrossChainTransferStatus status in new[] { CrossChainTransferStatus.FullySigned, CrossChainTransferStatus.Partial, CrossChainTransferStatus.Suspended }) depositIds.UnionWith(this.depositsIdsByStatus[status]); transfers = this.Get(depositIds.ToArray()).Where(t => t != null).ToArray(); @@ -1473,7 +1480,7 @@ private void AddComponentStats(StringBuilder benchLog) IEnumerable inprogress = transfers.Where(x => x.Status != CrossChainTransferStatus.Suspended && x.Status != CrossChainTransferStatus.Rejected); IEnumerable suspended = transfers.Where(x => x.Status == CrossChainTransferStatus.Suspended || x.Status == CrossChainTransferStatus.Rejected); - IEnumerable pendingWithdrawals = this.withdrawalHistoryProvider.GetPendingWithdrawals(inprogress.Concat(suspended)).OrderByDescending(p => p.SignatureCount); + IEnumerable pendingWithdrawals = this.withdrawalHistoryProvider.GetPendingWithdrawals(inprogress.Concat(suspended)).OrderBy(p => p.DepositHeight); if (pendingWithdrawals.Count() > 0) { diff --git a/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs b/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs index aa9c0f3404..7453f8eda3 100644 --- a/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs +++ b/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs @@ -7,6 +7,7 @@ using NBitcoin; using Stratis.Bitcoin.AsyncWork; using Stratis.Bitcoin.Configuration.Logging; +using Stratis.Bitcoin.Controllers.Models; using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Features.PoA; using Stratis.Bitcoin.Interfaces; @@ -63,7 +64,7 @@ public class MaturedBlocksSyncManager : IMaturedBlocksSyncManager private readonly IConversionRequestFeeService conversionRequestFeeService; private readonly Network network; private readonly IFederationManager federationManager; - private int mainChainActivationHeight; + private readonly Dictionary blockTimeOfDeposit; private IAsyncLoop requestDepositsTask; /// When we are fully synced we stop asking for more blocks for this amount of time. @@ -112,7 +113,7 @@ public MaturedBlocksSyncManager( this.conversionRequestFeeService = conversionRequestFeeService; this.network = network; this.federationManager = federationManager; - this.mainChainActivationHeight = int.MaxValue; + this.blockTimeOfDeposit = new Dictionary(); this.lockObject = new object(); this.logger = LogManager.GetCurrentClassLogger(); @@ -216,6 +217,16 @@ private async Task ProcessMatureBlockDepositsAsync(SerializableResult b.Deposits).Select(d => d.Id).ToArray()).ConfigureAwait(false); + + // The sync must be successful. Otherwise try again later. + if (existingTransfers == null) + return true; + + var existingTransfersDict = existingTransfers.Where(t => t != null).ToDictionary(t => t.DepositTransactionId, t => t); + var completedTransferIds = new HashSet(existingTransfers.Where(t => t?.Status == CrossChainTransferStatus.SeenInBlock).Select(t => t.DepositTransactionId)); + // Filter out conversion transactions & also log what we've received for diagnostic purposes. foreach (MaturedBlockDepositsModel maturedBlockDeposit in matureBlockDeposits.Value) { @@ -226,6 +237,10 @@ private async Task ProcessMatureBlockDepositsAsync(SerializableResult ProcessMatureBlockDepositsAsync(SerializableResult ProcessMatureBlockDepositsAsync(SerializableResult ProcessMatureBlockDepositsAsync(SerializableResult /// Get the first block on this chain that has a timestamp after the deposit's block time on the counterchain. /// This is so that we can assign a block height that the deposit 'arrived' on the sidechain. - /// TODO: This can probably be made more efficient than looping every time. /// - /// The matured block deposit's block time to check against. /// The conversion transaction we are currently working with. /// The chained header to use. /// true if found. - private bool FindApplicableConversionRequestHeader(MaturedBlockDepositsModel maturedBlockDeposit, IDeposit potentialConversionTransaction, out ChainedHeader chainedHeader) + private bool FindApplicableConversionRequestHeader(IDeposit potentialConversionTransaction, out ChainedHeader chainedHeader) { chainedHeader = this.chainIndexer.Tip; - bool found = false; - - this.logger.LogDebug($"Finding applicable header for deposit with block time '{maturedBlockDeposit.BlockInfo.BlockTime}'; chain tip '{this.chainIndexer.Tip}'."); + this.logger.LogDebug($"Finding applicable header for deposit with block time '{potentialConversionTransaction.BlockTime}'; chain tip '{this.chainIndexer.Tip}'."); - while (true) + // Get the first block on this chain that has a timestamp after the deposit's block time on the counterchain. + // Note that it is the block time of the block the deposit was made in, not the block time of the block it matured in (as this may be too close to this chain's tip to be reliably found). + int chainedHeaderHeight = BinarySearch.BinaryFindFirst((height) => { - if (chainedHeader == this.chainIndexer.Genesis) - break; + return this.chainIndexer[height].Header.Time > potentialConversionTransaction.BlockTime; + }, 0, this.chainIndexer.Tip.Height + 1); - if (chainedHeader.Previous.Header.Time <= maturedBlockDeposit.BlockInfo.BlockTime) - { - found = true; - break; - } - - chainedHeader = chainedHeader.Previous; + if (chainedHeaderHeight == -1) + { + this.logger.LogWarning("Unable to determine timestamp for conversion transaction '{0}', ignoring.", potentialConversionTransaction.Id); + return false; } - if (!found) - this.logger.LogWarning("Unable to determine timestamp for conversion transaction '{0}', ignoring.", potentialConversionTransaction.Id); + chainedHeader = this.chainIndexer[chainedHeaderHeight]; this.logger.LogDebug($"Applicable header selected '{chainedHeader}'"); - return found; + return true; } /// diff --git a/src/Stratis.Features.FederatedPeg/TargetChain/MempoolCleaner.cs b/src/Stratis.Features.FederatedPeg/TargetChain/MempoolCleaner.cs index 7ea2a8b4ab..1568c8d92a 100644 --- a/src/Stratis.Features.FederatedPeg/TargetChain/MempoolCleaner.cs +++ b/src/Stratis.Features.FederatedPeg/TargetChain/MempoolCleaner.cs @@ -112,23 +112,27 @@ private Task CleanMempoolAsync() if (transactionsToCheck.Count == 0) return Task.CompletedTask; - List completedTransactions = null; - - this.federationWalletManager.Synchronous(() => + this.store.Synchronous(() => { - completedTransactions = this.CompletedTransactions(transactionsToCheck).ToList(); - }); + List completedTransactions = null; - List transactionsToRemove = this.store.GetCompletedWithdrawalsForTransactions(transactionsToCheck) - .Union(completedTransactions) - .ToList(); + this.federationWalletManager.Synchronous(() => + { + completedTransactions = this.CompletedTransactions(transactionsToCheck).ToList(); + }); - if (transactionsToRemove.Count > 0) - { - this.mempoolOrphans.RemoveForBlock(transactionsToRemove); - this.logger.LogDebug("Removed {0} transactions from mempool", transactionsToRemove.Count); - } + List transactionsToRemove = this.store.GetCompletedWithdrawalsForTransactions(transactionsToCheck) + .Union(completedTransactions) + .ToList(); + + if (transactionsToRemove.Count > 0) + { + this.mempoolOrphans.RemoveForBlock(transactionsToRemove); + + this.logger.LogDebug("Removed {0} transactions from mempool", transactionsToRemove.Count); + } + }); return Task.CompletedTask; } diff --git a/src/Stratis.Features.FederatedPeg/TargetChain/WithdrawalHistoryProvider.cs b/src/Stratis.Features.FederatedPeg/TargetChain/WithdrawalHistoryProvider.cs index a88f8bddff..e208feb029 100644 --- a/src/Stratis.Features.FederatedPeg/TargetChain/WithdrawalHistoryProvider.cs +++ b/src/Stratis.Features.FederatedPeg/TargetChain/WithdrawalHistoryProvider.cs @@ -3,6 +3,7 @@ using NBitcoin; using Stratis.Bitcoin; using Stratis.Bitcoin.Features.MemoryPool; +using Stratis.Bitcoin.Features.Wallet; using Stratis.Features.Collateral.CounterChain; using Stratis.Features.FederatedPeg.Conversion; using Stratis.Features.FederatedPeg.Interfaces; @@ -22,6 +23,7 @@ public class WithdrawalHistoryProvider : IWithdrawalHistoryProvider private readonly MempoolManager mempoolManager; private readonly Network network; private readonly IWithdrawalExtractor withdrawalExtractor; + private readonly IFederationWalletManager federationWalletManager; /// /// The constructor. @@ -31,17 +33,20 @@ public class WithdrawalHistoryProvider : IWithdrawalHistoryProvider /// /// Repository containing all cross-network mint and burn transactions. /// Mempool which provides information about transactions in the mempool. /// Counter chain network. + /// Federation wallet manager. public WithdrawalHistoryProvider( Network network, IFederatedPegSettings federatedPegSettings, IConversionRequestRepository conversionRequestRepository, MempoolManager mempoolManager, - CounterChainNetworkWrapper counterChainNetworkWrapper) + CounterChainNetworkWrapper counterChainNetworkWrapper, + IFederationWalletManager federationWalletManager) { this.network = network; this.federatedPegSettings = federatedPegSettings; this.withdrawalExtractor = new WithdrawalExtractor(federatedPegSettings, new OpReturnDataReader(counterChainNetworkWrapper.CounterChainNetwork), network); this.mempoolManager = mempoolManager; + this.federationWalletManager = federationWalletManager; } /// @@ -106,14 +111,7 @@ public List GetPendingWithdrawals(IEnumerable @@ -87,19 +84,17 @@ public Transaction BuildWithdrawalTransaction(int blockHeight, uint256 depositId // Withdrawals from the sidechain won't have the OP_RETURN transaction tag, so we need to check against the ScriptPubKey of the Cirrus Dummy address. if (!this.federatedPegSettings.IsMainChain && recipient.ScriptPubKey.Length > 0) { - if (recipient.ScriptPubKey == this.cirrusRewardDummyAddressScriptPubKey && this.previousDistributionHeight != blockHeight) + if (recipient.ScriptPubKey == this.cirrusRewardDummyAddressScriptPubKey) { // Use the distribution manager to determine the actual list of recipients. - // TODO: This would probably be neater if it was moved to the CCTS with the current method accepting a list of recipients instead + this.logger.LogDebug("Generating recipient list for reward distribution."); + multiSigContext.Recipients = this.distributionManager.Distribute(blockHeight, recipient.WithPaymentReducedByFee(FederatedPegSettings.CrossChainTransferFee).Amount); // Reduce the overall amount by the fee first before splitting it up. - - // This can be transient as it is just to stop distribution happening multiple times - // on blocks that contain more than one deposit. - this.previousDistributionHeight = blockHeight; } if (recipient.ScriptPubKey == this.conversionTransactionFeeDistributionScriptPubKey) { + // Use the distribution manager to determine the actual list of recipients. this.logger.LogDebug("Generating recipient list for conversion transaction fee distribution."); multiSigContext.Recipients = this.distributionManager.DistributeToMultisigNodes(depositId, recipient.WithPaymentReducedByFee(FederatedPegSettings.CrossChainTransferFee).Amount); diff --git a/src/Stratis.Features.FederatedPeg/Wallet/FederationWalletManager.cs b/src/Stratis.Features.FederatedPeg/Wallet/FederationWalletManager.cs index 35ab18c06a..6b5447c2d2 100644 --- a/src/Stratis.Features.FederatedPeg/Wallet/FederationWalletManager.cs +++ b/src/Stratis.Features.FederatedPeg/Wallet/FederationWalletManager.cs @@ -10,9 +10,11 @@ using Stratis.Bitcoin.AsyncWork; using Stratis.Bitcoin.Configuration; using Stratis.Bitcoin.Configuration.Logging; +using Stratis.Bitcoin.EventBus.CoreEvents; using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.Features.Wallet.Interfaces; using Stratis.Bitcoin.Interfaces; +using Stratis.Bitcoin.Signals; using Stratis.Bitcoin.Utilities; using Stratis.Features.FederatedPeg.Controllers; using Stratis.Features.FederatedPeg.Interfaces; @@ -59,11 +61,18 @@ public class FederationWalletManager : LockProtected, IFederationWalletManager /// Timer for saving wallet files to the file system. private const int WalletSavetimeIntervalInMinutes = 5; + /// Minimum time to wait between saving wallet if not forced. + private const int WalletSavetimeMinIntervalInMinutes = 1; + /// Keep at least this many transactions in the wallet despite the /// max reorg age limit for spent transactions. This is so that it never /// looks like the wallet has become empty to the user. private const int MinimumRetainedTransactions = 100; + + /// The last time the wallet was saved. + private DateTime lastWalletSave; + /// The async loop we need to wait upon before we can shut down this manager. private IAsyncLoop asyncLoop; @@ -99,6 +108,8 @@ public class FederationWalletManager : LockProtected, IFederationWalletManager private readonly IBlockStore blockStore; + private readonly ISignals signals; + /// Indicates whether the federation is active. private bool isFederationActive; @@ -138,7 +149,8 @@ public FederationWalletManager( IDateTimeProvider dateTimeProvider, IFederatedPegSettings federatedPegSettings, IWithdrawalExtractor withdrawalExtractor, - IBlockStore blockStore) : base() + IBlockStore blockStore, + ISignals signals) : base() { Guard.NotNull(network, nameof(network)); Guard.NotNull(chainIndexer, nameof(chainIndexer)); @@ -164,6 +176,8 @@ public FederationWalletManager( this.withdrawalExtractor = withdrawalExtractor; this.isFederationActive = false; this.blockStore = blockStore; + this.signals = signals; + this.lastWalletSave = DateTime.Now; nodeStats.RegisterStats(this.AddComponentStats, StatsType.Component, this.GetType().Name); nodeStats.RegisterStats(this.AddInlineStats, StatsType.Inline, this.GetType().Name, 800); @@ -266,6 +280,7 @@ public void Start() if (this.fileStorage.Exists(WalletFileName)) { this.Wallet = this.fileStorage.LoadByFileName(WalletFileName); + Guard.Assert(this.Wallet.MultiSigAddress.Address == this.federatedPegSettings.MultiSigAddress.ToString()); this.RemoveUnconfirmedTransactionData(); } else @@ -283,7 +298,7 @@ public void Start() // save the wallets file every 5 minutes to help against crashes. this.asyncLoop = this.asyncProvider.CreateAndRunAsyncLoop("wallet persist job", token => { - this.SaveWallet(); + this.SaveWallet(true); this.logger.LogInformation("Wallets saved to file at {0}.", this.dateTimeProvider.GetUtcNow()); return Task.CompletedTask; @@ -300,7 +315,7 @@ public void Stop() lock (this.lockObject) { this.asyncLoop?.Dispose(); - this.SaveWallet(); + this.SaveWallet(true); } } @@ -476,9 +491,25 @@ public bool ProcessTransaction(Transaction transaction, int? blockHeight = null, IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(transaction, blockHash, blockHeight ?? 0); if (withdrawal != null) { - // Exit if already present and included in a block. + // Check the wallet for any existing transactions related to this deposit id. List<(Transaction transaction, IWithdrawal withdrawal)> walletData = this.FindWithdrawalTransactions(withdrawal.DepositId); - if ((walletData.Count == 1) && (walletData[0].withdrawal.BlockNumber != 0)) + + // Already redeemed in a confirmed block? + if (walletData.Count != 0 && blockHeight != null) + { + (_, IWithdrawal existingWithdrawal) = walletData.LastOrDefault(d => d.withdrawal.BlockNumber != 0 && d.withdrawal.Id != withdrawal.Id); + + if (existingWithdrawal != null) + { + string error = string.Format("Deposit '{0}' last redeemed in transaction '{1}' (height {2}) and then redeemed again in '{3}' (height {4})!.", withdrawal.DepositId, existingWithdrawal.Id, existingWithdrawal.BlockNumber, withdrawal.Id, withdrawal.BlockNumber); + + // Just display an error on the console for now. + this.logger.LogError(error); + + // Fall through and keep the latest withdrawal. + } + } + else if ((walletData.Count == 1) && (walletData[0].withdrawal.BlockNumber != 0)) { this.logger.LogDebug("Deposit '{0}' already included in block.", withdrawal.DepositId); return false; @@ -865,16 +896,18 @@ private SpendingDetails BuildSpendingDetails(Transaction transaction, } /// - public void SaveWallet() + public void SaveWallet(bool force = false) { lock (this.lockObject) { + // If this is not a forced save then check that we're not saving the wallet too often. + if (!force && ((DateTime.Now - this.lastWalletSave) < TimeSpan.FromMinutes(WalletSavetimeMinIntervalInMinutes))) + return; + if (this.Wallet != null) { - lock (this.lockObject) - { - this.fileStorage.SaveToFile(this.Wallet, WalletFileName); - } + this.fileStorage.SaveToFile(this.Wallet, WalletFileName); + this.lastWalletSave = DateTime.Now; } } } @@ -922,7 +955,8 @@ public bool RemoveWithdrawalTransactions(uint256 depositId) foreach ((Transaction transaction, IWithdrawal withdrawal) in this.FindWithdrawalTransactions(depositId)) { - walletUpdated |= this.RemoveTransaction(transaction); + if (withdrawal.BlockNumber == 0) + walletUpdated |= this.RemoveTransaction(transaction); } return walletUpdated; @@ -1112,6 +1146,22 @@ public ValidateTransactionResult ValidateTransaction(Transaction transaction, bo } } + + public string GetSpendingInfo(Transaction partialTransaction) + { + string ret = ""; + + foreach (TxIn input in partialTransaction.Inputs) + { + if (this.outpointLookup.TryGetValue(input.PrevOut, out TransactionData transactionData)) + ret += transactionData.BlockHeight + "-"; + + ret += input.PrevOut.Hash.ToString().Substring(0, 6) + "-" + input.PrevOut.N + ","; + } + + return ret; + } + /// public bool ValidateConsolidatingTransaction(Transaction transaction, bool checkSignature = false) { @@ -1355,6 +1405,8 @@ private void AddComponentStats(StringBuilder benchLog) benchLog.AppendLine("".PadRight(133, '=')); benchLog.AppendLine(); } + + this.signals.Publish(new FederationWalletStatusEvent(ConfirmedAmount, UnConfirmedAmount)); } } } \ No newline at end of file diff --git a/src/Stratis.Features.FederatedPeg/Wallet/FederationWalletSyncManager.cs b/src/Stratis.Features.FederatedPeg/Wallet/FederationWalletSyncManager.cs index 9ad936ef3f..5d25785827 100644 --- a/src/Stratis.Features.FederatedPeg/Wallet/FederationWalletSyncManager.cs +++ b/src/Stratis.Features.FederatedPeg/Wallet/FederationWalletSyncManager.cs @@ -89,7 +89,7 @@ public void Initialize() // state (behind the best chain). ICollection locators = this.federationWalletManager.GetWallet().BlockLocator; var blockLocator = new BlockLocator { Blocks = locators.ToList() }; - ChainedHeader fork = this.chain.FindFork(blockLocator); + ChainedHeader fork = this.chain.FindFork(blockLocator) ?? this.chain.Genesis; this.federationWalletManager.RemoveBlocks(fork); this.federationWalletManager.WalletTipHash = fork.HashBlock; this.walletTip = fork; diff --git a/src/Stratis.Features.FederatedPeg/Wallet/MultiSigTransactions.cs b/src/Stratis.Features.FederatedPeg/Wallet/MultiSigTransactions.cs index 58fca5f22e..ecfbde7686 100644 --- a/src/Stratis.Features.FederatedPeg/Wallet/MultiSigTransactions.cs +++ b/src/Stratis.Features.FederatedPeg/Wallet/MultiSigTransactions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NBitcoin; +using Org.BouncyCastle.Crypto.Parameters; using Stratis.Bitcoin.Utilities; using TracerAttributes; @@ -31,6 +32,18 @@ public MultiSigTransactions() : base() this.spentTransactionsByHeightDict = new SortedDictionary>(); } + public IEnumerable GetLastWithdrawals() + { + foreach (int height in this.spentTransactionsByHeightDict.Keys.Reverse()) + { + foreach (TransactionData transactionData in this.spentTransactionsByHeightDict[height]) + { + if (transactionData.SpendingDetails?.WithdrawalDetails?.MatchingDepositId != null) + yield return transactionData.SpendingDetails.WithdrawalDetails; + } + } + } + private void AddWithdrawal(TransactionData transactionData) { uint256 matchingDepositId = transactionData.SpendingDetails?.WithdrawalDetails?.MatchingDepositId; diff --git a/src/Stratis.Features.SQLiteWalletRepository.Tests/Stratis.Features.SQLiteWalletRepository.Tests.csproj b/src/Stratis.Features.SQLiteWalletRepository.Tests/Stratis.Features.SQLiteWalletRepository.Tests.csproj index 84738d1b51..91ca8fbb89 100644 --- a/src/Stratis.Features.SQLiteWalletRepository.Tests/Stratis.Features.SQLiteWalletRepository.Tests.csproj +++ b/src/Stratis.Features.SQLiteWalletRepository.Tests/Stratis.Features.SQLiteWalletRepository.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.Features.SQLiteWalletRepository.Tests Stratis.Features.SQLiteWalletRepository.Tests true diff --git a/src/Stratis.Features.SQLiteWalletRepository/External/ITransactionsToLists.cs b/src/Stratis.Features.SQLiteWalletRepository/External/ITransactionsToLists.cs index 4213121851..00a324264b 100644 --- a/src/Stratis.Features.SQLiteWalletRepository/External/ITransactionsToLists.cs +++ b/src/Stratis.Features.SQLiteWalletRepository/External/ITransactionsToLists.cs @@ -55,7 +55,19 @@ internal IEnumerable GetDestinations(Script redeemScript) case TxOutType.TX_SEGWIT: TxDestination txDestination = PayToWitTemplate.Instance.ExtractScriptPubKeyParameters(this.network, redeemScript); if (txDestination != null) - yield return new KeyId(txDestination.ToBytes()); + { + if (txDestination.ToBytes().Length == 20) + { + yield return PayToWitPubKeyHashTemplate.Instance.ExtractScriptPubKeyParameters(this.network, redeemScript); + } + else if (txDestination.ToBytes().Length == 32) + { + yield return PayToWitScriptHashTemplate.Instance.ExtractScriptPubKeyParameters(this.network, redeemScript); + } + + // This should not happen, segwit scripts should generally only have one of the two valid lengths. + yield return txDestination; + } break; default: if (this.scriptAddressReader is ScriptDestinationReader scriptDestinationReader) diff --git a/src/Stratis.Features.SQLiteWalletRepository/Stratis.Features.SQLiteWalletRepository.csproj b/src/Stratis.Features.SQLiteWalletRepository/Stratis.Features.SQLiteWalletRepository.csproj index 2c81cf2938..7681cd466d 100644 --- a/src/Stratis.Features.SQLiteWalletRepository/Stratis.Features.SQLiteWalletRepository.csproj +++ b/src/Stratis.Features.SQLiteWalletRepository/Stratis.Features.SQLiteWalletRepository.csproj @@ -3,7 +3,7 @@ Stratis Features SQLiteWalletRepository Stratis.Features.SQLiteWalletRepository - netcoreapp3.1 + net6.0 Stratis.Features.SQLiteWalletRepository Stratis.Features.SQLiteWalletRepository false @@ -14,7 +14,7 @@ false false false - 1.3.2.4 + 1.4.0.7 False Stratis Group Ltd. diff --git a/src/Stratis.Features.SQLiteWalletRepository/Tables/HDTransactionData.cs b/src/Stratis.Features.SQLiteWalletRepository/Tables/HDTransactionData.cs index 8cb3a6d06f..e235282afd 100644 --- a/src/Stratis.Features.SQLiteWalletRepository/Tables/HDTransactionData.cs +++ b/src/Stratis.Features.SQLiteWalletRepository/Tables/HDTransactionData.cs @@ -280,7 +280,7 @@ FROM HDTransactionData AS t2 WHERE t.WalletId = {strWalletId} AND t.AccountIndex = {strAccountIndex}{((address == null) ? "" : $@" AND t.Address = {strAddress}")} AND (t.OutputTxIsCoinbase != 0 OR t2.SpendTxId IS NULL){(!forCirrus ? "" : $@" AND t.OutputTxIsCoinbase = 0")} - GROUP BY t.OutputTxId + GROUP BY t.OutputTxId, t.Address UNION ALL"; string spends = $@" diff --git a/src/Stratis.Features.Unity3dApi/Controllers/Unity3dController.cs b/src/Stratis.Features.Unity3dApi/Controllers/Unity3dController.cs index a9cc196c89..e7aa73e7c5 100644 --- a/src/Stratis.Features.Unity3dApi/Controllers/Unity3dController.cs +++ b/src/Stratis.Features.Unity3dApi/Controllers/Unity3dController.cs @@ -582,47 +582,26 @@ public Task> ReceiptSearchAPI([FromQuery] string contractA return Task.FromResult(result); } - [Route("watch-nft-contract")] + [Route("reindex-all-contracts")] [HttpGet] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public void WatchNFTContract([FromQuery] string contractAddress) - { - this.NFTTransferIndexer.WatchNFTContract(contractAddress); - } - - [Route("watch-nft-contracts")] - [HttpPost] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public IActionResult WatchNFTContracts([FromBody] List contractAddresses) + public IActionResult ReindexAllContracts() { - foreach (string contractAddress in contractAddresses) - { - this.NFTTransferIndexer.WatchNFTContract(contractAddress); - } + this.NFTTransferIndexer.ReindexAllContracts(); return Ok(); } - [Route("unwatch-nft-contract")] + [Route("get-entire-state")] [HttpGet] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public void UnwatchNFTContract([FromQuery] string contractAddress) + public IActionResult GetEntireState() { - this.NFTTransferIndexer.UnwatchNFTContract(contractAddress); - } - - [Route("reindex-all-contracts")] - [HttpGet] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public IActionResult ReindexAllContracts() - { - this.NFTTransferIndexer.ReindexAllContracts(); + List state = this.NFTTransferIndexer.GetEntireState(); - return Ok(); + return Ok(state); } [Route("get-watched-nft-contracts")] diff --git a/src/Stratis.Features.Unity3dApi/LocalCallContract.cs b/src/Stratis.Features.Unity3dApi/LocalCallContract.cs new file mode 100644 index 0000000000..7f23b65dea --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/LocalCallContract.cs @@ -0,0 +1,156 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using NBitcoin; +using Stratis.Bitcoin.Features.SmartContracts.Models; +using Stratis.Bitcoin.Features.SmartContracts.Wallet; +using Stratis.Bitcoin.Features.SmartContracts; +using Stratis.SmartContracts.CLR.Caching; +using Stratis.SmartContracts.CLR.Local; +using Stratis.SmartContracts.CLR.Serialization; +using Stratis.SmartContracts.CLR; +using Stratis.SmartContracts; + +namespace Stratis.Features.Unity3dApi +{ + // TODO: Move this to a more central point once 1.4.0.0 stabilises + public interface ILocalCallContract + { + LocalExecutionResponse LocalCallSmartContract(LocalCallContractRequest request); + T LocalCallSmartContract(ulong? blockHeight, string sender, string contractAddress, string methodName, params object[] arguments); + } + + public class LocalCallContract : ILocalCallContract + { + private readonly Network network; + private readonly ChainIndexer chainIndexer; + private readonly ISmartContractTransactionService smartContractTransactionService; + private readonly ILocalExecutor localExecutor; + private readonly IContractPrimitiveSerializer primitiveSerializer; + private readonly IContractAssemblyCache contractAssemblyCache; + + public LocalCallContract(Network network, ISmartContractTransactionService smartContractTransactionService, ChainIndexer chainIndexer, ILocalExecutor localExecutor) + { + this.network = network; + this.chainIndexer = chainIndexer; + this.smartContractTransactionService = smartContractTransactionService; + this.localExecutor = localExecutor; + } + + public LocalExecutionResponse LocalCallSmartContract(LocalCallContractRequest request) + { + ContractTxData txData = this.smartContractTransactionService.BuildLocalCallTxData(request); + + var height = request.BlockHeight ?? (ulong)this.chainIndexer.Height; + + ILocalExecutionResult result = this.localExecutor.Execute( + height, + request.Sender?.ToUint160(this.network) ?? new uint160(), + !string.IsNullOrWhiteSpace(request.Amount) ? (Money)request.Amount : 0, + txData); + + var deserializer = new ApiLogDeserializer(this.primitiveSerializer, this.network, result.StateRoot, this.contractAssemblyCache); + + var response = new LocalExecutionResponse + { + InternalTransfers = deserializer.MapTransferInfo(result.InternalTransfers.ToArray()), + Logs = deserializer.MapLogResponses(result.Logs.ToArray()), + GasConsumed = result.GasConsumed, + Revert = result.Revert, + ErrorMessage = result.ErrorMessage, + Return = result.Return // All return values should be primitives, let default serializer handle. + }; + + return response; + } + + private IEnumerable EncodeParameters(params object[] arguments) + { + foreach (var parameter in arguments) + { + switch (parameter) + { + case bool boolVal: + yield return $"1#{boolVal}"; + break; + + case byte byteVal: + yield return $"2#{byteVal}"; + break; + + case char charVal: + yield return $"3#{charVal}"; + break; + + case string stringVal: + yield return $"4#{stringVal}"; + break; + + case uint uint32: + yield return $"5#{uint32}"; + break; + + case int int32: + yield return $"6#{int32}"; + break; + + case ulong uint64: + yield return $"7#{uint64}"; + break; + + case long int64: + yield return $"8#{int64}"; + break; + + case Address address: + yield return $"9#{address}"; + break; + + case byte[] byteArr: + yield return $"10#{BitConverter.ToString(byteArr).Replace("-", "")}"; + break; + + case UInt128 uInt128: + yield return $"11#{uInt128}"; + break; + + case UInt256 uInt256: + yield return $"12#{uInt256}"; + break; + + default: + throw new Exception($"Currently unsupported argument type: '{parameter.GetType().Name}'"); + } + } + } + + public T LocalCallSmartContract(ulong? blockHeight, string sender, string contractAddress, string methodName, params object[] arguments) + { + var request = new LocalCallContractRequest + { + BlockHeight = blockHeight, + Amount = "0", + ContractAddress = contractAddress, + GasLimit = 250_000, + GasPrice = 100, + MethodName = methodName, + Parameters = EncodeParameters(arguments).ToArray(), + Sender = sender + }; + + try + { + LocalExecutionResponse result = this.LocalCallSmartContract(request); + + if (result.Return == null) + return default(T); + + return (T)result.Return; + } + catch (Exception) + { + throw; + } + } + } +} diff --git a/src/Stratis.Features.Unity3dApi/NFTTransferIndexer.cs b/src/Stratis.Features.Unity3dApi/NFTTransferIndexer.cs index 02c0306465..bda4e547c6 100644 --- a/src/Stratis.Features.Unity3dApi/NFTTransferIndexer.cs +++ b/src/Stratis.Features.Unity3dApi/NFTTransferIndexer.cs @@ -11,10 +11,10 @@ using Newtonsoft.Json; using Stratis.Bitcoin.AsyncWork; using Stratis.Bitcoin.Configuration; -using Stratis.Bitcoin.Features.BlockStore.AddressIndexing; using Stratis.Bitcoin.Features.SmartContracts.Models; using Stratis.Bitcoin.Features.SmartContracts.Wallet; using Stratis.Bitcoin.Utilities; +using Stratis.SmartContracts.CLR; using FileMode = LiteDB.FileMode; namespace Stratis.Features.Unity3dApi @@ -24,12 +24,6 @@ public interface INFTTransferIndexer : IDisposable /// Initialized NFT indexer. void Initialize(); - /// Adds NFT contract to watch list. Only contracts from the watch list are being indexed. - void WatchNFTContract(string contractAddress); - - /// Removes NFT contract from watch list. - void UnwatchNFTContract(string contractAddress); - /// Provides a list of all nft contract addresses that are being tracked. List GetWatchedNFTContracts(); @@ -41,36 +35,48 @@ public interface INFTTransferIndexer : IDisposable /// Reindexes all tracked contracts. void ReindexAllContracts(); + + /// Retrieves all indexed data. + public List GetEntireState(); } /// This component maps addresses to NFT Ids they own. public class NFTTransferIndexer : INFTTransferIndexer { - public ChainedHeader IndexerTip { get; private set; } - + private const string NftTransferEventName = "TransferLog"; private const string DatabaseFilename = "NFTTransferIndexer.litedb"; private const string DbOwnedNFTsKey = "OwnedNfts"; + private const string IndexerStateKey = "IndexerState"; private readonly DataFolder dataFolder; private readonly ILogger logger; private readonly ChainIndexer chainIndexer; private readonly IAsyncProvider asyncProvider; + private readonly INodeLifetime nodeLifetime; private readonly ISmartContractTransactionService smartContractTransactionService; private readonly Network network; + private readonly NftContractLocalClient nftContractLocalClient; private LiteDatabase db; private LiteCollection NFTContractCollection; + private LiteCollection indexerState; + private HashSet knownContracts; private CancellationTokenSource cancellation; - private Task indexingTask; + private IAsyncLoop indexingLoop; - public NFTTransferIndexer(DataFolder dataFolder, ILoggerFactory loggerFactory, IAsyncProvider asyncProvider, - ChainIndexer chainIndexer, Network network, ISmartContractTransactionService smartContractTransactionService = null) + public NFTTransferIndexer(DataFolder dataFolder, ILoggerFactory loggerFactory, IAsyncProvider asyncProvider, INodeLifetime nodeLifetime, + ChainIndexer chainIndexer, Network network, ILocalExecutor localExecutor, Unity3dApiSettings apiSettings, ISmartContractTransactionService smartContractTransactionService = null) { this.network = network; this.dataFolder = dataFolder; this.cancellation = new CancellationTokenSource(); this.asyncProvider = asyncProvider; + this.nodeLifetime = nodeLifetime; this.chainIndexer = chainIndexer; + + var localCallContract = new LocalCallContract(network, smartContractTransactionService, chainIndexer, localExecutor); + + this.nftContractLocalClient = new NftContractLocalClient(localCallContract, apiSettings.LocalCallSenderAddress); this.smartContractTransactionService = smartContractTransactionService; this.logger = loggerFactory.CreateLogger(this.GetType().FullName); @@ -88,8 +94,41 @@ public void Initialize() this.db = new LiteDatabase(new ConnectionString() { Filename = dbPath, Mode = fileMode }); this.NFTContractCollection = this.db.GetCollection(DbOwnedNFTsKey); - this.indexingTask = Task.Run(async () => await this.IndexNFTsContinuouslyAsync().ConfigureAwait(false)); - this.asyncProvider.RegisterTask($"{nameof(AddressIndexer)}.{nameof(this.indexingTask)}", this.indexingTask); + this.indexerState = this.db.GetCollection(IndexerStateKey); + + if (this.indexerState.Count() == 0) + { + this.logger.LogInformation("NFT indexer state not found, restarting index."); + + // Will automatically add the state model once it is finished. + this.ReindexAllContracts(); + } + + this.logger.LogInformation("Building cache of known contract addresses."); + + this.knownContracts = new HashSet(); + + foreach (NFTContractModel model in this.NFTContractCollection.FindAll()) + { + this.knownContracts.Add(model.ContractAddress); + } + + this.logger.LogInformation("Finished building cache of known contract addresses."); + + this.indexingLoop = this.asyncProvider.CreateAndRunAsyncLoop(nameof(IndexNFTsContinuouslyAsync), async (cancellationTokenSource) => + { + try + { + await this.IndexNFTsContinuouslyAsync().ConfigureAwait(false); + } + catch (Exception e) + { + this.logger.LogWarning("Exception raised while indexing NFTs. {0}", e); + } + }, + this.nodeLifetime.ApplicationStopping, + repeatEvery: TimeSpans.TenSeconds, + startAfter: TimeSpans.Second); this.logger.LogDebug("NFTTransferIndexer initialized."); } @@ -101,49 +140,30 @@ private int GetWatchFromHeight() return watchFromHeight; } - /// - public void WatchNFTContract(string contractAddress) + private IndexerStateModel GetIndexerState() { - try - { - // Check that contract address is a valid address - var addr = new BitcoinPubKeyAddress(contractAddress, this.network); - } - catch (FormatException) - { - return; - } - - int watchFromHeight = this.GetWatchFromHeight(); + IndexerStateModel indexerStateModel = this.indexerState.FindAll().FirstOrDefault(); - if (!this.NFTContractCollection.Exists(x => x.ContractAddress == contractAddress)) + if (indexerStateModel == null) { - NFTContractModel model = new NFTContractModel() - { - ContractAddress = contractAddress, - LastUpdatedBlock = watchFromHeight, - OwnedIDsByAddress = new Dictionary>() - }; - - this.NFTContractCollection.Upsert(model); - - this.logger.LogDebug("Added contract " + contractAddress + " to watchlist."); + indexerStateModel = new IndexerStateModel() { LastProcessedHeight = GetWatchFromHeight() }; } - else - this.logger.LogDebug("Tried to add contract " + contractAddress + " to watchlist, but it's already tracked."); + + return indexerStateModel; } - /// - public void UnwatchNFTContract(string contractAddress) + private void UpdateLastUpdatedBlock(int blockHeight) { - NFTContractModel entryToRemove = this.NFTContractCollection.FindOne(x => x.ContractAddress == contractAddress); + IndexerStateModel indexerStateModel = this.indexerState.FindAll().FirstOrDefault(); - if (entryToRemove == null) - return; + if (indexerStateModel == null) + { + indexerStateModel = new IndexerStateModel(); + } - this.NFTContractCollection.Delete(entryToRemove.Id); + indexerStateModel.LastProcessedHeight = blockHeight; - this.logger.LogDebug("Unwatched contract " + contractAddress); + this.indexerState.Upsert(indexerStateModel); } /// @@ -159,7 +179,7 @@ public OwnedNFTsModel GetOwnedNFTs(string address) List NFTContractModels = this.NFTContractCollection.FindAll().Where(x => x.OwnedIDsByAddress.ContainsKey(address)).ToList(); - OwnedNFTsModel output = new OwnedNFTsModel() { OwnedIDsByContractAddress = new Dictionary>() }; + var output = new OwnedNFTsModel() { OwnedIDsByContractAddress = new Dictionary>() }; foreach (NFTContractModel contractModel in NFTContractModels) { @@ -181,130 +201,140 @@ public NFTContractModel GetAllNFTOwnersByContractAddress(string contractAddress) /// public void ReindexAllContracts() { - this.logger.LogTrace("ReindexAllContracts()"); - - int watchFromHeight = this.GetWatchFromHeight(); + var updated = new List(); foreach (NFTContractModel contractModel in this.NFTContractCollection.FindAll().ToList()) { contractModel.OwnedIDsByAddress = new Dictionary>(); - contractModel.LastUpdatedBlock = watchFromHeight; - this.NFTContractCollection.Upsert(contractModel); + updated.Add(contractModel); } - this.logger.LogTrace("ReindexAllContracts(-)"); + this.NFTContractCollection.Upsert(updated); + + this.UpdateLastUpdatedBlock(GetWatchFromHeight()); + + this.logger.LogInformation($"A re-index of all contracts will be triggered from block {GetWatchFromHeight()}."); } - private async Task IndexNFTsContinuouslyAsync() + /// + public List GetEntireState() { - await Task.Delay(1); - - this.logger.LogDebug("Indexing started"); + List state = this.NFTContractCollection.FindAll().ToList(); - try - { - while (!this.cancellation.Token.IsCancellationRequested) - { - List contracts = this.NFTContractCollection.FindAll().Select(x => x.ContractAddress).ToList(); + return state; + } - foreach (string contractAddr in contracts) - { - if (this.cancellation.Token.IsCancellationRequested) - break; + private async Task IndexNFTsContinuouslyAsync() + { + IndexerStateModel currentIndexerState = GetIndexerState(); - this.logger.LogTrace("Updating data for contract: " + contractAddr); + if (this.chainIndexer.Tip.Height < GetWatchFromHeight()) + { + await Task.Delay(5000); + return; + } - NFTContractModel currentContract = this.NFTContractCollection.FindOne(x => x.ContractAddress == contractAddr); + ChainedHeader chainTip = this.chainIndexer.Tip; - ChainedHeader chainTip = this.chainIndexer.Tip; + if (chainTip.Height == currentIndexerState.LastProcessedHeight) + { + this.logger.LogInformation("No need to update, already up to tip."); + return; + } - if (chainTip.Height == currentContract.LastUpdatedBlock) - { - this.logger.LogTrace("No need to update, already up to tip."); - continue; - } + // Return TransferLog receipts for any contract (we won't know definitively if they're NFT contracts until we check the cache or query the contract's supported interfaces). + this.logger.LogInformation($"Initiating receipt search from block {currentIndexerState.LastProcessedHeight + 1} to {chainTip.Height}."); + List receipts = this.smartContractTransactionService.ReceiptSearch((List)null, NftTransferEventName, null, currentIndexerState.LastProcessedHeight + 1, chainTip.Height); - List receipts = this.smartContractTransactionService.ReceiptSearch( - contractAddr, "TransferLog", null, currentContract.LastUpdatedBlock + 1, null); + if ((receipts == null) || (receipts.Count == 0)) + { + this.logger.LogInformation($"No receipts found, updated to height {chainTip.Height}."); + this.UpdateLastUpdatedBlock(chainTip.Height); - if ((receipts == null) || (receipts.Count == 0)) - { - currentContract.LastUpdatedBlock = chainTip.Height; - this.NFTContractCollection.Upsert(currentContract); + return; + } - this.logger.LogTrace("No receipts found. Updated to height " + chainTip.Height); - continue; - } + int processedCount = 0; - int lastReceiptHeight = 0; - if (receipts.Any()) - lastReceiptHeight = (int)receipts.Last().BlockNumber.Value; + var changedContracts = new HashSet(); - currentContract.LastUpdatedBlock = new List() { chainTip.Height, lastReceiptHeight }.Max(); + this.logger.LogInformation($"{receipts.Count} receipts found for indexing."); - List transferLogs = new List(receipts.Count); + foreach (ReceiptResponse receiptRes in receipts) + { + foreach (LogResponse logResponse in receiptRes.Logs) + { + if (logResponse.Log.Event != NftTransferEventName) + continue; - foreach (ReceiptResponse receiptRes in receipts) + // Now check if this is an NFT contract. As this is more expensive than retrieving the receipts we check it second. + if (!this.knownContracts.Contains(logResponse.Address)) + { + if (!this.nftContractLocalClient.SupportsInterface((ulong)chainTip.Height, logResponse.Address, TokenInterface.INonFungibleToken)) { - LogData log = receiptRes.Logs.First().Log; - string jsonLog = JsonConvert.SerializeObject(log); + this.logger.LogTrace("Found TransferLog for non-NFT contract: " + logResponse.Address); - TransferLogRoot infoObj = JsonConvert.DeserializeObject(jsonLog); - transferLogs.Add(infoObj.Data); + break; } - this.logger.LogDebug("Processing transafer logs."); + this.logger.LogInformation($"Found new NFT contract '{logResponse.Address}'"); + + this.knownContracts.Add(logResponse.Address); - for (int i = 0; i < transferLogs.Count; i++) + this.NFTContractCollection.Insert(new NFTContractModel { - TransferLog transferInfo = transferLogs[i]; + ContractAddress = logResponse.Address, + OwnedIDsByAddress = new Dictionary>() + }); + } - this.logger.LogDebug("log #{0}: From: {1} To: {2} Id:{3}", i, transferInfo.From, transferInfo.To, transferInfo.TokenId); + string jsonLog = JsonConvert.SerializeObject(logResponse.Log); - if ((transferInfo.From != null) && currentContract.OwnedIDsByAddress.ContainsKey(transferInfo.From)) - { - bool fromExists = currentContract.OwnedIDsByAddress.ContainsKey(transferInfo.From); + TransferLogRoot infoObj = JsonConvert.DeserializeObject(jsonLog); - this.logger.LogDebug("FromExists: {0} ", fromExists); + TransferLog transferInfo = infoObj.Data; - currentContract.OwnedIDsByAddress[transferInfo.From].Remove(transferInfo.TokenId); + this.logger.LogDebug("Log from: {0}, to: {1}, ID: {2}", transferInfo.From, transferInfo.To, transferInfo.TokenId); - if (currentContract.OwnedIDsByAddress[transferInfo.From].Count == 0) - currentContract.OwnedIDsByAddress.Remove(transferInfo.From); - } + // Check if the contract already had modifications and if so, use that one. + NFTContractModel currentContract = changedContracts.FirstOrDefault(c => c.ContractAddress == logResponse.Address); + if (currentContract == null) + currentContract = this.NFTContractCollection.FindOne(c => c.ContractAddress == logResponse.Address); - if (!currentContract.OwnedIDsByAddress.ContainsKey(transferInfo.To)) - { - this.logger.LogDebug("Added ID to To"); - currentContract.OwnedIDsByAddress.Add(transferInfo.To, new HashSet()); - } - else - this.logger.LogDebug("Already added!"); + if ((transferInfo.From != null) && currentContract.OwnedIDsByAddress.ContainsKey(transferInfo.From)) + { + bool fromExists = currentContract.OwnedIDsByAddress.ContainsKey(transferInfo.From); - currentContract.OwnedIDsByAddress[transferInfo.To].Add(transferInfo.TokenId); - } + this.logger.LogDebug("FromExists: {0} ", fromExists); - this.NFTContractCollection.Upsert(currentContract); + currentContract.OwnedIDsByAddress[transferInfo.From].Remove(transferInfo.TokenId); - this.logger.LogTrace("Found " + transferLogs.Count + " transfer logs. Last updated block: " + currentContract.LastUpdatedBlock); + if (currentContract.OwnedIDsByAddress[transferInfo.From].Count == 0) + currentContract.OwnedIDsByAddress.Remove(transferInfo.From); } - try - { - await Task.Delay(TimeSpan.FromSeconds(1), this.cancellation.Token); - } - catch (TaskCanceledException) + if (!currentContract.OwnedIDsByAddress.ContainsKey(transferInfo.To)) { + this.logger.LogDebug("Added ID to To"); + currentContract.OwnedIDsByAddress.Add(transferInfo.To, new HashSet()); } + else + this.logger.LogDebug("Already added!"); + + currentContract.OwnedIDsByAddress[transferInfo.To].Add(transferInfo.TokenId); + + processedCount++; + + changedContracts.Add(currentContract); } } - catch (Exception e) - { - this.logger.LogError(e.ToString()); - } - this.logger.LogDebug("Indexing stopped"); + this.NFTContractCollection.Upsert(changedContracts); + + this.UpdateLastUpdatedBlock(chainTip.Height); + + this.logger.LogInformation("Found " + processedCount + " transfer logs. Last updated block: " + chainTip.Height); } public void Dispose() @@ -312,7 +342,7 @@ public void Dispose() this.logger.LogDebug("Dispose()"); this.cancellation.Cancel(); - this.indexingTask?.GetAwaiter().GetResult(); + this.indexingLoop?.Dispose(); this.db?.Dispose(); this.logger.LogDebug("Dispose(-)"); @@ -327,8 +357,6 @@ public class NFTContractModel // Key is nft owner address, value is list of NFT IDs public Dictionary> OwnedIDsByAddress { get; set; } - - public int LastUpdatedBlock { get; set; } } public class OwnedNFTsModel @@ -336,6 +364,13 @@ public class OwnedNFTsModel public Dictionary> OwnedIDsByContractAddress { get; set; } } + public class IndexerStateModel + { + public int Id { get; set; } + + public int LastProcessedHeight { get; set; } + } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.3.0 (Newtonsoft.Json v11.0.0.0)")] public partial class TransferLogRoot { diff --git a/src/Stratis.Features.Unity3dApi/NftContractLocalCLient.cs b/src/Stratis.Features.Unity3dApi/NftContractLocalCLient.cs new file mode 100644 index 0000000000..267d82c27a --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/NftContractLocalCLient.cs @@ -0,0 +1,32 @@ +using Stratis.SmartContracts; + +namespace Stratis.Features.Unity3dApi +{ + public class NftContractLocalClient + { + public const string SupportsInterfaceMethodName = "SupportsInterface"; + + private readonly ILocalCallContract localCallContract; + private readonly string senderAddress; + + public NftContractLocalClient(ILocalCallContract localCallContract, string senderAddress) + { + this.localCallContract = localCallContract; + this.senderAddress = senderAddress; + } + + public bool SupportsInterface(ulong? blockHeight, string contractAddress, TokenInterface tokenInterfaceId) + { + return this.localCallContract.LocalCallSmartContract(blockHeight, this.senderAddress, contractAddress, SupportsInterfaceMethodName, (uint)tokenInterfaceId); + } + } + + public enum TokenInterface + { + ISupportsInterface = 1, + INonFungibleToken = 2, + INonFungibleTokenReceiver = 3, + INonFungibleTokenMetadata = 4, + INonFungibleTokenEnumerable = 5, + } +} diff --git a/src/Stratis.Features.Unity3dApi/Stratis.Features.Unity3dApi.csproj b/src/Stratis.Features.Unity3dApi/Stratis.Features.Unity3dApi.csproj index 17fd6d60ef..bf650bb820 100644 --- a/src/Stratis.Features.Unity3dApi/Stratis.Features.Unity3dApi.csproj +++ b/src/Stratis.Features.Unity3dApi/Stratis.Features.Unity3dApi.csproj @@ -1,8 +1,9 @@ - netcoreapp3.1 + net6.0 Library + false diff --git a/src/Stratis.Features.Unity3dApi/Unity3dApiSettings.cs b/src/Stratis.Features.Unity3dApi/Unity3dApiSettings.cs index d3cca9bfdd..ca1f42f502 100644 --- a/src/Stratis.Features.Unity3dApi/Unity3dApiSettings.cs +++ b/src/Stratis.Features.Unity3dApi/Unity3dApiSettings.cs @@ -44,6 +44,12 @@ public class Unity3dApiSettings /// Use HTTPS or not. public bool UseHttps { get; set; } + /// + /// A wallet address to use for local contract calls. + /// + /// Can be an arbitrary valid address. + public string LocalCallSenderAddress { get; set; } + /// /// Initializes an instance of the object from the node configuration. /// @@ -102,6 +108,11 @@ public Unity3dApiSettings(NodeSettings nodeSettings) Interval = keepAlive * 1000 }; } + + this.LocalCallSenderAddress = config.GetOrDefault("unityapi_localcallsenderaddress", nodeSettings.Network.CirrusRewardDummyAddress); + + if (string.IsNullOrWhiteSpace(this.LocalCallSenderAddress)) + throw new ConfigurationException("The local call sender address must be specified."); } /// Prints the help information on how to configure the API settings to the logger. @@ -116,6 +127,7 @@ public static void PrintHelp(Network network) builder.AppendLine($"-unityapi_keepalive= Keep Alive interval (set in seconds). Default: 0 (no keep alive)."); builder.AppendLine($"-unityapi_usehttps= Use https protocol on the API. Defaults to false."); builder.AppendLine($"-unityapi_certificatefilepath= Path to the certificate used for https traffic encryption. Defaults to . Password protected files are not supported. On MacOs, only p12 certificates can be used without password."); + builder.AppendLine($"-unityapi_localcallsenderaddress= Arbitrary address to be used for submitting local contract calls (non-monetary)."); var logger = NodeSettings.Default(network).LoggerFactory.CreateLogger(typeof(Unity3dApiSettings).FullName); logger.LogInformation(builder.ToString()); @@ -142,6 +154,7 @@ public static void BuildDefaultConfigurationFile(StringBuilder builder, Network builder.AppendLine($"#Path to the file containing the certificate to use for https traffic encryption. Password protected files are not supported. On MacOs, only p12 certificates can be used without password."); builder.AppendLine(@"#Please refer to .Net Core documentation for usage: 'https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2.-ctor?view=netcore-2.1#System_Security_Cryptography_X509Certificates_X509Certificate2__ctor_System_Byte___'."); builder.AppendLine($"#unityapi_certificatefilepath="); + builder.AppendLine($"#unityapi_localcallsenderaddress="); } } } diff --git a/src/Stratis.Interop.Contracts/Stratis.Interop.Contracts.csproj b/src/Stratis.Interop.Contracts/Stratis.Interop.Contracts.csproj index 4be36868e9..007a8d1788 100644 --- a/src/Stratis.Interop.Contracts/Stratis.Interop.Contracts.csproj +++ b/src/Stratis.Interop.Contracts/Stratis.Interop.Contracts.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 Stratis Group Ltd. Copyright © 2022 diff --git a/src/Stratis.Interop.Contracts/SupportedContractAddresses.cs b/src/Stratis.Interop.Contracts/SupportedContractAddresses.cs index 45d8d6361c..ff7fa1a2f2 100644 --- a/src/Stratis.Interop.Contracts/SupportedContractAddresses.cs +++ b/src/Stratis.Interop.Contracts/SupportedContractAddresses.cs @@ -16,25 +16,25 @@ public static class SupportedContractAddresses new SupportedContractAddress() { NativeNetwork = SupportedNativeChain.Ethereum, - NativeNetworkAddress = "0xf5dab0f35378ea5fc69149d0f20ba0c16b170a3d", - SRC20Address = "tQk6t6ithWWoBUQxphDShcYFF6s916mM4R", - TokenName = "TSTX", + NativeNetworkAddress = "0xe4a444cb3222fd8e9518db8f70a33aadb9a1a358", + SRC20Address = "tSnYKnLSEjFVYgMC5ajDxy3iuGd4boe3NA", + TokenName = "TSZ1", Decimals = 18 }, new SupportedContractAddress() { NativeNetwork = SupportedNativeChain.Ethereum, - NativeNetworkAddress = "0x2b3b0bd8219ffe0c22ffcdefbc81b7efa5c8d9ba", - SRC20Address = "tWCCJ3FxmoYuzrE4aUcDLDh9gn51EJ4cvM", - TokenName = "TSTY", + NativeNetworkAddress = "0xf197f5f8c406d269e2cc44aaf495fbc4eb519634", + SRC20Address = "tMZ2dKG9BKvCfVWvxUN47enXyaDyWLe44z", + TokenName = "TSZ2", Decimals = 8 }, new SupportedContractAddress() { NativeNetwork = SupportedNativeChain.Ethereum, - NativeNetworkAddress = "0x4cb3e0b719a7707c0148e21585d8011213de6708", - SRC20Address = "tQspjyuEap2vDaNkf9KRHQLdU3h8qq6dnX", - TokenName = "TSTZ", + NativeNetworkAddress = "0xa3c22370de5f9544f0c4de126b1e46ceadf0a51b", + SRC20Address = "tECnjt1eYCxK7wztSS92QQvKzqWAbpzfXt", + TokenName = "TSZ3", Decimals = 6 }, } diff --git a/src/Stratis.Patricia.Tests/Stratis.Patricia.Tests.csproj b/src/Stratis.Patricia.Tests/Stratis.Patricia.Tests.csproj index 7fef4f046d..d86e44689d 100644 --- a/src/Stratis.Patricia.Tests/Stratis.Patricia.Tests.csproj +++ b/src/Stratis.Patricia.Tests/Stratis.Patricia.Tests.csproj @@ -1,12 +1,14 @@ - + - netcoreapp3.1 + net6.0 + false - - + + + diff --git a/src/Stratis.Patricia/Stratis.Patricia.csproj b/src/Stratis.Patricia/Stratis.Patricia.csproj index 004d922b0c..47a79920c6 100644 --- a/src/Stratis.Patricia/Stratis.Patricia.csproj +++ b/src/Stratis.Patricia/Stratis.Patricia.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net6.0 Stratis Patricia Trie Stratis Group Ltd. A C# implementation of a merkle patricia trie. diff --git a/src/Stratis.Sidechains.Networks/CirrusMain.cs b/src/Stratis.Sidechains.Networks/CirrusMain.cs index 3743e452a5..e57ea5ac92 100644 --- a/src/Stratis.Sidechains.Networks/CirrusMain.cs +++ b/src/Stratis.Sidechains.Networks/CirrusMain.cs @@ -185,7 +185,8 @@ public CirrusMain() GetMiningTimestampV2ActivationHeight = 3_709_000, // Monday 14 February 00:00:00 (Estimated) GetMiningTimestampV2ActivationStrictHeight = 3_783_000, // Monday 28 February 07:00:00 (London Time) (Estimated) ContractSerializerV2ActivationHeight = 3_386_335, // Monday 13 December 16:00:00 (Estimated) - Release1300ActivationHeight = 4_334_400 + Release1300ActivationHeight = 4_334_400, + Release1400ActivationHeight = 5_345_325, // 7 December 2022 (Estimated) }; var buriedDeployments = new BuriedDeploymentsArray diff --git a/src/Stratis.Sidechains.Networks/CirrusTest.cs b/src/Stratis.Sidechains.Networks/CirrusTest.cs index 1289f99293..52eedcb109 100644 --- a/src/Stratis.Sidechains.Networks/CirrusTest.cs +++ b/src/Stratis.Sidechains.Networks/CirrusTest.cs @@ -136,7 +136,8 @@ public CirrusTest() GetMiningTimestampV2ActivationHeight = 3_000_000, // 15 January 2022 GetMiningTimestampV2ActivationStrictHeight = 3_121_500, // 17 January 2022 ContractSerializerV2ActivationHeight = 2_842_681, - Release1300ActivationHeight = 3_280_032 + Release1300ActivationHeight = 3_280_032, + Release1400ActivationHeight = 4_074_250, }; var buriedDeployments = new BuriedDeploymentsArray diff --git a/src/Stratis.Sidechains.Networks/Stratis.Sidechains.Networks.csproj b/src/Stratis.Sidechains.Networks/Stratis.Sidechains.Networks.csproj index 0ac4129e2c..4dff3276e0 100644 --- a/src/Stratis.Sidechains.Networks/Stratis.Sidechains.Networks.csproj +++ b/src/Stratis.Sidechains.Networks/Stratis.Sidechains.Networks.csproj @@ -1,11 +1,11 @@  - netcoreapp3.1 + net6.0 Full ..\None.ruleset - 1.3.2.4 + 1.4.0.7 Stratis Group Ltd. Stratis.Sidechains.Networks diff --git a/src/Stratis.SmartContracts.CLR.Tests/CSharpContractDecompilerTests.cs b/src/Stratis.SmartContracts.CLR.Tests/CSharpContractDecompilerTests.cs index 6e287a98cc..8e5d97b1ed 100644 --- a/src/Stratis.SmartContracts.CLR.Tests/CSharpContractDecompilerTests.cs +++ b/src/Stratis.SmartContracts.CLR.Tests/CSharpContractDecompilerTests.cs @@ -36,6 +36,7 @@ public void Basic_Contract_Decompiles() Result result = this.decompiler.GetSource(contractBytes); Assert.True(result.IsSuccess); Assert.Contains("public class Auction", result.Value); + Assert.DoesNotContain("Unknown result type (might be due to invalid IL or missing references)", result.Value); } [Fact] @@ -46,6 +47,7 @@ public void Multiple_Classes_Decompile() Assert.True(result.IsSuccess); Assert.Contains("public class CatOwner : SmartContract", result.Value); Assert.Contains("public class Cat : SmartContract", result.Value); + Assert.DoesNotContain("Unknown result type (might be due to invalid IL or missing references)", result.Value); } } } \ No newline at end of file diff --git a/src/Stratis.SmartContracts.CLR.Tests/Stratis.SmartContracts.CLR.Tests.csproj b/src/Stratis.SmartContracts.CLR.Tests/Stratis.SmartContracts.CLR.Tests.csproj index 6a4b6b4f7d..6da53f9e67 100644 --- a/src/Stratis.SmartContracts.CLR.Tests/Stratis.SmartContracts.CLR.Tests.csproj +++ b/src/Stratis.SmartContracts.CLR.Tests/Stratis.SmartContracts.CLR.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 false diff --git a/src/Stratis.SmartContracts.CLR.Validation.Tests/Stratis.SmartContracts.CLR.Validation.Tests.csproj b/src/Stratis.SmartContracts.CLR.Validation.Tests/Stratis.SmartContracts.CLR.Validation.Tests.csproj index 1a40f23853..0db3e3b850 100644 --- a/src/Stratis.SmartContracts.CLR.Validation.Tests/Stratis.SmartContracts.CLR.Validation.Tests.csproj +++ b/src/Stratis.SmartContracts.CLR.Validation.Tests/Stratis.SmartContracts.CLR.Validation.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 false diff --git a/src/Stratis.SmartContracts.CLR.Validation.Tests/ValidationTests.cs b/src/Stratis.SmartContracts.CLR.Validation.Tests/ValidationTests.cs index 213469a883..6cc52bc4dd 100644 --- a/src/Stratis.SmartContracts.CLR.Validation.Tests/ValidationTests.cs +++ b/src/Stratis.SmartContracts.CLR.Validation.Tests/ValidationTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; @@ -551,7 +552,9 @@ public TypeDefinition CompileToTypeDef(string source) { var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); var smartContracts = MetadataReference.CreateFromFile(typeof(Address).Assembly.Location); - var runtime = MetadataReference.CreateFromFile(typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly.Location); + + var assembly = Assembly.Load("System.Runtime"); + var runtime = MetadataReference.CreateFromFile(assembly.Location); SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); diff --git a/src/Stratis.SmartContracts.CLR.Validation/Stratis.SmartContracts.CLR.Validation.csproj b/src/Stratis.SmartContracts.CLR.Validation/Stratis.SmartContracts.CLR.Validation.csproj index a1640e41ac..ec4972e010 100644 --- a/src/Stratis.SmartContracts.CLR.Validation/Stratis.SmartContracts.CLR.Validation.csproj +++ b/src/Stratis.SmartContracts.CLR.Validation/Stratis.SmartContracts.CLR.Validation.csproj @@ -1,14 +1,14 @@  - netcoreapp3.1 + net6.0 2.0.1.0 Stratis Group Ltd. - + diff --git a/src/Stratis.SmartContracts.CLR/Decompilation/CSharpContractDecompiler.cs b/src/Stratis.SmartContracts.CLR/Decompilation/CSharpContractDecompiler.cs index fb301fc1c9..9cb28580e5 100644 --- a/src/Stratis.SmartContracts.CLR/Decompilation/CSharpContractDecompiler.cs +++ b/src/Stratis.SmartContracts.CLR/Decompilation/CSharpContractDecompiler.cs @@ -1,9 +1,12 @@ using System; +using System.Linq; using System.IO; +using System.Reflection; +using System.Reflection.PortableExecutable; using CSharpFunctionalExtensions; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; -using Mono.Cecil; +using ICSharpCode.Decompiler.Metadata; namespace Stratis.SmartContracts.CLR.Decompilation { @@ -18,8 +21,11 @@ public Result GetSource(byte[] bytecode) { try { - ModuleDefinition modDefinition = ModuleDefinition.ReadModule(memStream); - var decompiler = new CSharpDecompiler(modDefinition, new DecompilerSettings { }); + var peFile = new PEFile("placeholder", memStream); + var resolver = new UniversalAssemblyResolver(null, false, null, null, PEStreamOptions.Default); + var folder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + resolver.AddSearchDirectory(folder); + var decompiler = new CSharpDecompiler(peFile, resolver, new DecompilerSettings()); string cSharp = decompiler.DecompileWholeModuleAsString(); return Result.Ok(cSharp); } diff --git a/src/Stratis.SmartContracts.CLR/Serialization/MethodParameterStringSerializer.cs b/src/Stratis.SmartContracts.CLR/Serialization/MethodParameterStringSerializer.cs index 8e37db6cec..e60c022e8a 100644 --- a/src/Stratis.SmartContracts.CLR/Serialization/MethodParameterStringSerializer.cs +++ b/src/Stratis.SmartContracts.CLR/Serialization/MethodParameterStringSerializer.cs @@ -102,6 +102,22 @@ public object[] Deserialize(string parameters) return this.StringToObjects(parameters); } + public byte[] HexOrEncodingToByteArray(string hex) + { + if (!hex.Contains("#")) + return hex.HexToByteArray(); + + var byteSerializer = new MethodParameterByteSerializer(new ContractPrimitiveSerializer(this.network, null)); + + // "#" is a special case for indicating an empty list of parameters. + object[] objects = (hex == "#") ? new object[0] : this.Deserialize(hex); + + // RLP encode the parameters. + var output = byteSerializer.Serialize(objects); + + return output; + } + private object[] StringToObjects(string parameters) { string[] split = Regex.Split(parameters, @"(? - netcoreapp3.1 + net6.0 2.0.2.0 2.0.2.0 @@ -12,7 +12,7 @@ - + diff --git a/src/Stratis.SmartContracts.Core.Tests/Receipts/ReceiptMatcherTests.cs b/src/Stratis.SmartContracts.Core.Tests/Receipts/ReceiptMatcherTests.cs index d8649f08b5..033e77dd0f 100644 --- a/src/Stratis.SmartContracts.Core.Tests/Receipts/ReceiptMatcherTests.cs +++ b/src/Stratis.SmartContracts.Core.Tests/Receipts/ReceiptMatcherTests.cs @@ -51,7 +51,7 @@ public void MatchReceipts_Success() // Every receipt should have at least one log like this. for (int i = 0; i < 10; i++) { - List matches = matcher.MatchReceipts(receipts, null, + List matches = matcher.MatchReceipts(receipts, (uint160)null, new List { Encoding.UTF8.GetBytes("Event" + i) @@ -59,7 +59,7 @@ public void MatchReceipts_Success() Assert.Single(matches); - matches = matcher.MatchReceipts(receipts, null, + matches = matcher.MatchReceipts(receipts, (uint160)null, new List { Encoding.UTF8.GetBytes("Topic" + i) diff --git a/src/Stratis.SmartContracts.Core.Tests/Stratis.SmartContracts.Core.Tests.csproj b/src/Stratis.SmartContracts.Core.Tests/Stratis.SmartContracts.Core.Tests.csproj index e1efeec4fa..25ac3a6b21 100644 --- a/src/Stratis.SmartContracts.Core.Tests/Stratis.SmartContracts.Core.Tests.csproj +++ b/src/Stratis.SmartContracts.Core.Tests/Stratis.SmartContracts.Core.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 false diff --git a/src/Stratis.SmartContracts.Core/Receipts/ReceiptMatcher.cs b/src/Stratis.SmartContracts.Core/Receipts/ReceiptMatcher.cs index 5aae3b1e5e..daa8277316 100644 --- a/src/Stratis.SmartContracts.Core/Receipts/ReceiptMatcher.cs +++ b/src/Stratis.SmartContracts.Core/Receipts/ReceiptMatcher.cs @@ -9,13 +9,24 @@ public class ReceiptMatcher /// /// Matches the given receipts against the supplied address and topics, and returns those that are present in the filter. /// - /// - /// + /// The list of receipts that need to be evaluated. + /// The single address to match receipts against. /// - /// + /// The list of receipts that were matched. public List MatchReceipts(IEnumerable receipts, uint160 address, IEnumerable topics) { - // For each block, get all receipts, and if they match, add to list to return. + return MatchReceipts(receipts, new HashSet() { address }, topics); + } + + /// + /// Matches the given receipts against the supplied addresses and topics, and returns those that are present in the filter. + /// + /// The list of receipts that need to be evaluated. + /// The collection of addresses to match receipts against. + /// + /// The list of receipts that were matched. + public List MatchReceipts(IEnumerable receipts, HashSet addresses, IEnumerable topics) + { var receiptResponses = new List(); foreach (Receipt storedReceipt in receipts) @@ -24,11 +35,13 @@ public List MatchReceipts(IEnumerable receipts, uint160 addres continue; // Match the receipts where all data passes the filter. - if (storedReceipt.Logs.Any(log => BloomExtensions.Test(log.GetBloom(), address, topics))) + if (storedReceipt.Logs.Any(log => BloomExtensions.Test(log.GetBloom(), addresses, topics))) + { receiptResponses.Add(storedReceipt); + } } return receiptResponses; } } -} \ No newline at end of file +} diff --git a/src/Stratis.SmartContracts.Core/Receipts/ReceiptSearcher.cs b/src/Stratis.SmartContracts.Core/Receipts/ReceiptSearcher.cs index 8b4cc51ffe..ccf0569cde 100644 --- a/src/Stratis.SmartContracts.Core/Receipts/ReceiptSearcher.cs +++ b/src/Stratis.SmartContracts.Core/Receipts/ReceiptSearcher.cs @@ -26,6 +26,23 @@ public ReceiptSearcher(ChainIndexer chainIndexer, IBlockStore blockStore, IRecei this.matcher = new ReceiptMatcher(); } + public List SearchReceipts(string eventName, int fromBlock, int? toBlock, IEnumerable topics) + { + var topicsList = new List(); + + if (!string.IsNullOrWhiteSpace(eventName)) + { + topicsList.Add(Encoding.UTF8.GetBytes(eventName)); + } + + if (topics != null) + { + topicsList.AddRange(topics); + } + + return this.SearchReceipts((HashSet)null, fromBlock, toBlock, topicsList); + } + public List SearchReceipts(string contractAddress, string eventName, int fromBlock, int? toBlock, IEnumerable topics) { var topicsList = new List(); @@ -40,27 +57,61 @@ public List SearchReceipts(string contractAddress, string eventName, in topicsList.AddRange(topics); } - return this.SearchReceipts(contractAddress, fromBlock, toBlock, topicsList); + return this.SearchReceipts(new HashSet() { contractAddress }, fromBlock, toBlock, topicsList); } - public List SearchReceipts(string contractAddress, int fromBlock = 0, int? toBlock = null, IEnumerable topics = null) + public List SearchReceipts(HashSet contractAddresses, string eventName, int fromBlock, int? toBlock, IEnumerable topics) + { + var topicsList = new List(); + + if (!string.IsNullOrWhiteSpace(eventName)) + { + topicsList.Add(Encoding.UTF8.GetBytes(eventName)); + } + + if (topics != null) + { + topicsList.AddRange(topics); + } + + return this.SearchReceipts(contractAddresses, fromBlock, toBlock, topicsList); + } + + public List SearchReceipts(HashSet contractAddresses, int fromBlock = 0, int? toBlock = null, IEnumerable topics = null) { topics = topics?.Where(topic => topic != null) ?? Enumerable.Empty(); - // Build the bytes we can use to check for this event. - // TODO use address.ToUint160 extension when it is in .Core. - var addressUint160 = new uint160(new BitcoinPubKeyAddress(contractAddress, this.network).Hash.ToBytes()); + // Ensure that we perform the Keccak256 hash calculations only once before entering the loop. + // This leads to "only" a two-fold speed improvement mostly because the db header retrieval is still slow. + var filterBloom = new Bloom(); + + var addressesUint160 = new HashSet(); - var chainIndexerRangeQuery = new ChainIndexerRangeQuery(this.chainIndexer); + if (contractAddresses != null) + { + foreach (string contractAddress in contractAddresses) + { + // Build the bytes we can use to check for this event. + // TODO use address.ToUint160 extension when it is in .Core. + var addressUint160 = new uint160(new BitcoinPubKeyAddress(contractAddress, this.network).Hash.ToBytes()); + + addressesUint160.Add(addressUint160); + filterBloom.Add(addressUint160.ToBytes()); + } + } - // WORKAROUND - // This is a workaround due to the BlockStore.GetBlocks returning null for genesis. - // We don't ever expect any receipts in the genesis block, so it's safe to ignore it. - if (fromBlock == 0) - fromBlock = 1; + foreach (byte[] topic in topics) + { + if (topic != null) + { + filterBloom.Add(topic); + } + } - // Loop through all headers and check bloom. - IEnumerable blockHeaders = chainIndexerRangeQuery.EnumerateRange(fromBlock, toBlock); + IEnumerable blockHeaders = this.chainIndexer[toBlock ?? this.chainIndexer.Tip.Height] + .EnumerateToGenesis() + .TakeWhile(c => c.Height >= fromBlock) + .Reverse(); // Match the blocks where the combination of all receipts passes the filter. var matches = new List(); @@ -68,7 +119,7 @@ public List SearchReceipts(string contractAddress, int fromBlock = 0, i { var scHeader = (ISmartContractBlockHeader)chainedHeader.Header; - if (scHeader.LogsBloom.Test(addressUint160, topics)) + if (scHeader.LogsBloom.Test(filterBloom)) matches.Add(chainedHeader); } @@ -84,7 +135,7 @@ public List SearchReceipts(string contractAddress, int fromBlock = 0, i IList receipts = this.receiptRepository.RetrieveMany(transactionHashes); // For each block, get all receipts, and if they match, add to list to return. - return this.matcher.MatchReceipts(receipts, addressUint160, topics); + return this.matcher.MatchReceipts(receipts, addressesUint160, topics); } } -} \ No newline at end of file +} diff --git a/src/Stratis.SmartContracts.Core/Stratis.SmartContracts.Core.csproj b/src/Stratis.SmartContracts.Core/Stratis.SmartContracts.Core.csproj index c29d7cef28..570993eddd 100644 --- a/src/Stratis.SmartContracts.Core/Stratis.SmartContracts.Core.csproj +++ b/src/Stratis.SmartContracts.Core/Stratis.SmartContracts.Core.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 2.0.1.0 Stratis Group Ltd. diff --git a/src/Stratis.SmartContracts.IntegrationTests/PoW/SmartContractWalletTests.cs b/src/Stratis.SmartContracts.IntegrationTests/PoW/SmartContractWalletTests.cs index fe4da3c21e..d9589fb732 100644 --- a/src/Stratis.SmartContracts.IntegrationTests/PoW/SmartContractWalletTests.cs +++ b/src/Stratis.SmartContracts.IntegrationTests/PoW/SmartContractWalletTests.cs @@ -132,8 +132,12 @@ public void SendAndReceiveSmartContractTransactions() * * Until we update the SmartContractsController to retrieve only mature transactions, we need this test. */ + + /// + /// SmartContractsController_Builds_Transaction_With_Minimum_Inputs + /// [Fact] - public void SmartContractsController_Builds_Transaction_With_Minimum_Inputs() + public void BuildSmartContractControllerTransaction() { using (SmartContractNodeBuilder builder = SmartContractNodeBuilder.Create(this)) { diff --git a/src/Stratis.SmartContracts.IntegrationTests/Stratis.SmartContracts.IntegrationTests.csproj b/src/Stratis.SmartContracts.IntegrationTests/Stratis.SmartContracts.IntegrationTests.csproj index b538256a41..9571e113b8 100644 --- a/src/Stratis.SmartContracts.IntegrationTests/Stratis.SmartContracts.IntegrationTests.csproj +++ b/src/Stratis.SmartContracts.IntegrationTests/Stratis.SmartContracts.IntegrationTests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.SmartContracts.IntegrationTests Stratis.SmartContracts.IntegrationTests true diff --git a/src/Stratis.SmartContracts.Networks/Stratis.SmartContracts.Networks.csproj b/src/Stratis.SmartContracts.Networks/Stratis.SmartContracts.Networks.csproj index 3ec5aaaf66..1c07ecdc3a 100644 --- a/src/Stratis.SmartContracts.Networks/Stratis.SmartContracts.Networks.csproj +++ b/src/Stratis.SmartContracts.Networks/Stratis.SmartContracts.Networks.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 2.0.1.0 Stratis Group Ltd. diff --git a/src/Stratis.SmartContracts.RuntimeObserver/Stratis.SmartContracts.RuntimeObserver.csproj b/src/Stratis.SmartContracts.RuntimeObserver/Stratis.SmartContracts.RuntimeObserver.csproj index 993f7c8b5d..2360847516 100644 --- a/src/Stratis.SmartContracts.RuntimeObserver/Stratis.SmartContracts.RuntimeObserver.csproj +++ b/src/Stratis.SmartContracts.RuntimeObserver/Stratis.SmartContracts.RuntimeObserver.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 2.0.0.0 Stratis Group Ltd. diff --git a/src/Stratis.SmartContracts.Tests.Common/Stratis.SmartContracts.Tests.Common.csproj b/src/Stratis.SmartContracts.Tests.Common/Stratis.SmartContracts.Tests.Common.csproj index 07bf141d2a..f10d8df1c0 100644 --- a/src/Stratis.SmartContracts.Tests.Common/Stratis.SmartContracts.Tests.Common.csproj +++ b/src/Stratis.SmartContracts.Tests.Common/Stratis.SmartContracts.Tests.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 2.0.1.0 diff --git a/src/Stratis.StraxD/Program.cs b/src/Stratis.StraxD/Program.cs index adb49ffcad..9a030900ab 100644 --- a/src/Stratis.StraxD/Program.cs +++ b/src/Stratis.StraxD/Program.cs @@ -47,7 +47,6 @@ public static async Task Main(string[] args) .AddSQLiteWalletRepository() .AddPowPosMining(true) .UseApi() - .UseUnity3dApi() .AddRPC() .AddSignalR(options => { diff --git a/src/Stratis.StraxD/Stratis.StraxD.csproj b/src/Stratis.StraxD/Stratis.StraxD.csproj index 3e3e88c4b5..6d227eae98 100644 --- a/src/Stratis.StraxD/Stratis.StraxD.csproj +++ b/src/Stratis.StraxD/Stratis.StraxD.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.StraxD Exe Stratis.StraxD @@ -16,7 +16,7 @@ latest - 1.3.2.4 + 1.4.0.7 Stratis Group Ltd. diff --git a/src/Stratis.StraxDnsD/Stratis.StraxDnsD.csproj b/src/Stratis.StraxDnsD/Stratis.StraxDnsD.csproj index 3936c62b9b..1c06e4b7e7 100644 --- a/src/Stratis.StraxDnsD/Stratis.StraxDnsD.csproj +++ b/src/Stratis.StraxDnsD/Stratis.StraxDnsD.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Stratis.StraxDnsD Exe Stratis.StraxDnsD @@ -16,7 +16,7 @@ latest - 1.3.2.4 + 1.4.0.7 Stratis Group Ltd. diff --git a/src/SwapExtractionTool/SwapExtractionTool.csproj b/src/SwapExtractionTool/SwapExtractionTool.csproj index 9cd13607f6..0321b70cdc 100644 --- a/src/SwapExtractionTool/SwapExtractionTool.csproj +++ b/src/SwapExtractionTool/SwapExtractionTool.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 diff --git a/src/global.json b/src/global.json index 4f52351c10..88f12e1ed3 100644 --- a/src/global.json +++ b/src/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.102", + "version": "6.0.100", "rollForward": "latestFeature" } } \ No newline at end of file