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..839a8ef652 100644 --- a/Docker/Stratis.StraxD.TestNet/Dockerfile +++ b/Docker/Stratis.StraxD.TestNet/Dockerfile @@ -1,11 +1,19 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build ARG APPVERSION=master ENV APPVERSION=${APPVERSION} RUN if [ "$APPVERSION" = "master" ]; then git clone https://github.com/stratisproject/StratisFullNode.git; else git clone https://github.com/stratisproject/StratisFullNode.git -b "release/${APPVERSION}"; fi RUN cd /StratisFullNode/src/Stratis.StraxD && dotnet build -VOLUME /root/.stratisfullnode -WORKDIR /StratisFullNode/src/Stratis.StraxD -EXPOSE 27103 27104 27105 -ENTRYPOINT ["dotnet", "run", "-testnet"] + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 + +COPY --from=build /StratisFullNode/src/Stratis.StraxD/bin/Debug/net6.0 . + +VOLUME /root/.stratisnode + +WORKDIR . + +EXPOSE 27102 27103 27104 27105 + +ENTRYPOINT ["dotnet", "Stratis.StraxD.dll", "-testnet"] \ No newline at end of file diff --git a/Docker/Stratis.StraxD/Dockerfile b/Docker/Stratis.StraxD/Dockerfile index 2d38bf768e..aeadc0731f 100644 --- a/Docker/Stratis.StraxD/Dockerfile +++ b/Docker/Stratis.StraxD/Dockerfile @@ -1,11 +1,19 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build ARG APPVERSION=master ENV APPVERSION=${APPVERSION} RUN if [ "$APPVERSION" = "master" ]; then git clone https://github.com/stratisproject/StratisFullNode.git; else git clone https://github.com/stratisproject/StratisFullNode.git -b "release/${APPVERSION}"; fi RUN cd /StratisFullNode/src/Stratis.StraxD && dotnet build -VOLUME /root/.stratisfullnode -WORKDIR /StratisFullNode/src/Stratis.StraxD -EXPOSE 17105 17104 17103 -ENTRYPOINT ["dotnet", "run"] \ No newline at end of file + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 + +COPY --from=build /StratisFullNode/src/Stratis.StraxD/bin/Debug/net6.0 . + +VOLUME /root/.stratisnode + +WORKDIR . + +EXPOSE 17102 17103 17104 17105 + +ENTRYPOINT ["dotnet", "Stratis.StraxD.dll"] \ No newline at end of file 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 8c2de30254..d98b76b910 100644 --- a/src/FederationSetup/FederationSetup.csproj +++ b/src/FederationSetup/FederationSetup.csproj @@ -2,8 +2,8 @@ Exe - netcoreapp3.1 - 1.5.0.0 + net6.0 + 1.6.0.0 Stratis Group Ltd. diff --git a/src/FodyNlogAdapter/FodyNlogAdapter.csproj b/src/FodyNlogAdapter/FodyNlogAdapter.csproj index c9b490c3c5..9183eb910e 100644 --- a/src/FodyNlogAdapter/FodyNlogAdapter.csproj +++ b/src/FodyNlogAdapter/FodyNlogAdapter.csproj @@ -1,9 +1,9 @@  - netcoreapp3.1 + net6.0 FodyNlogAdapter - 1.5.0.0 + 1.6.0.0 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 902cbcbf73..1521919b88 100644 --- a/src/NBitcoin/BlockStake.cs +++ b/src/NBitcoin/BlockStake.cs @@ -264,7 +264,7 @@ public override Transaction CreateTransaction(string hex) } /// - public virtual Transaction CreateTransaction(string hex, ProtocolVersion protocolVersion) + public override Transaction CreateTransaction(string hex, ProtocolVersion protocolVersion) { var transaction = new PosTransaction(); transaction.FromBytes(Encoders.Hex.DecodeData(hex), protocolVersion); diff --git a/src/NBitcoin/NBitcoin.csproj b/src/NBitcoin/NBitcoin.csproj index 94c88f8b75..11a75ce965 100644 --- a/src/NBitcoin/NBitcoin.csproj +++ b/src/NBitcoin/NBitcoin.csproj @@ -7,11 +7,11 @@ - 4.0.1.0 + 4.0.0.88 - netcoreapp3.1 + net6.0 NBitcoin NStratis NStratis 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 5c9ae72640..4d22ef6186 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 d50e608edf..a5fdd34076 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.5.0.0 + net6.0 + 1.6.0.0 Stratis Group Ltd. Stratis Group Ltd. 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 bc2fbb2a3d..243341ef71 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.5.0.0 + 1.6.0.0 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 8f4a05783f..9b9e431a02 100644 --- a/src/Stratis.Bitcoin.Features.BlockStore/AddressIndexing/AddressIndexer.cs +++ b/src/Stratis.Bitcoin.Features.BlockStore/AddressIndexing/AddressIndexer.cs @@ -16,9 +16,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; @@ -67,6 +69,7 @@ public class AddressIndexer : IAddressIndexer private readonly IAsyncProvider asyncProvider; private readonly IScriptAddressReader scriptAddressReader; + private readonly IBlockStore blockStore; private readonly TimeSpan flushChangesInterval; @@ -116,6 +119,8 @@ public class AddressIndexer : IAddressIndexer private readonly IUtxoIndexer utxoIndexer; + private readonly ISignals signals; + private Task indexingTask; private DateTime lastFlushTime; @@ -143,8 +148,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, IBlockStore blockStore, + IConsensusManager consensusManager, IAsyncProvider asyncProvider, ChainIndexer chainIndexer, IDateTimeProvider dateTimeProvider, IUtxoIndexer utxoIndexer, ISignals signals) { this.storeSettings = storeSettings; this.network = network; @@ -155,6 +160,7 @@ public AddressIndexer(StoreSettings storeSettings, DataFolder dataFolder, Networ this.dateTimeProvider = dateTimeProvider; this.utxoIndexer = utxoIndexer; this.scriptAddressReader = new ScriptAddressReader(); + this.signals = signals; this.blockStore = blockStore; this.lockObject = new object(); @@ -406,6 +412,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 98dcf07105..0e8098dec8 100644 --- a/src/Stratis.Bitcoin.Features.BlockStore/Controllers/BlockStoreController.cs +++ b/src/Stratis.Bitcoin.Features.BlockStore/Controllers/BlockStoreController.cs @@ -333,6 +333,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 90f2ab9489..ef314b16fb 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.5.0.0 + 1.6.0.0 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 d9155e3bd9..c3bf132171 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.5.0.0 + 1.6.0.0 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 b22235fc67..cee310c928 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.5.0.0 + 1.6.0.0 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 8274ed169f..6de031a95c 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.5.0.0 + 1.6.0.0 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 f527a9d640..8677544c98 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.5.0.0 + net6.0 + 1.6.0.0 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 af5bd70719..4b01143496 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.5.0.0 + net6.0 + 1.6.0.0 Stratis Group Ltd. Stratis.Features.Interop Stratis.Features.Interop 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 2e23848dca..0aeba8e022 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.5.0.0 + 1.6.0.0 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 84140c3f5c..23e9e7f0f0 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; } @@ -264,14 +273,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(); } @@ -307,7 +316,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); } } @@ -349,19 +364,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); } } @@ -388,14 +408,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)); @@ -405,7 +425,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) { @@ -479,6 +499,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); } } @@ -580,7 +601,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 afc533927f..e6bb13f99f 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.5.0.0 + 1.6.0.0 False library Stratis Group Ltd. 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 706605c14e..ade0c409a5 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.5.0.0 + 1.6.0.0 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 afaa2d30ac..ad86615e48 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.5.0.0 + 1.6.0.0 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 b86ab5740f..b064c8e73e 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.5.0.0 + 1.6.0.0 False diff --git a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoAMiner.cs b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoAMiner.cs index 6f6bb9beb1..7ebd2a3ee0 100644 --- a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoAMiner.cs +++ b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoAMiner.cs @@ -48,8 +48,26 @@ public TestPoAMiner( 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, signals, 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 9608704970..bea51b252b 100644 --- a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/EnableVoteKickingTests.cs +++ b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/EnableVoteKickingTests.cs @@ -32,8 +32,8 @@ public async Task EnableAutoKickAsync() targetSpacingSeconds: 60, votingEnabled: true, autoKickIdleMembers: false, - federationMemberMaxIdleTimeSeconds: oldOptions.FederationMemberMaxIdleTimeSeconds, - pollExpiryBlocks: 450); + pollExpiryBlocks: 450, + federationMemberMaxIdleTimeSeconds: oldOptions.FederationMemberMaxIdleTimeSeconds); CoreNode node1 = builder.CreatePoANode(votingNetwork1, votingNetwork1.FederationKey1).Start(); CoreNode node2 = builder.CreatePoANode(votingNetwork2, votingNetwork2.FederationKey2).Start(); @@ -55,8 +55,8 @@ public async Task EnableAutoKickAsync() targetSpacingSeconds: 60, votingEnabled: true, autoKickIdleMembers: true, - federationMemberMaxIdleTimeSeconds: idleTimeSeconds, - pollExpiryBlocks: 450); + pollExpiryBlocks: 450, + federationMemberMaxIdleTimeSeconds: idleTimeSeconds); // 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 index feb4ebb096..e6bb8a15f7 100644 --- a/src/Stratis.Bitcoin.Features.PoA/Events/MiningStatisticsEvent.cs +++ b/src/Stratis.Bitcoin.Features.PoA/Events/MiningStatisticsEvent.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Stratis.Bitcoin.EventBus; +using Stratis.Bitcoin.EventBus; namespace Stratis.Bitcoin.Features.PoA.Events { diff --git a/src/Stratis.Bitcoin.Features.PoA/PoAConsensusOptions.cs b/src/Stratis.Bitcoin.Features.PoA/PoAConsensusOptions.cs index bf04a33a46..eb41aae407 100644 --- a/src/Stratis.Bitcoin.Features.PoA/PoAConsensusOptions.cs +++ b/src/Stratis.Bitcoin.Features.PoA/PoAConsensusOptions.cs @@ -57,7 +57,15 @@ public class PoAConsensusOptions : ConsensusOptions /// /// The height at which Release 1.3.0.0 became BIP activated. /// - public int Release1300ActivationHeight { get; set; } + public int Release1300ActivationHeight { get; protected 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; protected set; } /// /// The height at which inituitive mining slots become active. @@ -110,6 +118,8 @@ public class PoAConsensusOptions : ConsensusOptions /// . /// . /// . + /// . + /// . public PoAConsensusOptions( uint maxBlockBaseSize, int maxStandardVersion, @@ -131,7 +141,8 @@ public PoAConsensusOptions( int? release1100ActivationHeight = null, int? pollExpiryBlocks = null, int? contractSerializerV2ActivationHeight = null, - int? release1300ActivationHeight = null) + int? release1300ActivationHeight = null, + int? release1400ActivationHeight = null) : base(maxBlockBaseSize, maxStandardVersion, maxStandardTxWeight, maxBlockSigopsCost, maxStandardTxSigopsCost, witnessScaleFactor: 1) { this.GenesisFederationMembers = genesisFederationMembers; @@ -161,6 +172,8 @@ public PoAConsensusOptions( this.ContractSerializerV2ActivationHeight = contractSerializerV2ActivationHeight.Value; if (release1300ActivationHeight.HasValue) this.Release1300ActivationHeight = release1300ActivationHeight.Value; + if (release1400ActivationHeight.HasValue) + this.Release1400ActivationHeight = release1400ActivationHeight.Value; if (this.AutoKickIdleMembers && !this.VotingEnabled) throw new ArgumentException("Voting should be enabled for automatic kicking to work."); diff --git a/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs b/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs index 29ff8ee9cf..0ddc4fd537 100644 --- a/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs +++ b/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs @@ -222,6 +222,8 @@ private void GatherMiningStatistics() this.miningStatisticsLog = log.ToString(); + this.signals?.Publish(new MiningStatisticsEvent(this.miningStatistics, 0)); + return; } @@ -233,6 +235,8 @@ private void GatherMiningStatistics() this.miningStatisticsLog = log.ToString(); + this.signals?.Publish(new MiningStatisticsEvent(this.miningStatistics, 0)); + return; } @@ -249,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 @@ -288,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 { @@ -304,15 +308,12 @@ private void GatherMiningStatistics() this.miningStatistics.MinerHits = hitCount; - if (this.signals != null) - { - this.signals.Publish(new MiningStatisticsEvent(this.miningStatistics, maxDepth)); - } + 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 815c59b99a..2892f703cb 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.5.0.0 + 1.6.0.0 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/Stratis.Bitcoin.Features.RPC.csproj b/src/Stratis.Bitcoin.Features.RPC/Stratis.Bitcoin.Features.RPC.csproj index a0e6992a17..633fbf207f 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.5.0.0 + 1.6.0.0 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.SignalR/DaemonConfiguration.cs b/src/Stratis.Bitcoin.Features.SignalR/DaemonConfiguration.cs index 0b16c82683..533814ea77 100644 --- a/src/Stratis.Bitcoin.Features.SignalR/DaemonConfiguration.cs +++ b/src/Stratis.Bitcoin.Features.SignalR/DaemonConfiguration.cs @@ -12,10 +12,13 @@ public static class DaemonConfiguration new FullNodeClientEvent(), new TransactionReceivedClientEvent(), new WalletProcessedTransactionOfInterestClientEvent(), + new MultiSigMemberStateRequestClientEvent(), new TransactionAddedToMemoryPoolClientEvent(), new MiningStatisticsClientEvent(), new ConsensusManagerStatusClientEvent(), - new PeerConnectionInfoClientEvent() + 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/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 index 4ff77b1e5d..53889ef330 100644 --- a/src/Stratis.Bitcoin.Features.SignalR/Events/MiningStatisticsClientEvent.cs +++ b/src/Stratis.Bitcoin.Features.SignalR/Events/MiningStatisticsClientEvent.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Stratis.Bitcoin.EventBus; using Stratis.Bitcoin.Features.PoA.Events; @@ -16,7 +14,7 @@ public class MiningStatisticsClientEvent : IClientEvent public void BuildFrom(EventBase @event) { if (@event is MiningStatisticsEvent miningStatisticsEvent) - { + { this.IsMining = miningStatisticsEvent.MiningStatistics.ProducedBlockInLastRound; this.BlockProducerHit = miningStatisticsEvent.MiningStatistics.MinerHits; this.FederationMemberSize = miningStatisticsEvent.FederationMemberSize; 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/Stratis.Bitcoin.Features.SignalR.csproj b/src/Stratis.Bitcoin.Features.SignalR/Stratis.Bitcoin.Features.SignalR.csproj index 8efd2a0e08..ca2a24efe2 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.5.0.0 + net6.0 + 1.6.0.0 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 b1a7cf0a8c..aaa6c9b338 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.5.0.0 + net6.0 + 1.6.0.0 Stratis Group Ltd. Stratis.Features.SmartContracts Stratis.Features.SmartContracts 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/Stratis.Bitcoin.Features.Wallet.csproj b/src/Stratis.Bitcoin.Features.Wallet/Stratis.Bitcoin.Features.Wallet.csproj index 84a690417f..eaf82bdac3 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.5.0.0 + 1.6.0.0 False Stratis Group Ltd. diff --git a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs index 89d40aeb77..cb753b73cb 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs @@ -898,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) @@ -909,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.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 09e8aba552..22fd3d7ffe 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.5.0.0 + 1.6.0.0 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 f1153d42d0..f7d04713c0 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.5.0.0 + 1.6.0.0 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/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/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.Networks/Stratis.Bitcoin.Networks.csproj b/src/Stratis.Bitcoin.Networks/Stratis.Bitcoin.Networks.csproj index 691d080289..591e26032b 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.5.0.0 - 1.5.0.0 - 1.5.0.0 + 1.6.0.0 + 1.6.0.0 + 1.6.0.0 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 5ea13aeac8..b3dc6cb979 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.5.0.0 + 1.6.0.0 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/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/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/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 index e84a06aa80..7e990011b1 100644 --- a/src/Stratis.Bitcoin/EventBus/CoreEvents/ConsensusManagerStatusEvent.cs +++ b/src/Stratis.Bitcoin/EventBus/CoreEvents/ConsensusManagerStatusEvent.cs @@ -6,8 +6,9 @@ namespace Stratis.Bitcoin.EventBus.CoreEvents { public class ConsensusManagerStatusEvent : EventBase { - public readonly bool IsIbd; - public readonly int? HeaderHeight; + public bool IsIbd { get; } + + public int? HeaderHeight { get; } public ConsensusManagerStatusEvent(bool isIbd, int? 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/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/Stratis.Bitcoin.csproj b/src/Stratis.Bitcoin/Stratis.Bitcoin.csproj index c8cdcc325a..3ad5182236 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.5.0.0-dev + 1.6.0.0 False ..\Stratis.ruleset Stratis Group Ltd. 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/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.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 eef020d565..2400b10194 100644 --- a/src/Stratis.CirrusD/Stratis.CirrusD.csproj +++ b/src/Stratis.CirrusD/Stratis.CirrusD.csproj @@ -2,8 +2,8 @@ Exe - netcoreapp3.1 - 1.5.0.0 + net6.0 + 1.6.0.0 Stratis Group Ltd. Stratis Group Ltd. diff --git a/src/Stratis.CirrusDnsD/Stratis.CirrusDnsD.csproj b/src/Stratis.CirrusDnsD/Stratis.CirrusDnsD.csproj index 98f497f6a4..55594d0081 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.5.0.0 + 1.6.0.0 diff --git a/src/Stratis.CirrusMinerD/Program.cs b/src/Stratis.CirrusMinerD/Program.cs index 172268bb38..0b9fe21943 100644 --- a/src/Stratis.CirrusMinerD/Program.cs +++ b/src/Stratis.CirrusMinerD/Program.cs @@ -181,7 +181,7 @@ private static IFullNode BuildStraxNode(string[] args) .AddPowPosMining(true) .AddSignalR(options => { - DaemonConfiguration.ConfigureSignalRForCirrus(options); + DaemonConfiguration.ConfigureSignalRForStrax(options); }) .Build(); diff --git a/src/Stratis.CirrusMinerD/Stratis.CirrusMinerD.csproj b/src/Stratis.CirrusMinerD/Stratis.CirrusMinerD.csproj index 00535cb8f4..10d6f80488 100644 --- a/src/Stratis.CirrusMinerD/Stratis.CirrusMinerD.csproj +++ b/src/Stratis.CirrusMinerD/Stratis.CirrusMinerD.csproj @@ -2,8 +2,8 @@ Exe - netcoreapp3.1 - 1.5.0.0 + net6.0 + 1.6.0.0 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 4bbb1ca6f9..ce99a3988b 100644 --- a/src/Stratis.CirrusPegD/Stratis.CirrusPegD.csproj +++ b/src/Stratis.CirrusPegD/Stratis.CirrusPegD.csproj @@ -2,14 +2,15 @@ Exe - netcoreapp3.1 - 1.5.0.0 + net6.0 + 1.6.0.0 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..e17c405dd7 100644 --- a/src/Stratis.External.Masternodes/Stratis.External.Masternodes.csproj +++ b/src/Stratis.External.Masternodes/Stratis.External.Masternodes.csproj @@ -2,12 +2,12 @@ Exe - netcoreapp3.1 + net6.0 - + diff --git a/src/Stratis.Features.Collateral/CollateralPoAMiner.cs b/src/Stratis.Features.Collateral/CollateralPoAMiner.cs index 0697a34af3..97884be2b7 100644 --- a/src/Stratis.Features.Collateral/CollateralPoAMiner.cs +++ b/src/Stratis.Features.Collateral/CollateralPoAMiner.cs @@ -39,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, 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) + VotingManager votingManager, PoASettings poAMinerSettings, ICollateralChecker collateralChecker, IAsyncProvider asyncProvider, ICounterChainSettings counterChainSettings, 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, 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 bfe4b66e52..136894a2bd 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.1.0.0 + net6.0 + 4.1.0.1 Stratis Group Ltd. diff --git a/src/Stratis.Features.Diagnostic/Stratis.Features.Diagnostic.csproj b/src/Stratis.Features.Diagnostic/Stratis.Features.Diagnostic.csproj index 4f75c1723a..dd16ab2821 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.5.0.0 + 1.6.0.0 Stratis Group Ltd. diff --git a/src/Stratis.Features.FederatedPeg.IntegrationTests/NodeInitialisationTests.cs b/src/Stratis.Features.FederatedPeg.IntegrationTests/NodeInitialisationTests.cs index 7bfb9a20d0..466fa0f8e4 100644 --- a/src/Stratis.Features.FederatedPeg.IntegrationTests/NodeInitialisationTests.cs +++ b/src/Stratis.Features.FederatedPeg.IntegrationTests/NodeInitialisationTests.cs @@ -133,7 +133,7 @@ public void MainChainGatewayStarts() [Fact] public void MinerPairStarts() { - CirrusRegTest collateralSidechainNetwork = new CirrusSingleCollateralRegTest(this.mainNetwork); + CirrusRegTest collateralSidechainNetwork = new CirrusSingleCollateralRegTest(this.sidechainNetwork); using var sideNodeBuilder = SidechainNodeBuilder.CreateSidechainNodeBuilder(this); using var nodeBuilder = NodeBuilder.Create(this); @@ -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(this.sidechainNetwork); + + 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/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 b3fd7f9ffc..794aff4fdc 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/Distribution/RewardClaimerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/Distribution/RewardClaimerTests.cs @@ -100,7 +100,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); } } @@ -131,7 +131,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/MaturedBlocksProviderTests.cs b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs index 8a7e47b598..a7584dbed1 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs @@ -55,8 +55,10 @@ public MaturedBlocksProviderTests() this.network = new CirrusRegTest(); this.mainChainNetwork = new StraxRegTest(); + int oldValue = ((PoAConsensusOptions)this.network.Consensus.Options).Release1300ActivationHeight; + // TODO: Upgrade these tests to conform with release 1.3.0.0 activation. - ((PoAConsensusOptions)this.network.Consensus.Options).Release1300ActivationHeight = int.MaxValue; + ((PoAConsensusOptions)this.network.Consensus.Options).SetPrivatePropertyValue("Release1300ActivationHeight", int.MaxValue); this.opReturnDataReader = Substitute.For(); this.opReturnDataReader.TryGetTargetAddress(null, out string address).Returns(callInfo => { callInfo[1] = null; return false; }); @@ -89,6 +91,8 @@ public MaturedBlocksProviderTests() }); this.retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), this.federatedPegSettings, null, null); + + ((PoAConsensusOptions)this.network.Consensus.Options).SetPrivatePropertyValue("Release1300ActivationHeight", oldValue); } [Fact] 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/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 d95f4463e7..17764895ee 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.1.0.0 + 4.1.0.1 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..81dbd50cc3 100644 --- a/src/Stratis.Features.FederatedPeg/Wallet/MultiSigTransactions.cs +++ b/src/Stratis.Features.FederatedPeg/Wallet/MultiSigTransactions.cs @@ -31,6 +31,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/Stratis.Features.SQLiteWalletRepository.csproj b/src/Stratis.Features.SQLiteWalletRepository/Stratis.Features.SQLiteWalletRepository.csproj index 2f59152562..9fb2200065 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.5.0.0 + 1.6.0.0 False Stratis Group Ltd. diff --git a/src/Stratis.Features.Unity3dApi/LocalCallContract.cs b/src/Stratis.Features.Unity3dApi/LocalCallContract.cs index e82a6d4a4b..2e0c5fe531 100644 --- a/src/Stratis.Features.Unity3dApi/LocalCallContract.cs +++ b/src/Stratis.Features.Unity3dApi/LocalCallContract.cs @@ -13,7 +13,7 @@ namespace Stratis.Features.Unity3dApi { - // TODO: Move this to a more central point once 1.5.0.0 stabilises + // TODO: Move this to a more central point once 1.6.0.0 stabilises public interface ILocalCallContract { LocalExecutionResponse LocalCallSmartContract(LocalCallContractRequest request); diff --git a/src/Stratis.Features.Unity3dApi/NFTTransferIndexer.cs b/src/Stratis.Features.Unity3dApi/NFTTransferIndexer.cs index 3646ddb8b9..a6b48709fb 100644 --- a/src/Stratis.Features.Unity3dApi/NFTTransferIndexer.cs +++ b/src/Stratis.Features.Unity3dApi/NFTTransferIndexer.cs @@ -115,7 +115,7 @@ public void Initialize() this.logger.LogInformation("Finished building cache of known contract addresses."); - this.indexingLoop = this.asyncProvider.CreateAndRunAsyncLoop("IndexNftsContinuously", async (cancellationTokenSource) => + this.indexingLoop = this.asyncProvider.CreateAndRunAsyncLoop(nameof(IndexNFTsContinuouslyAsync), async (cancellationTokenSource) => { try { @@ -201,8 +201,6 @@ public NFTContractModel GetAllNFTOwnersByContractAddress(string contractAddress) /// public void ReindexAllContracts() { - this.logger.LogTrace("ReindexAllContracts()"); - var updated = new List(); foreach (NFTContractModel contractModel in this.NFTContractCollection.FindAll().ToList()) @@ -216,7 +214,7 @@ public void ReindexAllContracts() this.UpdateLastUpdatedBlock(GetWatchFromHeight()); - this.logger.LogTrace("ReindexAllContracts(-)"); + this.logger.LogInformation($"A re-index of all contracts will be triggered from block {GetWatchFromHeight()}."); } /// @@ -277,7 +275,7 @@ private async Task IndexNFTsContinuouslyAsync() break; } - this.logger.LogDebug("Found new NFT contract: " + logResponse.Address); + this.logger.LogInformation($"Found new NFT contract '{logResponse.Address}'"); this.knownContracts.Add(logResponse.Address); @@ -296,7 +294,10 @@ private async Task IndexNFTsContinuouslyAsync() this.logger.LogDebug("Log from: {0}, to: {1}, ID: {2}", transferInfo.From, transferInfo.To, transferInfo.TokenId); - NFTContractModel currentContract = this.NFTContractCollection.FindOne(c => c.ContractAddress == logResponse.Address); + // 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 ((transferInfo.From != null) && currentContract.OwnedIDsByAddress.ContainsKey(transferInfo.From)) { 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 fc244f0a1d..ca1f42f502 100644 --- a/src/Stratis.Features.Unity3dApi/Unity3dApiSettings.cs +++ b/src/Stratis.Features.Unity3dApi/Unity3dApiSettings.cs @@ -109,7 +109,7 @@ public Unity3dApiSettings(NodeSettings nodeSettings) }; } - this.LocalCallSenderAddress = config.GetOrDefault("unityapi_localcallsenderaddress", (string)null); + 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."); diff --git a/src/Stratis.FullNode.sln b/src/Stratis.FullNode.sln index 5c5e114540..875a8d413d 100644 --- a/src/Stratis.FullNode.sln +++ b/src/Stratis.FullNode.sln @@ -189,7 +189,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Features.Unity3dApi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Bitcoin.Features.ExternalApi", "Stratis.Bitcoin.Features.ExternalAPI\Stratis.Bitcoin.Features.ExternalApi.csproj", "{21D41C53-62D8-4F68-A3D1-88BB2AB195E3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stratis.Interop.Contracts", "Stratis.Interop.Contracts\Stratis.Interop.Contracts.csproj", "{3A82CB5E-FB80-4A19-9223-AF25123F7288}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Interop.Contracts", "Stratis.Interop.Contracts\Stratis.Interop.Contracts.csproj", "{3A82CB5E-FB80-4A19-9223-AF25123F7288}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.SCL", "Stratis.SCL\Stratis.SCL.csproj", "{B80F392A-10CD-4A19-9B55-A7FA477533FC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -505,6 +507,10 @@ Global {3A82CB5E-FB80-4A19-9223-AF25123F7288}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A82CB5E-FB80-4A19-9223-AF25123F7288}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A82CB5E-FB80-4A19-9223-AF25123F7288}.Release|Any CPU.Build.0 = Release|Any CPU + {B80F392A-10CD-4A19-9B55-A7FA477533FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B80F392A-10CD-4A19-9B55-A7FA477533FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B80F392A-10CD-4A19-9B55-A7FA477533FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B80F392A-10CD-4A19-9B55-A7FA477533FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -577,6 +583,7 @@ Global {B08D2057-F48D-4E72-99F4-95A35E6E0DFD} = {15D29FFD-6142-4DC5-AFFD-10BA0CA55C45} {21D41C53-62D8-4F68-A3D1-88BB2AB195E3} = {15D29FFD-6142-4DC5-AFFD-10BA0CA55C45} {3A82CB5E-FB80-4A19-9223-AF25123F7288} = {1B9A916F-DDAC-4675-B424-EDEDC1A58231} + {B80F392A-10CD-4A19-9B55-A7FA477533FC} = {1B9A916F-DDAC-4675-B424-EDEDC1A58231} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6C780ABA-5872-4B83-AD3F-A5BD423AD907} 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.SCL/ECRecover.cs b/src/Stratis.SCL/ECRecover.cs new file mode 100644 index 0000000000..0208f51462 --- /dev/null +++ b/src/Stratis.SCL/ECRecover.cs @@ -0,0 +1,56 @@ +using System; +using NBitcoin; +using Stratis.SmartContracts; + +namespace Stratis.SCL.Crypto +{ + public static class ECRecover + { + /// + /// Retrieves the address of the signer of an ECDSA signature. + /// + /// The message that was used to compute a hash that was signed. + /// The ECDSA signature prepended with header information specifying the correct value of recId. + /// The Address for the signer of a signature. + /// A bool representing whether or not the signer was retrieved successfully. + public static bool TryGetSigner(byte[] message, byte[] signature, out Address address) + { + address = Address.Zero; + + if (message == null || signature == null) + return false; + + // NBitcoin is very throwy + try + { + uint256 hashedUint256 = GetUint256FromMessage(message); + + PubKey pubKey = PubKey.RecoverCompact(hashedUint256, signature); + + address = CreateAddress(pubKey.Hash.ToBytes()); + + return true; + } + catch + { + return false; + } + } + + private static uint256 GetUint256FromMessage(byte[] message) + { + return new uint256(SHA3.Keccak256(message)); + } + + private static Address CreateAddress(byte[] bytes) + { + uint pn0 = BitConverter.ToUInt32(bytes, 0); + uint pn1 = BitConverter.ToUInt32(bytes, 4); + uint pn2 = BitConverter.ToUInt32(bytes, 8); + uint pn3 = BitConverter.ToUInt32(bytes, 12); + uint pn4 = BitConverter.ToUInt32(bytes, 16); + + return new Address(pn0, pn1, pn2, pn3, pn4); + } + } +} \ No newline at end of file diff --git a/src/Stratis.SCL/Operations.cs b/src/Stratis.SCL/Operations.cs new file mode 100644 index 0000000000..464fa45320 --- /dev/null +++ b/src/Stratis.SCL/Operations.cs @@ -0,0 +1,9 @@ +using System; + +namespace Stratis.SCL.Base +{ + public static class Operations + { + public static void Noop() { } + } +} diff --git a/src/Stratis.SCL/SHA3.cs b/src/Stratis.SCL/SHA3.cs new file mode 100644 index 0000000000..8e1071dc3d --- /dev/null +++ b/src/Stratis.SCL/SHA3.cs @@ -0,0 +1,17 @@ +using HashLib; + +namespace Stratis.SCL.Crypto +{ + public static class SHA3 + { + /// + /// Returns a 32-byte Keccak256 hash of the given bytes. + /// + /// + /// + public static byte[] Keccak256(byte[] input) + { + return HashFactory.Crypto.SHA3.CreateKeccak256().ComputeBytes(input).GetBytes(); + } + } +} diff --git a/src/Stratis.SCL/SSAS.cs b/src/Stratis.SCL/SSAS.cs new file mode 100644 index 0000000000..12001b6d7c --- /dev/null +++ b/src/Stratis.SCL/SSAS.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NBitcoin; +using NBitcoin.DataEncoders; +using Stratis.SmartContracts; + +namespace Stratis.SCL.Crypto +{ + public static class SSAS + { + class ChameleonNetwork : Network + { + public ChameleonNetwork(byte base58Prefix) + { + this.Base58Prefixes = new byte[][] { new byte[] { base58Prefix } }; + } + } + + public static byte[] ParseAddress(string address, out byte prefix) + { + prefix = (new Base58Encoder()).DecodeData(address)[0]; + var bitcoinAddress = BitcoinAddress.Create(address, new ChameleonNetwork(prefix)); + var pubKeyHash = ((BitcoinPubKeyAddress)bitcoinAddress).Hash; + + return pubKeyHash.ToBytes(); + } + + public static string[] GetURLArguments(string url, string[] argumentNames) + { + // Create a mapping of available url arguments. + Dictionary argDict = ParseQueryString(url); + + return argumentNames.Select(argName => argDict.TryGetValue(argName, out string argValue) ? argValue : null).ToArray(); + } + + /// + /// Retrieves the address of the signer of an ECDSA signature. + /// + /// The message that was signed. + /// The ECDSA signature prepended with header information specifying the correct value of recId. + /// The Address for the signer of a signature. + /// A bool representing whether or not the signer was retrieved successfully. + public static bool TryGetSignerSHA256(byte[] message, byte[] signature, out Address address) + { + address = Address.Zero; + + if (message == null || signature == null) + return false; + + // NBitcoin is very throwy + try + { + PubKey pubKey = PubKey.RecoverFromMessage(message, Convert.ToBase64String(signature)); + + address = CreateAddress(pubKey.Hash.ToBytes()); + + return true; + } + catch + { + return false; + } + } + + private static Address CreateAddress(byte[] bytes) + { + uint pn0 = BitConverter.ToUInt32(bytes, 0); + uint pn1 = BitConverter.ToUInt32(bytes, 4); + uint pn2 = BitConverter.ToUInt32(bytes, 8); + uint pn3 = BitConverter.ToUInt32(bytes, 12); + uint pn4 = BitConverter.ToUInt32(bytes, 16); + + return new Address(pn0, pn1, pn2, pn3, pn4); + } + + private static Dictionary ParseQueryString(string queryString) + { + Dictionary result = new Dictionary(); + + int startOfQueryString = queryString.IndexOf('?') + 1; + + if (!string.IsNullOrEmpty(queryString) && startOfQueryString != 0) + { + // Remove the '?' at the start of the query string + queryString = queryString.Substring(startOfQueryString); + + foreach (var part in queryString.Split('&')) + { + var keyValue = part.Split('='); + + if (keyValue.Length == 2) + { + result[keyValue[0]] = Uri.UnescapeDataString(keyValue[1]); + } + } + } + + return result; + } + } +} diff --git a/src/Stratis.SCL/Stratis.SCL.csproj b/src/Stratis.SCL/Stratis.SCL.csproj new file mode 100644 index 0000000000..da4381a498 --- /dev/null +++ b/src/Stratis.SCL/Stratis.SCL.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + + 2.1.0.0 + 2.1.0.0 + 2.1.0.0-beta + Stratis Group Ltd. + Stratis.SCL + + + + + + + + + + + + + + diff --git a/src/Stratis.Sidechains.Networks/CirrusMain.cs b/src/Stratis.Sidechains.Networks/CirrusMain.cs index 0702dd400e..a48c34fcd3 100644 --- a/src/Stratis.Sidechains.Networks/CirrusMain.cs +++ b/src/Stratis.Sidechains.Networks/CirrusMain.cs @@ -183,7 +183,8 @@ public CirrusMain() release1100ActivationHeight: 3_426_950, // Monday, 20 December 2021 10:00:00 AM (Estimated) pollExpiryBlocks: 50_000, // Roughly 9 days 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 7c446e208a..0ebac04561 100644 --- a/src/Stratis.Sidechains.Networks/CirrusTest.cs +++ b/src/Stratis.Sidechains.Networks/CirrusTest.cs @@ -134,7 +134,8 @@ public CirrusTest() getMiningTimestampV2ActivationStrictHeight: 3_121_500, // 17 January 2022 pollExpiryBlocks: 450, // 2 hours 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 ad387009d2..eac179c3ba 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.5.0.0 + 1.6.0.0 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/ContractCompilerTests.cs b/src/Stratis.SmartContracts.CLR.Tests/ContractCompilerTests.cs index 5a30e83cc5..7976d4fadc 100644 --- a/src/Stratis.SmartContracts.CLR.Tests/ContractCompilerTests.cs +++ b/src/Stratis.SmartContracts.CLR.Tests/ContractCompilerTests.cs @@ -33,12 +33,13 @@ public void SmartContract_ReferenceResolver_HasCorrectAssemblies() { List allowedAssemblies = ReferencedAssemblyResolver.AllowedAssemblies.ToList(); - Assert.Equal(5, allowedAssemblies.Count); + Assert.Equal(6, allowedAssemblies.Count); Assert.Contains(allowedAssemblies, a => a.GetName().Name == "System.Runtime"); Assert.Contains(allowedAssemblies, a => a.GetName().Name == "System.Private.CoreLib"); Assert.Contains(allowedAssemblies, a => a.GetName().Name == "Stratis.SmartContracts"); Assert.Contains(allowedAssemblies, a => a.GetName().Name == "System.Linq"); Assert.Contains(allowedAssemblies, a => a.GetName().Name == "Stratis.SmartContracts.Standards"); + Assert.Contains(allowedAssemblies, a => a.GetName().Name == "Stratis.SCL"); } [Fact] diff --git a/src/Stratis.SmartContracts.CLR.Tests/ContractExecutorTests.cs b/src/Stratis.SmartContracts.CLR.Tests/ContractExecutorTests.cs index 19ae959cdd..bbcee578ac 100644 --- a/src/Stratis.SmartContracts.CLR.Tests/ContractExecutorTests.cs +++ b/src/Stratis.SmartContracts.CLR.Tests/ContractExecutorTests.cs @@ -333,6 +333,12 @@ public void Execute_MultipleIfElseBlocks_ExecutionSucceeds() AssertSuccessfulContractMethodExecution(nameof(MultipleIfElseBlocks), nameof(MultipleIfElseBlocks.PersistNormalizeValue), new object[] { "z" }); } + [Fact] + public void Execute_LibraryContract_ExecutionSucceeds() + { + AssertSuccessfulContractMethodExecution(nameof(LibraryTest), nameof(LibraryTest.Exists)); + } + private void AssertSuccessfulContractMethodExecution(string contractName, string methodName, object[] methodParameters = null, string expectedReturn = null) { var transactionValue = (Money)100; diff --git a/src/Stratis.SmartContracts.CLR.Tests/SmartContracts/LibraryTest.cs b/src/Stratis.SmartContracts.CLR.Tests/SmartContracts/LibraryTest.cs new file mode 100644 index 0000000000..9b48635de2 --- /dev/null +++ b/src/Stratis.SmartContracts.CLR.Tests/SmartContracts/LibraryTest.cs @@ -0,0 +1,16 @@ +using Stratis.SmartContracts; +using Base = Stratis.SCL.Base; + +[Deploy] +public class LibraryTest : SmartContract +{ + public LibraryTest(ISmartContractState state) : base(state) + { + Base.Operations.Noop(); + } + + public void Exists() + { + State.SetBool("Exists", true); + } +} 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..1b6a8a40d5 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 @@ -74,6 +74,9 @@ Always + + Always + PreserveNewest diff --git a/src/Stratis.SmartContracts.CLR.Validation.Tests/SmartContractDeterminismValidatorTests.cs b/src/Stratis.SmartContracts.CLR.Validation.Tests/SmartContractDeterminismValidatorTests.cs index 5971b9029c..9ed64d361c 100644 --- a/src/Stratis.SmartContracts.CLR.Validation.Tests/SmartContractDeterminismValidatorTests.cs +++ b/src/Stratis.SmartContracts.CLR.Validation.Tests/SmartContractDeterminismValidatorTests.cs @@ -834,6 +834,44 @@ public Test(ISmartContractState state): base(state) Assert.False(result.IsValid); Assert.NotEmpty(result.Errors); Assert.True(result.Errors.All(e => e is ModuleDefinitionValidationResult)); - } + } + + [Fact] + public void SmartContractValidator_Allows_SCL() + { + var adjustedSource = @" +using Stratis.SmartContracts; +using Base = Stratis.SCL.Base; + +[Deploy] +public class LibraryTest : SmartContract +{ + public LibraryTest(ISmartContractState state) : base(state) + { + Base.Operations.Noop(); + } + + public void Exists() + { + State.SetBool(""Exists"", true); + } +}"; + ContractCompilationResult compilationResult = ContractCompiler.Compile(adjustedSource); + Assert.True(compilationResult.Success); + + byte[] assemblyBytes = compilationResult.Compilation; + IContractModuleDefinition decompilation = ContractDecompiler.GetModuleDefinition(assemblyBytes).Value; + + // Add a module reference + decompilation.ModuleDefinition.ModuleReferences.Add(new ModuleReference("Test.dll")); + + var moduleDefinition = decompilation.ModuleDefinition; + + SmartContractValidationResult result = new SmartContractValidator().Validate(moduleDefinition); + + Assert.False(result.IsValid); + Assert.NotEmpty(result.Errors); + Assert.True(result.Errors.All(e => e is ModuleDefinitionValidationResult)); + } } } \ No newline at end of file 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/DeterminismPolicy.cs b/src/Stratis.SmartContracts.CLR.Validation/DeterminismPolicy.cs index 28b28af563..50d440fcfc 100644 --- a/src/Stratis.SmartContracts.CLR.Validation/DeterminismPolicy.cs +++ b/src/Stratis.SmartContracts.CLR.Validation/DeterminismPolicy.cs @@ -14,7 +14,9 @@ public static class DeterminismPolicy public static WhitelistPolicy WhitelistPolicy = new WhitelistPolicy() .Namespace(nameof(System), AccessPolicy.Denied, SystemPolicy) .Namespace(typeof(RuntimeHelpers).Namespace, AccessPolicy.Denied, CompilerServicesPolicy) - .Namespace(typeof(SmartContract).Namespace, AccessPolicy.Allowed, SmartContractsPolicy); + .Namespace(typeof(SmartContract).Namespace, AccessPolicy.Allowed, SmartContractsPolicy) + .Namespace(typeof(SCL.Crypto.ECRecover).Namespace, AccessPolicy.Allowed, SmartContractsPolicy) + .Namespace(typeof(SCL.Base.Operations).Namespace, AccessPolicy.Allowed, SmartContractsPolicy); public static ValidationPolicy Default = new ValidationPolicy() .WhitelistValidator(WhitelistPolicy) diff --git a/src/Stratis.SmartContracts.CLR.Validation/FormatPolicy.cs b/src/Stratis.SmartContracts.CLR.Validation/FormatPolicy.cs index 037c833857..f729a94538 100644 --- a/src/Stratis.SmartContracts.CLR.Validation/FormatPolicy.cs +++ b/src/Stratis.SmartContracts.CLR.Validation/FormatPolicy.cs @@ -5,6 +5,7 @@ using Stratis.SmartContracts.CLR.Validation.Validators.Module; using Stratis.SmartContracts.CLR.Validation.Validators.Type; using Stratis.SmartContracts.Standards; +using Stratis.SCL.Crypto; namespace Stratis.SmartContracts.CLR.Validation { @@ -23,7 +24,8 @@ public static class FormatPolicy Core, typeof(SmartContract).Assembly, typeof(Enumerable).Assembly, - typeof(IStandardToken).Assembly + typeof(IStandardToken).Assembly, + typeof(SCL.Base.Operations).Assembly, }; public static ValidationPolicy Default = new ValidationPolicy() 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..460f52ec2c 100644 --- a/src/Stratis.SmartContracts.CLR.Validation/Stratis.SmartContracts.CLR.Validation.csproj +++ b/src/Stratis.SmartContracts.CLR.Validation/Stratis.SmartContracts.CLR.Validation.csproj @@ -1,16 +1,20 @@  - netcoreapp3.1 + net6.0 - 2.0.1.0 + 2.1.0.0-beta Stratis Group Ltd. - + + + + + diff --git a/src/Stratis.SmartContracts.CLR/Compilation/ReferencedAssemblyResolver.cs b/src/Stratis.SmartContracts.CLR/Compilation/ReferencedAssemblyResolver.cs index 76a05075a0..ecf6621853 100644 --- a/src/Stratis.SmartContracts.CLR/Compilation/ReferencedAssemblyResolver.cs +++ b/src/Stratis.SmartContracts.CLR/Compilation/ReferencedAssemblyResolver.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Stratis.SCL; using Stratis.SmartContracts.Standards; namespace Stratis.SmartContracts.CLR.Compilation @@ -23,7 +24,8 @@ public static class ReferencedAssemblyResolver Core, typeof(SmartContract).Assembly, typeof(Enumerable).Assembly, - typeof(IStandardToken).Assembly + typeof(IStandardToken).Assembly, + typeof(SCL.Base.Operations).Assembly }; } } \ No newline at end of file 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 - 2.0.2.0 + 2.1.0.0 + 2.1.0.0 + 2.1.0.0-beta Stratis Group Ltd. Stratis.SmartContracts.CLR - + 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/Stratis.SmartContracts.Core.csproj b/src/Stratis.SmartContracts.Core/Stratis.SmartContracts.Core.csproj index c29d7cef28..a65d425719 100644 --- a/src/Stratis.SmartContracts.Core/Stratis.SmartContracts.Core.csproj +++ b/src/Stratis.SmartContracts.Core/Stratis.SmartContracts.Core.csproj @@ -1,9 +1,9 @@  - netcoreapp3.1 + net6.0 - 2.0.1.0 + 2.1.0.0-beta Stratis Group Ltd. Stratis Group Ltd. diff --git a/src/Stratis.SmartContracts.IntegrationTests/ECRecoverTests.cs b/src/Stratis.SmartContracts.IntegrationTests/ECRecoverTests.cs new file mode 100644 index 0000000000..28404a729f --- /dev/null +++ b/src/Stratis.SmartContracts.IntegrationTests/ECRecoverTests.cs @@ -0,0 +1,179 @@ +using System.Threading.Tasks; +using NBitcoin; +using Stratis.Bitcoin.Features.SmartContracts.Models; +using Stratis.SCL.Crypto; +using Stratis.SmartContracts.CLR; +using Stratis.SmartContracts.CLR.Compilation; +using Stratis.SmartContracts.CLR.Serialization; +using Stratis.SmartContracts.Core; +using Stratis.SmartContracts.Networks; +using Stratis.SmartContracts.Tests.Common.MockChain; +using Xunit; +using EcRecoverProvider = Stratis.SCL.Crypto.ECRecover; +using Key = NBitcoin.Key; + +namespace Stratis.SmartContracts.IntegrationTests +{ + public class ECRecoverTests + { + // 2 things to test: + + // 1) That we have the ECDSA code and can make it available. + + [Fact] + public void CanSignAndRetrieveSender() + { + var network = new SmartContractsRegTest(); + var privateKey = new Key(); + Address address = privateKey.PubKey.GetAddress(network).ToString().ToAddress(network); + byte[] message = new byte[] { 0x69, 0x76, 0xAA }; + + // Sign a message + byte[] offChainSignature = SignMessage(privateKey, message); + + // Get the address out of the signature + EcRecoverProvider.TryGetSigner(message, offChainSignature, out Address recoveredAddress); + + // Check that the address matches that generated from the private key. + Assert.Equal(address, recoveredAddress); + } + + [Fact] + public void GetSigner_Returns_Address_Zero_When_Message_Or_Signature_Null() + { + var network = new SmartContractsRegTest(); + var privateKey = new Key(); + Address address = privateKey.PubKey.GetAddress(network).ToString().ToAddress(network); + byte[] message = new byte[] { 0x69, 0x76, 0xAA }; + + // Sign a message + byte[] offChainSignature = SignMessage(privateKey, message); + + // Get the address out of the signature + Assert.False(EcRecoverProvider.TryGetSigner(null, offChainSignature, out Address recoveredAddress)); + + Assert.Equal(Address.Zero, recoveredAddress); + + Assert.False(EcRecoverProvider.TryGetSigner(message, null, out Address recoveredAddress2)); + + Assert.Equal(Address.Zero, recoveredAddress2); + } + + /// + /// Signs a message, returning an ECDSA signature. + /// + /// The private key used to sign the message. + /// The complete message to be signed. + /// The ECDSA signature prepended with header information specifying the correct value of recId. + private static byte[] SignMessage(Key privateKey, byte[] message) + { + uint256 hashedUint256 = new uint256(SHA3.Keccak256(message)); + + return privateKey.SignCompact(hashedUint256); + } + + [Fact] + public void CanCallEcRecoverContractWithValidSignatureAsync() + { + using (PoWMockChain chain = new PoWMockChain(2)) + { + var node1 = chain.Nodes[0]; + + node1.MineBlocks(1); + + var network = chain.Nodes[0].CoreNode.FullNode.Network; + + var privateKey = new Key(); + string address = privateKey.PubKey.GetAddress(network).ToString(); + byte[] message = new byte[] { 0x69, 0x76, 0xAA }; + byte[] signature = SignMessage(privateKey, message); + + // TODO: If the incorrect parameters are passed to the constructor, the contract does not get properly created ('Method does not exist on contract'), but a success response is still returned? + + byte[] contract = ContractCompiler.CompileFile("SmartContracts/EcRecoverContract.cs").Compilation; + string[] createParameters = new string[] { string.Format("{0}#{1}", (int)MethodParameterDataType.Address, address) }; + BuildCreateContractTransactionResponse createResult = node1.SendCreateContractTransaction(contract, 1, createParameters); + + Assert.NotNull(createResult); + Assert.True(createResult.Success); + + node1.WaitMempoolCount(1); + node1.MineBlocks(1); + + string[] callParameters = new string[] + { + string.Format("{0}#{1}", (int)MethodParameterDataType.ByteArray, message.ToHexString()), + string.Format("{0}#{1}", (int)MethodParameterDataType.ByteArray, signature.ToHexString()) + }; + + BuildCallContractTransactionResponse response = node1.SendCallContractTransaction("CheckThirdPartySignature", createResult.NewContractAddress, 1, callParameters); + Assert.NotNull(response); + Assert.True(response.Success); + + node1.WaitMempoolCount(1); + node1.MineBlocks(1); + + ReceiptResponse receipt = node1.GetReceipt(response.TransactionId.ToString()); + + Assert.NotNull(receipt); + Assert.True(receipt.Success); + Assert.Equal("True", receipt.ReturnValue); + } + } + + [Fact] + public void CanCallEcRecoverContractWithInvalidSignatureAsync() + { + using (PoWMockChain chain = new PoWMockChain(2)) + { + var node1 = chain.Nodes[0]; + + node1.MineBlocks(1); + + var network = chain.Nodes[0].CoreNode.FullNode.Network; + + var privateKey = new Key(); + string address = privateKey.PubKey.GetAddress(network).ToString(); + byte[] message = new byte[] { 0x69, 0x76, 0xAA }; + + // Make the signature with a key unrelated to the third party signer for the contract. + byte[] signature = SignMessage(new Key(), message); + + // TODO: If the incorrect parameters are passed to the constructor, the contract does not get properly created ('Method does not exist on contract'), but a success response is still returned? + + byte[] contract = ContractCompiler.CompileFile("SmartContracts/EcRecoverContract.cs").Compilation; + string[] createParameters = new string[] { string.Format("{0}#{1}", (int)MethodParameterDataType.Address, address) }; + BuildCreateContractTransactionResponse createResult = node1.SendCreateContractTransaction(contract, 1, createParameters); + + Assert.NotNull(createResult); + Assert.True(createResult.Success); + + node1.WaitMempoolCount(1); + node1.MineBlocks(1); + + string[] callParameters = new string[] + { + string.Format("{0}#{1}", (int)MethodParameterDataType.ByteArray, message.ToHexString()), + string.Format("{0}#{1}", (int)MethodParameterDataType.ByteArray, signature.ToHexString()) + }; + + BuildCallContractTransactionResponse response = node1.SendCallContractTransaction("CheckThirdPartySignature", createResult.NewContractAddress, 1, callParameters); + Assert.NotNull(response); + Assert.True(response.Success); + + node1.WaitMempoolCount(1); + node1.MineBlocks(1); + + ReceiptResponse receipt = node1.GetReceipt(response.TransactionId.ToString()); + + Assert.NotNull(receipt); + Assert.True(receipt.Success); + Assert.Equal("False", receipt.ReturnValue); + } + } + + // 2) That we can enable the method in new contracts without affecting the older contracts + + // TODO + } +} \ No newline at end of file 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/SmartContracts/EcRecoverContract.cs b/src/Stratis.SmartContracts.IntegrationTests/SmartContracts/EcRecoverContract.cs new file mode 100644 index 0000000000..2e40d6ef2f --- /dev/null +++ b/src/Stratis.SmartContracts.IntegrationTests/SmartContracts/EcRecoverContract.cs @@ -0,0 +1,28 @@ +using Stratis.SmartContracts; +using EcRecover = Stratis.SCL.Crypto.ECRecover; + +public class EcRecoverContract : SmartContract +{ + public Address ThirdPartySigner + { + get + { + return this.State.GetAddress(nameof(this.ThirdPartySigner)); + } + set + { + this.State.SetAddress(nameof(this.ThirdPartySigner), value); + } + } + + public EcRecoverContract(ISmartContractState state, Address thirdPartySigner) : base(state) + { + this.ThirdPartySigner = thirdPartySigner; + } + + public bool CheckThirdPartySignature(byte[] message, byte[] signature) + { + EcRecover.TryGetSigner(message, signature, out Address signerOfMessage); + return (signerOfMessage == this.ThirdPartySigner); + } +} \ No newline at end of file diff --git a/src/Stratis.SmartContracts.IntegrationTests/Stratis.SmartContracts.IntegrationTests.csproj b/src/Stratis.SmartContracts.IntegrationTests/Stratis.SmartContracts.IntegrationTests.csproj index b538256a41..81e45e9e43 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 @@ -93,6 +93,9 @@ PreserveNewest + + Always + PreserveNewest diff --git a/src/Stratis.SmartContracts.Networks/Stratis.SmartContracts.Networks.csproj b/src/Stratis.SmartContracts.Networks/Stratis.SmartContracts.Networks.csproj index 3ec5aaaf66..3939480c03 100644 --- a/src/Stratis.SmartContracts.Networks/Stratis.SmartContracts.Networks.csproj +++ b/src/Stratis.SmartContracts.Networks/Stratis.SmartContracts.Networks.csproj @@ -1,9 +1,9 @@  - netcoreapp3.1 + net6.0 - 2.0.1.0 + 2.1.0.0-beta 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..95038bde34 100644 --- a/src/Stratis.SmartContracts.RuntimeObserver/Stratis.SmartContracts.RuntimeObserver.csproj +++ b/src/Stratis.SmartContracts.RuntimeObserver/Stratis.SmartContracts.RuntimeObserver.csproj @@ -1,9 +1,9 @@  - netcoreapp3.1 + net6.0 - 2.0.0.0 + 2.1.0.0-beta 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..f2bd999ece 100644 --- a/src/Stratis.SmartContracts.Tests.Common/Stratis.SmartContracts.Tests.Common.csproj +++ b/src/Stratis.SmartContracts.Tests.Common/Stratis.SmartContracts.Tests.Common.csproj @@ -1,9 +1,9 @@  - netcoreapp3.1 + net6.0 - 2.0.1.0 + 2.1.0.0-beta diff --git a/src/Stratis.StraxD/Program.cs b/src/Stratis.StraxD/Program.cs index 2371b470cc..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(false) .AddRPC() .AddSignalR(options => { diff --git a/src/Stratis.StraxD/Stratis.StraxD.csproj b/src/Stratis.StraxD/Stratis.StraxD.csproj index f131ddacef..4db4a3a21d 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.5.0.0 + 1.6.0.0 Stratis Group Ltd. diff --git a/src/Stratis.StraxDnsD/Stratis.StraxDnsD.csproj b/src/Stratis.StraxDnsD/Stratis.StraxDnsD.csproj index 4246a57d4d..e754cc9252 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.5.0.0 + 1.6.0.0 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